Dynamic Header in Flutter App
How to make AppBar with dynamic height using Sliver in Flutter
*Some sentences are generated by AI 🚀 🚀 🚀
Flutter AppBar is a Material Design app bar that consists of a toolbar and potentially other widgets, such as a TabBar and a FlexibleSpaceBar. It is usually placed at the top of the screen and has the ability to contain other widgets within its layout. The AppBar commonly displays branding information such as logos and titles and often contains buttons or other points of user interaction.
The AppBar can be customized by changing its background color, elevation, leading widget, title widget, actions widget, etc.
Here’s an example of how to use the background-color property to change the background color of an AppBar:
appBar: AppBar (
title: const Text ('Title'),
backgroundColor: Colors.green,
),
In this article, we will explore about SliverAppBar to make a dynamic header on Flutter. By the end, you will know how to make a screen like the one below:
If we slice the screen above, we can divide it into 3 parts.
- Collapsed AppBar
- Expanded AppBar
- Body
Did you notice that the body and the AppBar are scrollable? 😲
So here there are 2 combined scrollable widgets. In Flutter usually, we can't use 2 scrollable widgets on a screen. It will throw an error.
For this issue, we need to use NestedScrollView or CustomScrollView widget to handle 2 scrollable widgets. But to really understand this workaround, we need to learn about slivers in Flutter.
Do you familiar with sliver (not silver) in Flutter?
A sliver is a portion of a scrollable area, which means anything that scrolls in Flutter is a sliver. If you’ve used ListView
or GridView
, congratulations! You've already used a sliver in your Flutter app. Most often, slivers are wrapped in convenience classes such as these. This is because slivers use a different layout protocol from most other widgets in the Flutter framework. [ref]
As a novice in Flutter, I usually use ListView
or SingleChildScrollview
in my apps. And for complex UI like nested scroll widgets, ListView
is not enough. That's why we need to learn more about sliver.
To learn about Sliver in Flutter, you can follow this awesome workshop:
DartPad Workshops Getting started with slivers
ok back to the screen…
We will use NestedScrollView and SliverAppBar( you can find them in the workshop step 5).
Now we have to listen to the ScrollController so that we know when to hide the expanded AppBar and then show the collapsed AppBar and vice versa. We will have the StatefullWidget because we need to initialize our scrollController.
class DynamicAppbar extends StatefulWidget {
const DynamicAppbar({super.key});
@override
State<DynamicAppbar> createState() => _DynamicAppbarState();
}
class _DynamicAppbarState extends State<DynamicAppbar> {
....
Then define all variables that we need.
class _DynamicAppbarState extends State<DynamicAppbar> {
late ScrollController _scrollController;
bool lastStatus = true;
double height = 390;
...
_scrollController
is the controller that we will use later on NestedScrollView.
lastStatus
is the condition that we listen for the position of the scroll widget.
and last the height
we use for the expanded AppBar. You can define it as you need. In this example, I need about 390 pixels to show all my content in the AppBar when it's expanded.
Now we need to create a condition for whether the AppBar will be collapsed or expanded. lets called it as _isShrink.
bool get _isShrink {
return _scrollController.hasClients &&
_scrollController.offset > (height - kToolbarHeight);
}
we return value based on scroll offset by the difference between the toolbar and AppBar.
This is the toolbar, or some people name it the status bar.
why toolbar?
Because when we display widgets on the screen, the starting point is from the top left of the screen (including the toolbar). That’s why Flutter provides a widget named SafeArea
widget. This widget is useful when we want to place a widget that is displayed between the statusbar
and bottombar.
Next, create the listener function and set it to the lastStatus
value.
void _scrollListener() {
if (_isShrink != lastStatus) {
setState(() {
lastStatus = _isShrink;
});
}
}
and we use the function for the Scrollcontroller listener
@override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_scrollListener);
}
we had initialized the scroll controller. Before we jump to the build
method, don't forget to dispose
the controller.
Because…. why not 😆 😆. It’s good for memory when removing unused listeners.
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
ok, we had done for the variable and listener. Now we start building widgets.
This is a long code for the whole view. I will point only to the important part. For the rest of the code, you can find it in the repository later that I provided.
- first, in the body of
Scaffold
, we use theNestedScrollView
widget and implement the_scrollController
that we define before. I also use theSafeArea
widget 😃
body: SafeArea(
child: NestedScrollView(
controller: _scrollController,
....
- In
SliverAppbar
we use_isShrink
variable to determine which widget we have to show.
flexibleSpace: FlexibleSpaceBar(
title: _isShrink
? const Text('pmatatias Statistic',
style: TextStyle(fontSize: 16))
: const SizedBox(),
So…
basically, in this state, we show the AppBar which is collapsed when state is shrunk. Otherwise we show SizedBox which means it displays nothing on the screen.
Instead of displaying the title, we display the FlexibleSpaceBar background. This Container contains all widget that shows when the AppBar is expanded.
background: Container(
margin: const EdgeInsets.only(top: kToolbarHeight-16),
padding: const EdgeInsets.only(left: 8),
child: Column(
...
- And last the body widget. Here I will use ListView.
By default, it sets the minimum height equal to the screen height. To add some decorations like rounded borders and background color, I wrap the ListView in a Container widget.
Container(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 10),
decoration: const BoxDecoration(
color: Palette.myrtleGreen,
borderRadius: BorderRadius.only(
topRight: Radius.circular(30), topLeft: Radius.circular(30))),
child: ListView(
.... rest of widget body
Finally, we need to combine all the pieces. It looks like below:
Ps: for some extracted widgets you may found in the repository GitHub here:
ck-linecode/lib/dynamic_appbar at main · pmatatias/ck-linecode · GitHub
🙏Thank you for having the end. Don't forget to clap 👏 if you like this article. 😃