skip to content

Lazy Lists & Grids

ListView.builder and GridView.builder create items only as they scroll into view, and a fixed itemExtent or prototypeItem skips measuring each one. Interviewers ask why a plain ListView is costly.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Flutter, what is the difference between a ListView built from a children list and ListView.builder, and when should you use each?

level: juniorimportance: must knowfreq 78%

answer

  1. children list is built eagerly
  2. itemBuilder called on demand
  3. only visible plus cache area
  4. itemCount enables accurate extent
  5. .separated for dividers

basics

~20 s

ListView(children: ...) needs every child widget constructed up front, which suits short, fixed lists. ListView.builder calls itemBuilder only for indexes near the viewport, so a 5,000-row list builds just the rows on screen plus a small cache area.

solid answer

~40 s

`ListView(children: [...])` takes a ready-made `List<Widget>`, so the parent's `build` constructs a widget for **every** item each time it runs; the docs recommend it for a small number of children. `ListView.builder` takes an `itemBuilder(context, index)` that Flutter calls **only for items near the viewport** — the visible ones plus a cache area, 250 logical pixels by default. Items that scroll far away are discarded and rebuilt when they return. Pass `itemCount` so the list knows where it ends; without it the list is unbounded until the builder returns `null`. For a 5,000-part catalogue I'd use `ListView.builder`, or `ListView.separated` when I need dividers, which takes a `separatorBuilder` and a required `itemCount`. A settings screen with eight rows is fine as a plain `ListView`.

code

dart · 31 lines
dart
import 'package:flutter/material.dart';

class Part {
  const Part({required this.name, required this.number, required this.stock});

  final String name;
  final String number;
  final int stock;
}

class PartsCatalogue extends StatelessWidget {
  const PartsCatalogue({super.key, required this.parts});

  final List<Part> parts;

  @override
  Widget build(BuildContext context) {
    return ListView.separated(
      itemCount: parts.length,
      separatorBuilder: (BuildContext context, int index) => const Divider(height: 1),
      itemBuilder: (BuildContext context, int index) {
        final Part part = parts[index];
        return ListTile(
          title: Text(part.name),
          subtitle: Text(part.number),
          trailing: Text('${part.stock} in stock'),
        );
      },
    );
  }
}

go deeper

for a junior

Know that ListView.builder builds items on demand and is the default for long lists, while the plain constructor suits a few fixed items.

for a middle

Explain which layer is lazy, what itemCount changes, the cache area, and why row state resets after scrolling away.

for a senior

Spot eager work in item construction, choose between builder, separated and custom delegates, and keep item state out of rows.

for a principal

Make lazy lists and cheap item widgets a team default for any data-driven list, and review exceptions rather than the rule.

## Two ways to give a list its items Every `ListView` is a scroll view with a **child delegate** that answers "what is the widget at index *i*?". The constructors differ in when those widgets are created. | Constructor | Delegate | Widgets created | Good for | |---|---|---|---| | `ListView(children: ...)` | `SliverChildListDelegate` | all of them, when the parent builds the list | a few, fixed items | | `ListView.builder` | `SliverChildBuilderDelegate` | on demand, per index near the viewport | long or unbounded lists | | `ListView.separated` | builder delegate with interleaved separators | on demand | long lists with dividers | | `ListView.custom` | any `SliverChildDelegate` | depends on the delegate | special cases | ## What is actually lazy There are three layers: **widgets** (cheap descriptions), **elements** (the live tree) and **render objects** (layout and paint). - With **`ListView(children: ...)`**, all widget objects exist before the list lays out. Elements and render objects are still created only as items come near the viewport, but building the list itself — and any work done to produce each widget, such as formatting prices — happens for every item on every rebuild of the parent. - With **`ListView.builder`**, even the widget is created on demand. The framework calls `itemBuilder` only for indexes inside the visible area plus the **cache area** before and after it. When an item leaves that region, its element and `State` are discarded; if it comes back, `itemBuilder` runs again. For a parts catalogue of 5,000 entries, the plain constructor would allocate 5,000 row widgets every time the screen rebuilds, even though perhaps a dozen are visible. ## Using the builder well 1. **Pass `itemCount`.** The builder is called only for `0 <= index < itemCount`, and the list can estimate its full scroll extent. Without it the list is infinite until `itemBuilder` returns `null`, and the scrollbar can grow as the user scrolls. 2. **Create widgets inside `itemBuilder`.** The docs warn against returning pre-built widgets; if you already have them all, the plain constructor is the honest choice. 3. **Keep items cheap.** The builder can run many times during a fast fling. 4. **Do not keep state in items** unless you opt in to keep-alive; a row's `State` is disposed when it scrolls away. ## ListView.separated `ListView.separated` interleaves a separator between items: `itemBuilder` builds the items and `separatorBuilder` builds the dividers. Both are lazy, and `itemCount` is **required** — it is the number of items, not items plus separators. ## Defaults worth knowing - `addAutomaticKeepAlives: true` — each child is wrapped so it *can* request to stay alive; - `addRepaintBoundaries: true` — each child gets its own `RepaintBoundary`; - `addSemanticIndexes: true` — screen readers get item positions; - cache area: 250 logical pixels before and after the viewport, set with `scrollCacheExtent`. ## When the plain constructor is right - a short menu, settings page or form that always fits in a screen or two; - items that are compile-time constants; - content where every item is visible most of the time, so laziness saves nothing. ## Summary for an interview The builder form creates items on demand, the plain form creates the whole list of widgets up front. Pick the builder for anything long, pass `itemCount`, and use `.separated` for dividers.

  • What happens if ListView.builder is given no itemCount?
    The list is treated as unbounded: `itemBuilder` keeps being called for higher indexes until it returns `null`. The maximum scroll extent is then only known when the user reaches the end, so the scrollbar can change size while scrolling. Pass `itemCount` whenever the length is known.
  • Does a plain ListView create render objects for all of its children at once?
    No. It needs every child widget constructed up front, but the underlying sliver still inflates elements and render objects only for items near the viewport. The waste is in building the full widget list, and whatever work produces it, on every rebuild of the parent.
  • Why does a row's State reset after scrolling far away and back in a ListView.builder?
    Items outside the visible area and the cache area are removed, and their `State` is disposed. When the row returns, `itemBuilder` creates it afresh. Keep that state in a model above the list, or have the row opt in to keep-alive.

A plain ListView is printing the whole 5,000-page catalogue before opening it; ListView.builder prints each page only as you turn to it.

saying these in an interview costs you the question

  • A plain ListView lays out and paints all of its children immediately.
  • ListView.builder keeps every built item alive after it scrolls away.
  • itemCount is optional and changes nothing about scrolling.
  • ListView.separated's itemCount counts separators as well as items.
  • Pre-building all rows and returning them from itemBuilder is fine.
open as a page

In Flutter's GridView.builder, how do SliverGridDelegateWithFixedCrossAxisCount and SliverGridDelegateWithMaxCrossAxisExtent decide each tile's size?

level: middleimportance: should knowfreq 40%

basics

~20 s

FixedCrossAxisCount divides the width into a set number of columns; MaxCrossAxisExtent picks the fewest columns no wider than a maximum. Height is width divided by childAspectRatio, default 1.0, unless mainAxisExtent is set, and each tile is forced to exactly that size.

open as a page

In a Flutter ListView.builder, what do itemExtent and prototypeItem save, and when would you use itemExtentBuilder instead?

level: middleimportance: should knowfreq 42%

basics

~20 s

Both tell the list each item's main-axis extent up front, so it finds the index at any offset without laying out the items in between. itemExtent gives a number, prototypeItem measures one sample widget, and itemExtentBuilder gives a known extent per index.

open as a page

In a Flutter ListView.builder, why does an item lose its state when scrolled away, and how do AutomaticKeepAliveClientMixin and addAutomaticKeepAlives keep it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Items beyond the visible area and cache area are removed and their State disposed. addAutomaticKeepAlives, true by default, wraps each item so it can ask to stay; the item's State must mix in AutomaticKeepAliveClientMixin, return true from wantKeepAlive and call super.build.

open as a page

In Flutter 3.44 and later, what does a ListView's scrollCacheExtent control, and what did it replace?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

scrollCacheExtent sets how far beyond each edge of the viewport a lazy list builds and lays out items before they are visible, 250 logical pixels by default. Since Flutter 3.44 it replaces the deprecated cacheExtent and cacheExtentStyle pair with one ScrollCacheExtent value.

open as a page