Custom Flutter SliverAppBar Behavior
Flutter SliverAppBar The Complete Guide - Part 1
A guide for creating custom SliverAppBar behaviors in Flutter
TOC
- Flutter SliverAppBar The Complete Guide - Part 1
Introduction ℹ️
Expanded app bar (Collapsing Toolbar) is one of the material design app bar behaviors that is widely used to hide app bar content when scrolling up. A simple use case is AppBar which might show a full profile picture when the user scrolls down and slowly transition to show only the user name when the user scrolls up.
In Flutter we can achieve the same behavior using the SliverAppBar widget. As the widget name suggests it could only be used inside the CustomScrollView widget. This widget helps you in creating various scrolling effects such as lists, grids, and expanding headers by supplying slivers (SliverAppBar, SliverList, SliverGrid …) directly to it.
But wait … What the heck is sliver? 🧐
A sliver is just a portion of a scrollable area. That’s it! All of the scrollable views you use, like ListView and GridView, are actually implemented using Slivers. You can kind of think of Slivers as a lower-level interface, providing finer-grained control on implementing scrollable areas. Because slivers can lazily build each item just as it scrolls into view, slivers are particularly useful for efficiently scrolling through large numbers of children. - Slivers, Demystified
What We Are Going to Build? 👀
We are going to build the following custom behavior:
Let’s Discover SliverAppBar Widget! 🕵🏼
SliverAppBar( pinned: _pinned, snap: _snap, floating: _floating, expandedHeight: 160.0, collapsedHeight: 80.0, flexibleSpace: const FlexibleSpaceBar( title: Text('SliverAppBar'), background: FlutterLogo(), ), ),
As mentioned previously this widget is used directly in the CustomScrollView and it contains helpful properties to customize its behavior.
The most important properties are:
We will focus on these properties:
collapsedHeight
: a double value that defines the height of the app bar when it’s collapsed.expandedHeight
: a double value that defines the height of the app bar when it’s expanded.flexibleSpace
: This widget is stacked behind the toolbar and the tab bar and it’s usually used with theFlexibleSpaceBar
widget. The FlexibleSpaceBar widget contains properties to customize the space when the app bar is expanded.
We will focus on two properties:
-
The
title
is a widget that gets animated (scaled up/down) when collapsing/expanding the app bar. -
The
background
is a widget that fades out when the app bar is collapsed. -
There are other properties that control the behavior of the collapsed app bar whether it’s
pinned
,floating
,snap
, and more. To learn more check the official documentation.
Let’s Build it! 🏗
First of all, we will use the
flutter_hooks
package to make it easier to manage the local state and deal with the ScrollControllers. But of course, you can useStatefulWidget
andsetState
or any other local state management solution.Let’s start by adding theStack widget to add the movie background image and the CustomScrollView widget to be able to use the SliverAppBar.
class MovieProfilePage extends HookWidget { const MovieProfilePage({super.key, required this.movieDetails}); final MovieDetails movieDetails; @override Widget build(BuildContext context) { return Stack( children: [ /// The background image BlurredBackdropImage(movieDetails: movieDetails), CustomScrollView( slivers: [ ], ), ], ); } }
Now let’s add the SliverAppBar inside the CustomScrollView.
@override Widget build(BuildContext context) { const collapsedBarHeight = 60.0; const expandedBarHeight = 400.0; return Stack( children: [ /// Blurred Background Image Widget BlurredBackdropImage(movieDetails: movieDetails), CustomScrollView( slivers: [ SliverAppBar( expandedHeight: expandedBarHeight, collapsedHeight: collapsedBarHeight, centerTitle: false, pinned: true, /// 1 title: CollapsedAppBarContent(movieDetails: movieDetails), elevation: 0, /// 2 backgroundColor: Colors.transparent, leading: const BackButton( color: Colors.white, ), /// 3 flexibleSpace: FlexibleSpaceBar( background: ExpandedAppBarContent( movieDetails: movieDetails, ), ), ), /// 4 SliverToBoxAdapter( child: ConstrainedBox( constraints: BoxConstraints( minHeight: MediaQuery.of(context).size.height, ), child: Material( elevation: 7, borderRadius: const BorderRadius.only( topLeft: Radius.circular(15), topRight: Radius.circular(15), ), child: PageBodyWidget(movieDetails: movieDetails), ), ), ) ], ), ], ); } }
- Adds the
CollapsedAppBarContent
widget that contains the topRow
to the title field. The title widget will always stay visible. - Changes the AppBar
background
color totransparent
to show theBlurredBackdropImage
widget. - Adds
FlexibleSpaceBar
widget and assigns theExpandedAppBarContent
widget to the background field. Thebackground
field content fades out when the app bar collapsed, and this is exactly what we need! - Adds
SliverToBoxAdapter
widget to be able to add the body content. Simply, what theSliverToBoxAdapter
does is just a bridge between slivers and regular widgets (i.e. box-based widgets). Since theCustomScrollView
can only have slivers objects as direct descendants.
The app bar now would look like this:
Now if we analyze the required design we would find out that we have the
CollapsedAppBarContent
widget is overlapping theExpandedAppBarContent
widget. Also when the app bar is collapsed we need to change the background color to black instead of transparent.But how could we achieve this behavior? 👀
First of all, we need to know whether the AppBar is collapsed or expanded; based on this we could show the proper content of the AppBar.
To do this, we could use the
NotificationListener
widget and listen to theScrollNotifications
to detect the changes in the scroll offset. Based on that, we could know if the app bar is either collapsed or expanded.@override Widget build(BuildContext context) { ... /// 1 final scrollController = useScrollController(); final isCollapsed = useState(false); return NotificationListener<ScrollNotification>( onNotification: (notification) { /// 2 isCollapsed.value = scrollController.hasClients && scrollController.offset > (expandedBarHeight - collapsedBarHeight); return false; }, child: Stack( children: [ BlurredBackdropImage(movieDetails: movieDetails), CustomScrollView( controller: scrollController, slivers: [ SliverAppBar( expandedHeight: expandedBarHeight, collapsedHeight: collapsedBarHeight, centerTitle: false, pinned: true, /// 3 title: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: isCollapsed.value ? 1 : 0, child: CollapsedAppBarContent(movieDetails: movieDetails), ), elevation: 0, /// 4 backgroundColor: isCollapsed.value ? Colors.black : Colors.transparent, leading: const BackButton( color: Colors.white, ), flexibleSpace: FlexibleSpaceBar( background: ExpandedAppBarContent( movieDetails: movieDetails, ), ), ), ... ], ), ], ), ); }
- Uses two hooks,
useScrollController()
hook to createScrollController
and auseState()
hook with afalse
initial value to create theisCollapsed
state object. TheisCollapsed
flag will be used to update app bar content based on its value. - Updates the
isCollapsed
state value by checking that thescrollController
object is attached to the scroll view. Moreover, it checks whether the current scroll offset is larger than the difference between theexpandedBarHeight
andcollapsedBarHeight
. - Wraps the
CollapsedAppBarContent
widget withAnimatedOpacity
widget to hide and show the content based on theisCollapsed
value. - Changes the
backgroundColor
based on theisCollapsed
value.
The app bar now would look like this:
And hurray we did it! 🎉🎊
Bouns 🌟
We could also add haptic feedback when the app bar is collapsed to further improve the user experience. We can do this by using the HapticFeedback class, which allows access to the haptic feedback interface on the device.
@override Widget build(BuildContext context) { ... final didAddFeedback = useState(false); return NotificationListener<ScrollNotification>( onNotification: (notification) { ... /// When the app bar is collapsed and the feedback /// hasn't been added previously will invoke /// the `mediumImpact()` method, otherwise will /// reset the didAddFeedback value. /// if (isCollapsed.value && !didAddFeedback.value) { HapticFeedback.mediumImpact(); didAddFeedback.value = true; } else if (!isCollapsed.value) { didAddFeedback.value = false; } return false; }, child: Stack( children: [ ... ], ), ], ), ); }
We added another hook state variable named didAddFeedback based on it will call the
HapticFeedback.mediumImpact()
method.Wrap up 🏁
The slivers help you to customize the scrolling behavior and to build complex scrolling effects. Mastering slivers will enable you to improve the user interface and, more importantly, the experience.
Thanks for reading! ❤️
GitHub