Scrolling — almost every app needs it, and almost nobody wants to learn it. Think of every time you read about issues related to scrolling not working as intended and how they should be fixed. And don't even get me started on slivers 🪄, the it-which-must-not-be-named in the magical world of Flutter. But in case you ever get in trouble, you can use this write-up as an entrance point to the wonderful and sometimes strenuous process of designing scrolling in Flutter.
Everything is a widget
Let’s start with a recap.
As you may have heard, in Flutter, “everything is a widget.” One can use widgets to do the primitive stuff, like display, offset, and style something on the screen. You might want to use widgets like
Text. To lay out widgets horizontally or vertically, or in whatever fashion you like, you also use widgets that help with that. Widgets like
Stack focus on layouting multiple-child widgets on the screen, while others, such as
Padding, deal with layouting single-child widgets. Sometimes, you have to use more advanced widgets to handle overflowing, mainly to efficiently display large horizontal or vertical content. Such widgets handle scrolling for you, and many of them use remarkably efficient algorithms, so you don’t end up rendering all children but only those you actually need.
Let’s talk about constraints
All widgets have something in common, but let’s focus on box constraints. As you probably know, constraints in Flutter work differently from the web, to give an example. Flutter constraints provide an efficient way to lay out and display widgets on the screen. As the Flutter docs say, “Constraints go down. Sizes go up. Parent sets position.” That is the bread and butter of constraints in Flutter.
In practice, each widget asks its parent for constraints, and it provides them with minimal and maximal height and width. The process repeats for all childen of this widget. Then, the widget positions its child widgets and tells a parent about its size. And so on. This describes the quote from the Flutter docs. As a result, the widget only calls for the desired size, but the actual sizing and aligning are done by its parent.
Due to properties of Flutter constraints, when you lay out a
Row stretched to the whole screen with two
Containerchildren, where the former sets its height to 100 and the latter to 200, children are rendered with max height, not considering the manually set heights. That’s exactly how it should work based on the quote and previous explanation. But, sometimes, you want to enforce that the size is truly what you set up. You have two basic options to consider here. You either set
[IntrinsicHeight] above the
Row. Both will limit the height for your needs, but with the former, you have to specify the min-height. While using the latter, it magically works without any further input, and the height is set as the max height of the children. While the latter approach sounds easier because it works by itself, be aware that it does some additional computation that makes the algorithm
O(N2), so use it with caution.
As has already been mentioned, to lay out widgets, we use widgets that can lay out single-child or multi-child widgets. While
Column works amazingly well for layout widgets in a column, when you reach the limit of its parent constraints, an error like
A RenderFlex overflowed by 154 pixels on the bottom. will appear. That tells you the widget cannot lay out its children according to its constraints and children’s sizes.
If you’re coming straight from web development, this might seem very weird to you. There, scrolling is automatically available on the whole document, or you can set up heights and overscroll to enable it on any element. You don’t distinguish if the element is scrollable or not; everything can be set up. This may be a bit easier for beginners, but in the end, the whole layout and rendering process of the website performs worse than Flutter.
Here, I just want to pay respect to Flutter for providing wonderful error messages, coupled with several possible reasons and suggestions on how to solve the problem.
In Flutter, if you want to enable scrolling, you must wrap the desired widget in a
[Scrollable]. That widget implements all interactions and gesture recognition, among other things. Most of the time, you will use a descendant of that widget, like
[ListView]. To keep it simple, you can wrap the desired widget in
[SingleChildScrollView], which enables scrolling by wrapping it as its child.
Although technically feasible, don’t just wrap a
SingleChildScrollView. This scrollable widget has no optimization, and you will render everything every time. Imagine that you have a list of hundreds or thousands of widgets. Instead, use another scrollable widget descendant, like
ListView. By using virtualization, this widget substantially improves performance, but it still has to compute for the visibility of every child. And so, to reach peak performance, it’s preferred to use
ListView.builder, where children are built on demand by specifying an
IndexedWidgetBuilder, which returns a widget for concrete index. With this constructor, you can have a virtually infinite number of children in the list, and all the computing only happens for the visible ones, possibly for a couple of before and after that. Hence, scrolling is faster using precomputed data.
Principle of virtual scrolling — only visible items are rendered. (Virtual scrolling | LogRocket)
Don’t forget to check the scrolling widgets catalog. There, you can find additional scroll widgets, configurations, notifications, and many other useful tools.
Now, let’s dig further down our rabbit hole and explore one particular word that instills fear in many coders: slivers.
Slivers are also widgets, because everything is a widget.
Under the hood, they’re used for every scrollable, such as
ListView. While slivers are a low-level interface, they are essential for creating custom animations, scrolling control or unifying scroll across multiple scrollable areas. Here’s a neat definition:
Sliver widgets differ from others by not being represented as boxes and not having box constraints. Standard widgets are based on
[RenderBox], and to lay them out,
BoxConstraints are passed to it; the final size is calculated from that. Sliver widgets are based on
[RenderSliver], and during layout, each sliver receives a
SliverConstraints, and based on that,
SliverGeometry is computed. Slivers also need to know where they are on the scrollable. As explained when discussing constraints, regular box widgets don’t care about their position.
RenderSlivers are protocols for rendering based on
RenderObject. Since this article only covers the essentials of scrolling, we might take a more careful look into it next time.
To render slivers, use
CustomScrollView and pass your slivers inside the
slivers property. Slivers are built lazily when they appear on the screen in the viewport. That is beneficial performance-wise, even with all the effects based on scrolling you can do with them. While slivers implement
Widget, you cannot pass a non-sliver widget into the
slivers property. The analysis won’t tell you that, so just keep that in mind. Instead, put there widgets that usually start with a
Sliver*prefix, such as
SliverAppBar. Beware that this only applies to widgets producing
RenderSliver. Similarly, you cannot use slivers in place of a regular widget because they do not produce
It’s worth mentioning that if you want to use slivers and
CustomScrollView while relying on Talkback/VoiceOver to support some accessibility features in Flutter’s scroll areas, you definitely can!
From the Flutter API, this widget can send notifications to the platform API, so it might surprise you with an announcement of "showing items 1 to 10 of 23". You must provide the first visible child index and the total number of children. Some implementations of slivers like
ListView can do it automatically. If you pay special attention to accessibility, take a look at the docs.
The simplest sliver to describe is
[SliverToBoxAdapter]. As the name suggests, it converts a box widget to a sliver, so you can easily display
CustomScrollView. But be aware of overusing it for multiple widgets that would otherwise be used inside
SliverList. The reasoning for this is very similar to the aforementioned
Column. Simply put – it works, but especially if you have dynamic number of widgets that better suit a list or grid, use slivers with built-in support.
SliverToBoxAdapter should be used as a last resort.
This sliver implements the Material Design app bar.
[SliverAppBar] is used as the first sliver in
CustomScrollView with various parameters for modification. You can set up a
actions widgets, and
flexibleSpace that expands for you and shrinks on scroll.
It also utilizes the
[SliverPersistentHeader] sliver, which provides a simple API for building content that can shrink on scroll. Keep in mind that its delegate has to have
minExtent defined and
Next, let’s take a look at
[SliverGrid], the two equivalents of
GridView widgets. Both slivers accept the
delegate parameter that accepts a
SliverChildBuilderDelegate. The former delegate accepts explicit
List<Widget> list of box widgets. The latter accepts a builder that builds box widgets lazily based on
BuildContext and index. To
SliverChildBuilderDelegateyou can also pass the total number of widgets using a
childCount property, or if null, the number is determined by index of the last builder call that returns
SliverGrid also requires the
gridDelegate property, which can be
SliverGridDelegateWithMaxCrossAxisExtent. As the name implies, the first explicitly specifies the number of items in cross axis with the
crossAxisCount property. Further, the second specifies the
maxCrossAxisExtent property that sets the widget’s max extent in cross axis.
Slivers for list and grid come in many different variations. For example,
[SliverFixedExtentList], where each widget has the same extent,
[SliverPrototypeExtentList], where each widget has the same constraints as the prototype, or
[SliverReorderableList] that allows you to reorder the list items and provides callback for reordering. Both also have animated versions, which support animations when inserting or removing items; you’ll find them under
Another sliver you shouldn’t miss is
[SliverFillRemaining]. Based on the value of its property
hasScrollBody, the sliver will fill the maximum available extent when true, meaning the whole viewport. When false, it will only fill the space that remains to be filled. This will help you position a widget to the bottom of the viewport, and when another sliver before that extends the viewport, it will be the last thing you can scroll to. And when true, you can create custom slides-like scrolling area. It can also alter its behavior to fill up extra space if used around another sliver that alters the layout, like
In the process of writing this article and testing the parameters, I came to notice this sliver’s intriguing behavior when it’s not the last item in the scrollable and
hasScrollBody is set to true. Try it yourself and set the alignment to
Alignment.bottomCenter. The resulting effect is very interesting because the content is visible and on the bottom of viewport every time you see this viewport-extent widget on your screen.
Another cool sliver is
[SliverFillViewport] that accepts multiple widgets in a
children argument and sizes each of them to fill the whole viewport. It can be used with additional arguments like
viewportFraction that scales axis extent based on this fraction. If you set up
panEnds to true, which is a default value, the last widget will be offset so the widget is centered in the viewport. I can imagine using this sliver for a slides-like application, especially when combining it with no scroll behavior and a handful of icons for moving on to the next item.
There are hundreds of slivers you can implement while also extending your own. But make no mistake — equivalents for many regular widgets like
[SliverLayoutBuilder] already exist, so it’s always best to check beforehand.
Applying slivers doesn’t have to be a daunting task since their basic use can be very easy to pick up. Therefore, don’t be scared to try them out — you can achieve very cool effects with them even if you’re just starting out your journey with Flutter.
And if you still need some convincing, check out this small demo, showing off all slivers we have been through in action.
An example of slivers discussed in this article. (NetGlade)
Layouts in Flutter | docs.flutter.dev
Understanding constraints | docs.flutter.dev
Using slivers to achieve fancy scrolling | docs.flutter.dev
Virtual scrolling | LogRocket