skip to content

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

level: middleimportance: should knowfreq 42%

answer

  1. no measuring each row
  2. index from offset arithmetic
  3. prototype measured once
  4. only one of the three
  5. varied but known heights

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.

solid answer

~50 s

Without an extent, a `ListView.builder` learns each row's height only by laying it out, so a big jump — a `jumpTo` near the end, or dragging the scrollbar — makes it lay out every row in between, and the total extent is estimated from the average of the rows seen so far. `itemExtent: 72` switches the list to a fixed-extent sliver: the first visible index is just offset divided by 72, and with `itemCount` the total length is exact. Children are forced to that extent, so taller content is squeezed or overflows. `prototypeItem` does the same using the measured size of one sample widget, useful when the height depends on fonts or text scale. `itemExtentBuilder(index, dimensions)` returns a known extent per index, for rows that differ but are predictable, such as section headers. Only one of the three may be set.

code

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

class PartRow extends StatelessWidget {
  const PartRow({super.key, required this.name, required this.number});

  final String name;
  final String number;

  @override
  Widget build(BuildContext context) {
    return ListTile(title: Text(name, maxLines: 1), subtitle: Text(number));
  }
}

class PartsList extends StatelessWidget {
  const PartsList({super.key, required this.names, required this.numbers});

  final List<String> names;
  final List<String> numbers;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: names.length,
      prototypeItem: const PartRow(name: 'Brake pad set, front axle', number: 'BP-000000'),
      itemBuilder: (BuildContext context, int index) {
        return PartRow(name: names[index], number: numbers[index]);
      },
    );
  }
}

go deeper

for a junior

Know that setting itemExtent or prototypeItem on a list of same-height rows makes scrolling cheaper.

for a middle

Explain the cost of measuring rows on big jumps, the tight constraint itemExtent imposes, and why only one of the three may be set.

for a senior

Choose between the three from the row design and text scaling, and diagnose clipped rows or jumpy scrollbars from the wrong choice.

for a principal

Encourage row designs with predictable heights in large data lists, trading some visual freedom for smooth scrolling and exact scrollbars.

## The cost they remove A lazy list must answer two questions on every scroll: *which index is at this offset?* and *how long is the whole list?* When items size themselves, the only way to know an item's extent is to lay it out. - **Jumping far** — `jumpTo` near the end, dragging a scrollbar thumb, restoring an offset — requires laying out every item between the last known one and the target. - **Total length** is extrapolated from the average extent of the items laid out so far, so the scrollbar thumb can change size as the user scrolls. In a 5,000-part catalogue, dragging the scrollbar to the bottom can therefore build and lay out thousands of rows in one frame. ## The three options | Parameter | Internal sliver | Extent comes from | |---|---|---| | `itemExtent` | `SliverFixedExtentList` | one fixed `double` | | `prototypeItem` | `SliverPrototypeExtentList` | the measured size of one sample widget | | `itemExtentBuilder` | `SliverVariedExtentList` | a callback per index | They are **mutually exclusive**: the constructor asserts that you pass at most one. ### itemExtent With a fixed extent, finding the first visible index is arithmetic, offset divided by extent, and with `itemCount` the maximum scroll extent is exact. Each child is laid out with a **tight** main-axis constraint of that extent: a row whose content needs more height is squeezed or overflows, and one that needs less is stretched. ```dart ListView.builder( itemCount: parts.length, itemExtent: 72, itemBuilder: (BuildContext context, int index) => PartRow(part: parts[index]), ) ``` ### prototypeItem A hard-coded 72 breaks when the user increases text size or the theme changes font metrics. `prototypeItem` takes a representative widget, lays it out once, and uses its main-axis size for every row. Choose a prototype with the *longest* realistic content — a long part name, a two-line subtitle — so real rows are not cut off. ### itemExtentBuilder `itemExtentBuilder` has the signature `double? Function(int index, SliverLayoutDimensions dimensions)`. It suits lists whose rows differ in predictable ways, such as a 40-pixel category header followed by 72-pixel part rows. It is called several times per layout, so keep it cheap, and return `null` for indexes beyond the end. ## When not to use them - Rows whose height depends on unpredictable content, such as free-text notes that wrap differently. Forcing an extent clips them; let them size themselves. - Lists short enough that jump cost never matters. ## Checklist 1. Are all rows the same height? Use `itemExtent`, or `prototypeItem` if that height depends on text metrics. 2. Do heights vary by a rule you can compute from the index? Use `itemExtentBuilder`. 3. Otherwise, leave all three unset and accept estimated extents. 4. Always pass `itemCount` when the length is known, so the fixed-extent math can give an exact total. ## Summary These parameters trade layout flexibility for arithmetic: the list stops measuring rows to find its place. That matters most for long lists with scrollbars, jumps and restored positions.

  • Why is prototypeItem often safer than a hard-coded itemExtent?
    `prototypeItem` measures a real widget, so its height follows the theme's fonts and the user's text scale. A hard-coded `itemExtent` stays fixed while text grows, so rows get clipped at larger text sizes.
  • What happens to a row whose content is taller than the itemExtent?
    Each child is laid out with a tight main-axis constraint equal to `itemExtent`, so it cannot grow. Its content is squeezed and may report an overflow, and the next row starts exactly one extent later.

saying these in an interview costs you the question

  • itemExtent is only a hint that rows may exceed.
  • You can combine itemExtent with prototypeItem for extra speed.
  • prototypeItem is rendered as the first row of the list.
  • Without an extent, the list always knows its exact total length.
  • itemExtentBuilder is called once per item and cached forever.