Common mistakes with ListViews in Flutter
A couple of thing to avoid to keep scrolling smooth
1. Shrink wrapping ListView.builder or using NeverScrollableScrollPhysics.
The main advantage of using ListView.builder
is its optimization mechanism, which initializes only the items needed to be shown on the screen. This makes it perform smoothly with thousands of items, just as it would with a dozen.
What if the list we want to display needs to be embedded in another scroll view? A common mistake is disabling scrolling by adding NeverScrollableScrollPhysics
or setting shrinkWrap
to true
. This approach initializes all the items at once, which can be problematic. We can conduct a simple experiment to confirm this issue.
SingleChildScrollView(
child: Column(
children: [
const Card(child: Text("Header card")),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 1000,
shrinkWrap: true,
itemBuilder: (context, index) {
print("building item #${index}");
return Card(child: Text(index.toString()));
},
),
const Card(child: Text("Footer card")),
],
),
)
As we can see from the output, all the items are initialized simultaneously, leading to a significant performance loss. This happens because the entire list is rendered at once, rather than just the visible portion.
So, what should we do instead? The recommended approach is to use Slivers. Slivers allow for more efficient rendering by only building the items that are currently visible on the screen. This helps maintain smooth performance, even with large lists.
CustomScrollView(
slivers: [
const SliverToBoxAdapter(child: Card(child: Text("Header card"))),
SliverList.builder(
itemBuilder: (context, index) {
print("building item #${index}");
return Card(child: Text(index.toString()));
},
),
const SliverToBoxAdapter(child: Card(child: Text("Footer card"))),
],
)
As we can see, it initialises only the items that are to be shown on the screen, plus a bit more due to viewport caching. It is needed to have some items already pre-initialised, so that there were no jitter when user starts scrolling. The amount of pixels to be cached is configurable by cacheExtent
property of CustomScrollView
or ListView
and defaults to 250.
2. Letting every item in the list determine height on its own.
One of the most expensive operations in Flutter UI rendering is calculating widget sizes. When drawing lists, there are ways to avoid excessive calculations, especially for lists where all items have the same height.
You can use two properties for this:
prototypeItem
— a widget whose height will be used for every list item.itemExtent
— a numeric value that specifies the height of each item.
Many tutorials recommend using itemExtent, but it has some often overlooked disadvantages. If you provide a constant value and your list items contain text, the layout might break when the user increases the font size in the OS settings.
To avoid that you can calculate real size of items by multiplying font size to OS text scale and adding paddings. That might become troublesome and hard to maintain. There’s a better way to do it:
ListView.builder(
itemCount: 1000,
prototypeItem: const Card(child: Text("")),
itemBuilder: (context, index) {
return Card(child: Text(index.toString()));
},
)
In case of the sliver lists, we also have a possibility to provide a prototype:
SliverPrototypeExtentList.builder(
itemBuilder: (context, index) {
return Card(child: Text(index.toString()));
},
prototypeItem: const Card(child: Text("")),
),
Note that ListView.separator
doesn’t have these properties, consider adding a divider to list items and using ListView.builder
instead.
Not to be unfounded, let’s do a benchmark:
As we can see, having Slivers
with an itemPrototype
gives the best results, however, the boost from having itemPrototype
is not that noticeable in this case.
3. Wrapping a ListView into a Padding widget
If you want to add padding to a ListView
widget (this also applies to SingleChildScrollView
), make sure you don’t wrap a ListView
in a Padding
widget. Doing so will act like a margin to the scrollable content rather than padding. Instead, add the padding parameter directly to your ListView
(or SingleChildScrollView
) object.
ListView.builder(
itemCount: 1000,
padding: const EdgeInsets.all(40),
prototypeItem: const Card(child: Text("")),
itemBuilder: (context, index) {
return Card(child: Text(index.toString()));
},
)
The difference could be seen here:
Unfortunately, neither SliverLists
nor CustomScrollView
have such a parameter. In that case in order to add some padding to contents of a CustomScrollView
you can use SliverPadding
widget:
SliverPadding(
padding: EdgeInsets.all(20),
sliver: SliverPrototypeExtentList.builder(
itemBuilder: (context, index) {
return Card(child: Text(index.toString()));
},
prototypeItem: const Card(child: Text("")),
),
),
4. Using wrong scroll physics for different platforms
Android and iOS have different scroll physics and overscroll behaviour. Here’s a quote from documentation:
Android and iOS both have complex scrolling physics simulations that are difficult to describe verbally. Generally, iOS’s scrollable has more weight and dynamic friction but Android has more static friction. Therefore iOS gains high speed more gradually but stops less abruptly and is more slippery at slow speeds.
When you explicitly set physics
to BouncingScrollPhysics
or to ClampingScrollPhysics
then it would look native only on one platform. Just use AlwaysScrollableScrollPhysics
, unless it’s some rare case when you really need to have scroll physics set this way.
If you want to read more about how to make Flutter apps feel move native, there are more articles on that:
Making Flutter apps look more native. Part 1: tap effects
Making Flutter apps look more native. Part 2: refresh indicators
5. Adding keys to every list item and expecting that it will improve the scrolling performance
It’s a common myth that adding keys to the list items helps to recycle widgets and improves performance. Let’s first do some benchmarking and then see why does it work this way.
To do that, let’ make an integration test. We will be measuring performance of a page containing 200 items of medium complexity by scrolling it to the end and tracking the metrics.
As we can see, there’s no performance boost when using keys in the list.
Then when would adding keys to list item make any sense?
- When items in the list can be reordered, edited, removed, etc
- When writing UI tests and interacting with list items
6. Not using restorationId.
Imagine scrolling news section in an app, that you like, then being disturbed by a call, and then, when the call is finished, the app forgets where you were reading and shows the initial scroll position.
Solution is simple: add a restorationId
to every Scrollable in your app
ListView.builder(
restorationId: 'news_feed',
...
)
Note, that it works only in a RestorationScope
and when the app navigation is also configured to restore.
Code could be found here: https://github.com/Pomis/flutter_native_ui_examples/tree/main/lib/3_list_view_mistakes
I hope that this article will help you to make your scrolling a bit smoother.