Flutter TabBar and SliverAppBar that hides when you scroll down

Issue

I am trying to create an app with a top application bar and a tab bar below. When you scroll down, the bar should hide by moving off the screen (but tabs should stay), and when you scroll back up, the application bar should show again. This behaviour can be seen in WhatsApp. Please see this video for a demonstration. (Taken from Material.io). This is a similar behaviour, although the app bar and tab bar are hidden on scroll, so it is not exactly the behaviour I am looking for.

I have been able to achieve the autohiding, however, there are a few issues:

  1. I have to set the snap of the SliverAppBar to true. Without this, the application bar will not show when I scroll back up.

    Although this is works, it is not the behaviour I am looking for. I want the application bar to show smoothly (similar to WhatsApp) rather than coming into view even if you scroll very little.

    To clarify, when I scroll all the way down, even if I scroll up very little, the app bar should come into view. I do not want to have to scroll all the way up to see the app bar.

  2. When I scroll down and change tabs, a little bit of the content is cut out of view.

    Below is a GIF showing the behaviour:

    GIF demonstrating output

    (See the part when I scroll down on the listView (tab1), then move back to tab2)

Here is the code for the DefaultTabController:

DefaultTabController(
  length: 2,
  child: new Scaffold(
    body: new NestedScrollView(
      headerSliverBuilder:
          (BuildContext context, bool innerBoxIsScrolled) {
        return <Widget>[
          new SliverAppBar(
            title: Text("Application"),
            floating: true,
            pinned: true,
            snap: true,    // <--- this is required if I want the application bar to show when I scroll up
            bottom: new TabBar(
              tabs: [ ... ],    // <-- total of 2 tabs
            ),
          ),
        ];
      },
      body: new TabBarView(
        children: [ ... ]    // <--- the array item is a ListView
      ),
    ),
  ),
),

In case it is needed, the full code is in this GitHub repository. main.dart is here.

I also found this related question: Hide Appbar on Scroll Flutter?. However, it did not provide the solution. The same problems persist, and when you scroll up, the SliverAppBar will not show. (So snap: true is required)

I also found this issue on Flutter’s GitHub. (Edit: someone commented that they are waiting for the Flutter team to fix this. Is there a possibility that there is no solution?)

This is the output of flutter doctor -v: Pastebin. Certain issues are found, but from what I have learned, they should not have an impact.

Edit: There are two issues for this:

Solution

Update – Sliver App Bar Expanded

If you want to see Sliver App Bar expanded as soon as someone scrolls up i.e. not scrolling all the way to top but just little bit, Then just change snap: false to snap: true in code 🙂


Solution [Fixing All Points]

After surfing google, stackoverflow, github issues, reddit for hours. I could finally come up with a solution that addresses following:

  1. Sliver App bar with title getting hidden and only tab bar visible after scrolling down. You would see title again when you reach top.

  2. MAJOR : When you scroll in Tab 1 & then Navigate to Tab 2, you would not see any overlap. The content of Tab 2 will not get obstructed by Sliver App bar.

  3. Sliver Padding for top most element in List is 0.

  4. Preserves the Scroll Position in individual Tabs

below is the code, I would attempt to explain in a bit (dartpad preview) :

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Flutter Code Sample';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatelessWidget(),
    );
  }
}

class MyStatelessWidget extends StatelessWidget {
  const MyStatelessWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final List<String> _tabs = <String>['Tab 1', 'Tab 2'];
    return DefaultTabController(
      length: _tabs.length,
      child: Scaffold(
        body: NestedScrollView(
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return <Widget>[
              SliverOverlapAbsorber(
                handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                sliver: SliverAppBar(
                  title: const Text('Books'),
                  floating: true,
                  pinned: true,
                  snap: false,
                  forceElevated: innerBoxIsScrolled,
                  bottom: TabBar(
                    tabs: _tabs.map((String name) => Tab(text: name)).toList(),
                  ),
                ),
              ),
            ];
          },
          body: TabBarView(
            children: _tabs.map((String name) {
              return SafeArea(
                top: false,
                bottom: false,
                child: Builder(
                  builder: (BuildContext context) {
                    return CustomScrollView(
                      key: PageStorageKey<String>(name),
                      slivers: <Widget>[
                        SliverOverlapInjector(
                          handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                        ),
                        SliverPadding(
                          padding: const EdgeInsets.all(8.0),
                          sliver: SliverList(
                            delegate: SliverChildBuilderDelegate(
                              (BuildContext context, int index) {
                                return ListTile(
                                  title: Text('Item $index'),
                                );
                              },
                              childCount: 30,
                            ),
                          ),
                        ),
                      ],
                    );
                  },
                ),
              );
            }).toList(),
          ),
        ),
      ),
    );
  }
}

Test it out all you want in dartpad, once you are fine then lets try to understand what is happening here.

Most of the code is from flutter documentation of NestedScrollView

They have mentioned very nicely in comments. I am no expert so I would just highlight what I think solved most of the issues.

I believe Two things are critical here:

  1. SliverOverlapAbsorber & SliverOverlapInjector
  2. Use of SliverList instead of ListView

Whatever extra space we were seeing or the space which sliver app bar consumed and first list item was overlapped was mainly resolved with the use of above two points.

To remember the scroll position of tabs, they added PageStorageKey inside CustomScrollView:

key: PageStorageKey<String>(name),

name is just a string -> ‘Tab 1’

They also mentioned in docs that we can use SliverFixedExtentList, SliverGrid, basically Sliver widgets. Now use of Sliver widgets should be done when needed. In one of the Flutter Youtube videos (official channel) they mentioned that ListView, GridView, are all high level implementation of Slivers. So Slivers is low level stuff if you looking to super customize scrolling or appearance behaviour.

Please let me know in comments if I missed something or said wrong.

Answered By – krupesh Anadkat

Answer Checked By – Marie Seifert (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *