Flutter SliverAppBar: Snap those headers!

Lê Dân
Flutter Community
Published in
4 min readAug 6, 2020

Let’s go on an adventure today.

Our quest is to implement a header with custom snapping behavior.

Something like this:

Getting Started

You can follow the article with the completed code here.

Since ListView doesn’t support scroll effects. To CustomScrollView we go!

First, we should check if there is any prebuilt way to do this.

SliverAppBar looks promising. It even has the snap property!

  • We give the SliverAppBar an expandedHeight which will allow the flexibleSpace widget to expand and collapse within the SliverAppBar.
  • We turn on the pinned and stretch effects.
  • We add an infinite load SliverList.
  • The status bar height is MediaQuery.of(context).padding.top
  • We have the maximum header height, maxHeight, which is 200 plus the status bar height. Therefore, the expandedHeight should be only 200 because the SliverAppBar already includes the status bar height internally.
  • We also have the minimum header height, minHeight, which is the constant kToolbarHeight plus the status bar height.

Usually, the flexibleSpace in the SliverAppBar is occupied by a FlexibleSpaceBar, but in our case, it is not customizable enough. So we will implement our own flexibleSpace widget called Header.

Header will have 2 parameters: maxHeight and minHeight, which indicates the header’s maximum height after expanding and minimum height after collapsing.

This widget, by using LayoutBuilder, will have access to the current size constraints given upon it by the parents. This means constraints.maxHeight will return the current height of the SliverAppBar.

We can then use this information to calculate the expand/collapse ratio.

Because all of the header effects are done with Tween, and Tween is designed to work with an Animation, we should then map the ratio to an AlwaysStoppedAnimation.

Then, with that animation value, we can interpolate the UI components, for example, here we animate the title from large-sized in the bottom-left to small-sized in the bottom center.

Our Header’s build function will look like this.

SliverAppBar’s snap property

We will now try the snap property of the SliverAppBar. One caveat is we also must enable floating.

Hmm, the behavior is not exactly what we want. Since floating must be enabled, the SliverAppBar can expand whenever the user scrolls down and overlay on top of the list.

Seems like we need to implement our own snap behavior.

We need to do 2 things:

  • Know when the user stops scrolling
  • Check to see if the header is closer to the expanded position or collapsed position, and scroll the list to that position.

To achieve the first thing, we can use a NotificationListener to listen to user scroll events.

There are multiple types of ScrollNotification:

And in our case, a ScrollEndNotification, which signifies a Scrollable has stopped scrolling, is sufficient.

We should put the NotificationListener above our CustomScrollView because the notifications will travel up the Widget tree from below.

Then, given the CustomScrollView’s ScrollController, let’s try to scroll the list to the start position, which is offset 0.

Weird, our callback is being fired, but the list is not moving!

Let’s check the ScrollNotification’s documentation to see if we are doing anything wrong.

When a scroll notification is received by a [NotificationListener], the listener will have already completed build and layout, and it is therefore too late for that widget to call [State.setState]

Turns out the ScrollEndNotification was called during the current build phase, which prevents the jumpTo command from updating the scroll position.

To solve this, we can wrap the function with Future.microTask() to run the callback after the build ends.

Voilà!

The next step is to check the header size for expanding/collapsing.

We observe that the distance to scroll from the expanded position to the collapsed position is exactly the difference between the maximum header height and minimum header height.

We can then check whether the current offset is closer to which end and scroll the list to that position.

Finally! The header is snapping the way we wanted!

What happens when the list is short?

Here when there are only 3 items, our effect is not working because the list determined that it does not need to scroll anywhere.

All we have to do is add an AlwaysScrollablePhysics to the list. This means telling the list, hey, no matter what happens, just scroll.

Showing empty or error status

Let’s say when the list is empty, we want to display a message at the center.

We can do this by replacing the SliverList with a SliverFillRemaining, which always fills the remaining space in the list.

The collapsed position is used to allow space for the long list below. But in this case, it does not make sense because there is no long list.

So we should prevent that.

In _snapAppBar(), we can check if the list is empty, and always scroll to the expanded position.

But we don’t have to because SliverFillRemaining has a hasScrollBody property that indicates whether it should be treated as a long scrollable list. Turning this off will do the trick.

Recap

By using NotificationListener and a few layout calculations, our SliverAppBar can now snap at the beginning of the list without needing to float all around the places.

Moreover, our snapping behavior also supports short lists, and cases where we need to display empty or error states.

That’s all for now folks.

See you on the next adventure!

https://www.twitter.com/FlutterComm

--

--