Instagram Profile Header Layout In Flutter

6,104

Solution 1

You can achieve this behaviour using NestedScrollView with Scaffold.

As we need the widgets between the AppBar and TabBar to be dynamically built and scrolled until TabBar reaches AppBar, use the appBar property of the Scaffold to build your AppBar and use headerSliverBuilder to build other widgets of unknown heights. Use the body property of NestedScrollView to build your tab views.

This way the elements of the headerSliverBuilder would scroll away till the body reaches the bottom of the AppBar.

Might be a little confusing to understand with mere words, here is an example for you.

Code:

// InstaProfilePage
class InstaProfilePage extends StatefulWidget {
  @override
  _InstaProfilePageState createState() => _InstaProfilePageState();
}

class _InstaProfilePageState extends State<InstaProfilePage> {
  double get randHeight => Random().nextInt(100).toDouble();

  List<Widget> _randomChildren;

  // Children with random heights - You can build your widgets of unknown heights here
  // I'm just passing the context in case if any widgets built here needs  access to context based data like Theme or MediaQuery
  List<Widget> _randomHeightWidgets(BuildContext context) {
    _randomChildren ??= List.generate(3, (index) {
      final height = randHeight.clamp(
        50.0,
        MediaQuery.of(context).size.width, // simply using MediaQuery to demonstrate usage of context
      );
      return Container(
        color: Colors.primaries[index],
        height: height,
        child: Text('Random Height Child ${index + 1}'),
      );
    });

    return _randomChildren;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // Persistent AppBar that never scrolls
      appBar: AppBar(
        title: Text('AppBar'),
        elevation: 0.0,
      ),
      body: DefaultTabController(
        length: 2,
        child: NestedScrollView(
          // allows you to build a list of elements that would be scrolled away till the body reached the top
          headerSliverBuilder: (context, _) {
            return [
              SliverList(
                delegate: SliverChildListDelegate(
                  _randomHeightWidgets(context),
                ),
              ),
            ];
          },
          // You tab view goes here
          body: Column(
            children: <Widget>[
              TabBar(
                tabs: [
                  Tab(text: 'A'),
                  Tab(text: 'B'),
                ],
              ),
              Expanded(
                child: TabBarView(
                  children: [
                    GridView.count(
                      padding: EdgeInsets.zero,
                      crossAxisCount: 3,
                      children: Colors.primaries.map((color) {
                        return Container(color: color, height: 150.0);
                      }).toList(),
                    ),
                    ListView(
                      padding: EdgeInsets.zero,
                      children: Colors.primaries.map((color) {
                        return Container(color: color, height: 150.0);
                      }).toList(),
                    )
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Output:

Output Image

Hope this helps!

Solution 2

Another solution is that you could use a pinned SliverAppBar with FlexibleSpaceBar within DefaultTabController. Sample codes:

  Scaffold(
    body: DefaultTabController(
      length: 2,
      child: NestedScrollView(
        headerSliverBuilder: (context, value) {
          return [
            SliverAppBar(
              floating: true,
              pinned: true,
              bottom: TabBar(
                tabs: [
                  Tab(text: "Posts"),
                  Tab(text: "Likes"),
                ],
              ),
              expandedHeight: 450,
              flexibleSpace: FlexibleSpaceBar(
                collapseMode: CollapseMode.pin,
                background: Profile(), // This is where you build the profile part
              ),
            ),
          ];
        },
        body: TabBarView(
          children: [
            Container(
              child: ListView.builder(
                itemCount: 100,
                itemBuilder: (context, index) {
                  return Container(
                    height: 40,
                    alignment: Alignment.center,
                    color: Colors.lightBlue[100 * (index % 9)],
                    child: Text('List Item $index'),
                  );
                },
              ),
            ),
            Container(
              child: ListView.builder(
                itemCount: 100,
                itemBuilder: (context, index) {
                  return Container(
                    height: 40,
                    alignment: Alignment.center,
                    color: Colors.lightBlue[100 * (index % 9)],
                    child: Text('List Item $index'),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    ),
  ),

Before scrolling:

enter image description here

After scrolling:

enter image description here

Share:
6,104
Ted Henry
Author by

Ted Henry

Struggling along building mobile apps.

Updated on December 01, 2022

Comments

  • Ted Henry
    Ted Henry over 1 year

    I've been investigating SliverAppBar, CustomScrollView, NestedScrollView, SliverPersistentHeader, and more. I cannot find a way to build something like the Instagram user profile screen's header where only the tab bar is pinned. The main body of the screen is a TabBarView and each pane has a scrollable list.

    With SliverAppBar, it is easy to add the TabBar in the bottom parameter. But I want to have an extra widget of unknown/variable height above that TabBar. The extra widget should scroll out of the way when the page is scrolled and and then the TabBar is what is pinned at the top of the screen.

    enter image description here

    All I could manage was a fixed content before the tab bar and a fixed tab bar. I cannot get the header to scroll up and stick the TabBar at the top just just below the AppBar.

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(MaterialApp(home: MyApp()));
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return DefaultTabController(
          length: 2,
          child: Scaffold(
            appBar: AppBar(
              title: Text("pabloaleko"),
            ),
            body: CustomScrollView(
              physics: const BouncingScrollPhysics(),
              slivers: <Widget>[
                SliverToBoxAdapter(
                  child: SafeArea(
                    child: Text("an unknown\namount of content\n goes here in the header"),
                  ),
                ),
                SliverToBoxAdapter(
                  child: TabBar(
                    tabs: [
                      Tab(child: Text('Days', style: TextStyle(color: Colors.black))),
                      Tab(child: Text('Months', style: TextStyle(color: Colors.black))),
                    ],
                  ),
                ),
                SliverFillRemaining(
                  child: TabBarView(
                    children: [
                      ListView(
                        children: <Widget>[
                          ListTile(title: Text('Sunday 1')),
                          ListTile(title: Text('Monday 2')),
                          ListTile(title: Text('Tuesday 3')),
                          ListTile(title: Text('Wednesday 4')),
                          ListTile(title: Text('Thursday 5')),
                          ListTile(title: Text('Friday 6')),
                          ListTile(title: Text('Saturday 7')),
                          ListTile(title: Text('Sunday 8')),
                          ListTile(title: Text('Monday 9')),
                          ListTile(title: Text('Tuesday 10')),
                          ListTile(title: Text('Wednesday 11')),
                          ListTile(title: Text('Thursday 12')),
                          ListTile(title: Text('Friday 13')),
                          ListTile(title: Text('Saturday 14')),
                        ],
                      ),
                      ListView(
                        children: <Widget>[
                          ListTile(title: Text('January')),
                          ListTile(title: Text('February')),
                          ListTile(title: Text('March')),
                          ListTile(title: Text('April')),
                          ListTile(title: Text('May')),
                          ListTile(title: Text('June')),
                          ListTile(title: Text('July')),
                          ListTile(title: Text('August')),
                          ListTile(title: Text('September')),
                          ListTile(title: Text('October')),
                          ListTile(title: Text('November')),
                          ListTile(title: Text('December')),
                        ],
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    enter image description here

  • Ted Henry
    Ted Henry over 4 years
    This is a nice solution.Why are "A" and "B" tab text showing up as white on white in my app but black on white in your app?
  • Hemanth Raj
    Hemanth Raj over 4 years
    Hey, sorry about that. I had my Theme's primary color set to Colors.white. It basically depends on the primaryColor and accentColor of your . Theme
  • kanwar manraj
    kanwar manraj over 3 years
    Hey!! @HemanthRaj I tried this method, but I am stuck with this issue if you please could check it out Bottom overflow due to bottom navigation bar and tab Bar stackoverflow.com/q/65429750/13406343?sem=2
  • daicky777
    daicky777 about 3 years
    How can you implement swipe refresh for the top NestedScrollView??