Effortless sticky headers in Flutter
In this article I want to show you how to implement sticky headers behaviour in Flutter without any packages. I will use slivers that are already in Flutter SDK.
This is how these sticky headers look like:
SliverMainAxisGroup
A sliver that places multiple sliver children in a linear array along the main axis, one after another.
How It Works:
- Offset Tracking: A running total of the space (scroll extent) occupied by the slivers that have been laid out is maintained.
- Setting Constraints for Next Child:
- The amount of space used from the start to the current offset is calculated.
- The remaining space for the next sliver is determined by subtracting the used space from the total available space.
- The scroll position for the next sliver is found by subtracting the current offset from the provided scroll position. If the result is negative, a value of 0.0 is used.
- Ensuring Slivers Stay Within Bounds: After all the slivers are laid out, an extra check is done to ensure each sliver remains within its designated space. If a sliver is outside its bounds, its position is adjusted.
- Finalizing the Geometry: The total space (scroll extent) needed for the group of slivers is set. The amount of this space that is currently visible (paint extent) and the maximum space that can be occupied are also determined.
For this article two simple models are used:
final class Section {
Section(
this.title,
this.items,
);
final List<Item> items;
final String title;
}
final class Item {
Item(this.content);
final String content;
}
Some test data:
final _data = List.generate(
20,
(index) => Section(
'Section $index',
List.generate(
10,
(index) => Item('Item $index'),
),
),
);
SliverMainAxisGroup places all children into a "container" and manages their layout. To achieve stickiness, it is necessary to wrap the items and header widget with this container.
This is how it looks like:
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
for (final section in _data)
SliverMainAxisGroup(
slivers: [
SliverAppBar(
pinned: true,
title: Text(
section.title,
style: Theme.of(context).textTheme.bodyLarge,
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(
title: Text(section.items[index].content),
),
childCount: section.items.length,
),
),
],
),
],
),
);
}
In each axis group, a SliverAppBar is created to represent the header and a SliverList is created to represent the items.
Thanks for reading
Thank you for reading me! Subscribe to my free newsletter to never miss articles like this. For telegram lovers, I created channel as well: https://t.me/mdevnotes. If you have any questions, feel free to join the discussion!
All necessary links and code can be found below. See you soon!