Scrolling. It’s all people do these days — scrolling through their phones. As it happens, it’s an important ability for mobile apps. Let’s delve into how to scroll in Flutter using the ListView widget.
As you can see from the class hierarchy below, there’s a lot that makes up the Widget, ListView. We’ll start with the basics and work up to the more involved features.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots over gists to show concepts rather than just show code in my articles. I find them easier to work with frankly. However, you can click or tap on these screenshots to see the code they represent in a gist or in Github. Ironically, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program on our computers — not on our phones. For now.
No Moving Pictures, No Social Media
There will be gif files in this article demonstrating aspects of the topic at hand. However, it’s said viewing such gif files is not possible when reading this article on platforms like Instagram, Facebook, etc. They may come out as static pictures or simply blank placeholder boxes. Please, be aware of this and maybe read this article on medium.com
We’ll start with a basic list. It’s a finite list. And like all that use the ListView Widget, it’s a list composed of other widgets. They’re referenced as ‘child’ widgets. This list is found in an example from the Flutter Cookbook labelled appropriately enough, Basic List. And man! Is it ever! It’s only three ListTile Widgets.
It’s Only The Children
Only the named parameter, children, is called upon in this very simple example and supplies a List of widgets. All the other parameters pertaining to essentially ‘how’ the user can scroll those children, and what happens when a child is scrolled ‘out of view’ are left alone — some, therefore, are set to their default values. Below is a screenshot of the class, ListView, and it’s basic constructor.
There are a few more constructors made available for this class. They’re identified as named constructors, and offer further functions and features to the developer. We’ll get to them soon enough.
From Child To Parent
Yes, deep in the class hierarchy is where many of those parameters come into play. It’s in the build() function of the ScrollView class, where the scrollable list of widgets is made. However, we won’t get this deep into the class hierarchy with this article. ListView is a very popular widget for developers, and so we’ll keep to the class, ListView, and not its parent classes. That’s for subsequent articles to come one day.
Not much to that first example, but there is another Cookbook example close by that utilizes a second parameter as well. It’s called, Create a horizontal List Example, and in this example, instead of scrolling vertically, you can scroll horizontally. That additional named parameter is called, scrollDirection. Instead of taking on its default value, Axis.vertical, the ListView object in this example is assigned the value, Axis.horizontal.
Horizontal With Constraints
Unlike the first example, these ‘child’ widgets specify their widths allowing the list to successfully scroll horizontally. Trying this horizontal scroll with the first example would trip an assertion error with a width value of infinity.
List In Reverse
When set to Axis.vertical, items are listed from top to bottom. However, if another named parameter, reverse, is set to true, those items are then listed instead from bottom to top. Let’s see that now back in our first example.
If the named parameter, scrollDirection, was set to Axis.horizontal, then the reading direction would be from left-to-right — the items listed from left-to-right. Again, however, if the named parameter, reverse, is set to true and not it’s default value, false, the reading direction would instead from right-to-left.
Only Those Seen
The constructor used so far to instantiate a ListView object is appropriate for short lists with simple widgets as children. That’s because all the widgets that make up the List are constructed then and there. It is usually more efficient, however, to create children ‘on the fly’ only constructing those that are actually going to be visible to the user while destroying those already shown. This is done using the ListView constructor, ListView.builder.
We will use this ‘builder’ constructor from now on. We’ll utilize yet another Cookbook example, Working with Long Lists, and see if we can better demonstrate the remaining parameters. The only real difference from the basic constructor and this constructor’s parameters is that the named parameter, children, of type, List<Widget>, is now replaced by one named, itemBuilder, a class of type, IndexedWidgetBuilder.
Builder Of Long Lists
This too is a simple example. However, it does create ten thousand String objects starting with the word, ‘item’, and then the current count appended on the end. It’s a long list. Not that long for the original ListView constructor, admittedly, but that’s only because it’s composed of very very simple widgets. It would use up a bit of memory needlessly, however. And if the list of Widgets were more complex, and or that list was no less than infinitely long, the ‘builder’ constructor is your best choice indeed. It saves on resources. Constructing only what will be visible; destroying what is no longer visible but reuses bits of what it can. They call this ‘lazily construction.’
In both the basic constructor and in the ‘builder’ constructor, the child widgets are passed on to other classes delegated to return a ‘Sliver’ of the scrollable area which contains one of the child Widgets. Below is the heart of these classes — their respective build() functions. On the left, the class the ‘builder’ constructor uses, SliverChildBuilderDelegate, and on the right, the basic ListView version uses the class, SliverChildListDelegate.
Scrolling Slivers Of Widgets
Each build() function returns a Widget. The ‘builder’ version is a little longer as it has to contend with a ‘builder’ routine while the ‘basic’ version is provided a finite list of already constructed ‘child’ widgets. Like the ‘builder’ version, the ‘basic’ version is also provided an index parameter. However, the ‘basic’ version merely goes to the list of provided child widgets and picks out one by that index. While the ‘builder’ version passes the index on to the builder routine.
Note, there’s a third additional ListView constructor that also uses the class, SliverChildBuilderDelegate. It’s called, ListView.separated, and it also produces a scrollable list of widgets but each is separated by specified ‘separators.’ Thus, all three constructors create a band of widgets (Slivers) that make up the scrollable area of the ListView object.
Three Types Of Slivers
By default, each such widget is enclosed or ‘wrapped’ in three other classes. These three other classes directly affect how the enclosed widgets actually scroll and how they’re presented to the user while scrolling.
As you see below in both the ‘builder’ and the ‘basic’ ListView constructor, by default, all three classes have associated boolean properties that are set to true. Let’s quickly introduce you to each one of these properties in the order they’re implemented.
Repaint As You Scroll
The first one is the property, addRepaintBoundaries. By default, for better performance, each sliver item is not ‘repainted’ as it scrolls along. Nothing in the item itself has changed when scrolling, and so there’s no need to repaint it on the screen as it moves. By taking in the original child widget, a new child widget is created of the class type, RepaintBoundary. It’s suggested, however, there may be times if the list is composed of simple child widgets — for example, ones of one solid colour or of short pieces of text, it may be even more efficient to set, addRepaintBoundaries, to false and allow the repainting of the child widgets as they scroll. However, it most cases there’s no need.
Bring Voice To Your Scrolling
The next property, addSemanticIndexes, provides a means for linguistic programs like TalkBack or Voiceover to make announcements to the visually impaired user while the items scroll. For example, allowing such programs to state to the user how many items are currently visible. By default, this ability is set to true.
Keep Alive When Out Of Sight
The last property, addAutomaticKeepAlives, is also set to true by default. Instead of being a candidate for garbage collection when an item scrolls off-screen and is now out of sight of the user, they are simply retained in memory. It’s assumed of all likelihood the scrolling would be back and forth bringing such items back in sight — making it needlessly expensive to reconstruct them again. Otherwise, if the property was set to false, they would possibly be destroyed (removed from memory) when scrolled off-screen.
The Builder Builds
And so, in our little ‘long list’ example, when the property, builder, is encountered (on left below) in the SliverChildBuilderDelegate’s build() function, the anonymous function assigned to the named parameter, itemBuilder, (on the right below) is called. It then returns a Text object assigned inside a ListTile object. This, depending on the value of those three boolean properties is further wrapped one after the other in a new Widget finally deemed a sliver of the scrollable list.
Note, the anonymous function assigned to the named parameter, itemBuilder, could contain any number of classes, functions, and widgets by any degree of complexity as some further samples below would convey. It could even contain another ListView.builder as depicted in the last sample. Again, to save on performance, it won’t create all of the Slivers that could be made — only those made visible by the user’s scrolling.
Control The Scroll
Let’s continue now through the remaining parameters available to you when using the ListView Class. The next one, after the reverse parameter, is the parameter, controller. This involves the class, ScrollController. Which does exactly that: Controls the scroll.
With it, you can dictate that the scrollable list starts at a particular position and not at the beginning as it does by default. For example, it could start where you left off when you were scrolling in the app last.
With it, you can incorporate ‘listeners’ into your scrollable list. You’ll then ‘know’ when you scrolling, when you’ve stopped scrolling and where you’ve stopped scrolling.
Now I blatantly stole some examples from a gentleman on medium. I didn’t have the where-with-all to think up my own. He wrote a wonderful article on the subject of the ListView widget and its ScrollController, and I felt his were great examples demonstrating but a few of the features available to you when using the ScrollController in your own ListViews. I will, of course, notify him of my discretion.
There’s A Gist For That
I’ve composed a composite example, ListViewScrollController.dart, including all three features illustrated above. Again, merely a few capabilities made available to you using a ScrollController with your ListView.
Where Are You?
From left-to-right, we’ll review each example. In the first one, you see that scrolling up to the top of the listing will change the app’s title to ‘reached the top.’ This requires a ScrollController object being provided to the ListView.builder and assigned a ‘listener.’ In the example’s initState() function, the ScrollController is instantiated and assigned such a listener.
This listener has the means to determine where at any point in time we’re currently positioned in the scrollable list using the ScrollController property, offset. The offset is a double value indicating the ‘accumulative distance’ in ‘logical pixels’ from the beginning of the scrollable listing. The beginning of the listing has an offset of 0.0. Once instantiated, the ScrollController object is taken in by the ListView’s named parameter, controller.
Logical pixels are roughly the same visual size across devices while Physical pixels are the size of the actual hardware pixels on the device. Logical pixels are to be device-independent and resolution-independent pixels.
The Offset From the Start
In most cases, ListView’s starts with an offset of 0.0. In other words, start from the beginning of the scrollable listing. Of course, with the ScrollController object, you can start wherever you like. Its constructor will accommodate that with the named parameter, initialScrollOffset. Note, it defaults with 0.0.
Reach The Top
Now back to our example. Looking at the Listener routine defined and introduced to the ListView by the addListener() function, we see the ScrollController knows the ‘start’ of the scrollable list using the value from, con.position.minScrollExtent. You guessed it — its value would be 0.0. And so, if the ‘current offset’ (con.offset) is 0.0, you’ve reached the start of the list. The title is thus changed appropriately.
Automate The Scrolling
The next example involves scrolling automatically with the use of a ScrollController object. In our example, a little automation is used.
As you see in the screenshot above, we define a function called, rowButtons(), that returns the widget containing the ‘up’ and ‘down’ buttons presented in the second example. Also in the code is the ScrollController function, animatTo(), that’s responsible for the animation. The first parameter in the function indicates the offset to ‘move to’ from the current offset position in the scrollable list. In this example, we merely move by ‘the width’ of a listed child widget, itemSize. You can see how that’s done by ‘adding’ or ‘subtracting’ the specified size.
The curve parameter describes the ‘how’ the animation moves: if it speeds up as it moves and or slows down as it moves. In this example, the animation is performed at a constant speed with the option, Curves.linear. It’s the parameter, duration, that dictates the actual speed as it sets the time it takes to complete the animation.
Note, the function, jumpTo(), commented out in the screenshot moves the scrollable listing with a flash and without the animation. Not nearly as neat.
As you see in the screenshot above, the function, rowButtons(), is placed above the scrollable list produced by the ListView.builder constructor. A new parameter, itemExtent, is introduced in the constructor, and it specifies ‘the width’ of each ‘child’ widget listed in the scrollable list. It’s described as ‘ the size of the child along the main axis’. In other words, the width either vertically or horizontally depending on if the ListView parameter, scrollDirection, is set, Axis.vertical, or Axis.horizontal.
IIt’s said in the documentation it would mean better performance to always specify the ‘itemExtent’ property when making your ListViews as this then relieves Flutter in having to compute ‘the width’ itself every time.
Dispose Of The Controller
Don’t forget to dispose of the ScrollController when you’re done. They take up resources and should be cleaned up when no longer in use.
The next parameter is called, primary. It’s used to indicate whether this is the primary scroll view associated with the parent PrimaryScrollController. This means there must be no ScrollController parameter if primary is set to true.
When this is true, the scroll view is scrollable even if it does not have sufficient content to actually scroll. Otherwise, by default, the user can only scroll the view if it has sufficient content. On iOS, if set true, this also identifies the scroll view that will scroll to top in response to a ‘tap’ in the status bar.
The Physics of Scrolling
The next parameter, physics, determines how the scrollable list will behave when the user reaches the start or the end of the scroll and how it behaves when the user ‘let’s go’ and it continues scrolling for a time. It takes in a class object of type, ScrollPhysics.
Currently, there are four subsclasses of the class, ScrollPhysics, provided to your ListView Widget. Three of them are demonstrated above. The first one emulates the behaviour seen in Apple iPhones, BouncingScrollPhysics. The second, ClampingScrollPhysics, gives the ‘overscroll’ indication glow — typically seen on Android phones. The last one demonstrated is called, NeverScrollableScrollPhysics, disabling scrolling capability completely. The one not on display is, AlwaysScrollableScrollPhysics. It provides the appropriate behaviour depending upon the platform: Android or iOS.
Shrinkwrap The Scroll
Next is the boolean named parameter, shrinkWrap. In the documentation, by default, what they call ‘the scroll view’ (i.e. I’ve been calling the scrollable list) will take up all the space available to match its parent element in size. Even if the list of items requires less space. You can see that in the original ‘Basic List’ example we had started with. To reveal its size, I’ve wrapped the ‘scroll view’ with a thick red border defined by its parent, a Container Widget.
As stated in the documentation, “ If the scroll view does not shrink wrap, then the scroll view will expand to the maximum allowed size in the [scrollDirection].” — In this example, it’s in the vertical direction. If you set the property, shrinkWrap, to true as we’ve done above, the scrollable list (or scroll view) will wrap its content and be as big as its children widgets will allow. Again, you can readily see the new size with a thick red border.
The documentation further notes there’s a penalty in performance with this property set to true: “ Shrink wrapping the content of the scroll view is significantly more expensive than expanding to the maximum allowed size because the content can expand and contract during scrolling, which means the size of the scroll view needs to be recomputed whenever the scroll position changes.”
Padding The Scroll’s Children
Flutter is regularly padding its widgets with empty spaces using the EdgeInsetsGeometry Class. For example, if not specified, a little bit of padding is always done ‘around’ the child widgets listed in the ListView Widget.
In the screenshot above, we have Card Widgets making up the scrollable list to better illustrate the padding. From left to right, the double value supplied to the EdgeInsetsGeometry object is from 0.0, to 16.0, and then to 100.0.
There’s A Cache
The next parameter we’ll look at is cacheExtent. There’s an area of just before and just after the ‘visible’ portion of the scrollable area or ‘viewport’ as they describe it in the documentation, that contains content laid out although not even yet visible on the screen. The cacheExtent is a property of type, double. It’s the ‘number of logical pixels’ that are painted in those two cached areas although not yet visible on the screen. The bigger the numeric amount, the more is ‘painted’ in cache memory that is about to become visible when the user scrolls.
It’s A Drag
The last named-parameter, dragStartBehavior, can have one of two possible enumerated types: DragStartBehavior.start and DragStartBehavior.down. The default is set to set to DragStartBehavior.start. It means that ‘scrolling drag behaviour’ will begin upon the detection of a drag gesture. If set to
DragStartBehavior.down, the ‘scrolling drag behaviour’ will begin when a mouse click down event is first detected. It’s said, setting this to DragStartBehavior.start will make the drag animation smoother. While the DragStartBehavior.down, it’s said, will make ‘scrolling drag behaviour’ feel slightly more reactive.
Let’s leave it at that for now. Take a break. You deserve it.
* Source code as of March 27, 2019
+Source code as of March 09, 2019
^Source code as of March 01, 2019
#Source code as of Feb. 21, 2019