Common mistakes with ListViews in Flutter

Roman Ismagilov
5 min readMay 14, 2024

--

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:

  1. prototypeItem — a widget whose height will be used for every list item.
  2. 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:

Test is done on a Pixel 8 phone 20 times

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.

Test is done 20 times on a Pixel 8 phone

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.

--

--

Roman Ismagilov

Covering some non-obvious nuances of Flutter development in my articles