skip to content

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%

answer

  1. outside the cache area, disposed
  2. addAutomaticKeepAlives only listens
  3. wantKeepAlive opts in
  4. must call super.build
  5. updateKeepAlive when it changes

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.

solid answer

~40 s

A lazy list keeps elements only for items in the viewport plus the cache area; the rest are unmounted, so a row's `State` — a half-typed quantity, an expanded panel — is disposed and recreated fresh when it returns. `addAutomaticKeepAlives: true`, the default, does **not** keep anything alive by itself: it wraps each child in an `AutomaticKeepAlive` that listens for keep-alive requests. The row opts in: its `State` mixes in `AutomaticKeepAliveClientMixin`, overrides `bool get wantKeepAlive`, calls `super.build(context)` and ignores the result, and calls `updateKeepAlive()` when `wantKeepAlive` changes. Kept-alive items stay in memory with their render objects, so this suits a few expensive items, not every row in a 5,000-part catalogue. Usually the better fix is to lift the state — quantities per part number — into a model above the list.

code

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

class PartQuantityRow extends StatefulWidget {
  const PartQuantityRow({super.key, required this.partName});

  final String partName;

  @override
  State<PartQuantityRow> createState() => _PartQuantityRowState();
}

class _PartQuantityRowState extends State<PartQuantityRow>
    with AutomaticKeepAliveClientMixin<PartQuantityRow> {
  final TextEditingController _quantity = TextEditingController();
  bool _edited = false;

  @override
  bool get wantKeepAlive => _edited;

  void _onChanged(String value) {
    if (!_edited && value.isNotEmpty) {
      _edited = true;
      updateKeepAlive();
    }
  }

  @override
  void dispose() {
    _quantity.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListTile(
      title: Text(widget.partName),
      trailing: SizedBox(
        width: 64,
        child: TextField(controller: _quantity, keyboardType: TextInputType.number, onChanged: _onChanged),
      ),
    );
  }
}

go deeper

for a junior

Know that list items lose their state when scrolled far away because they are rebuilt from scratch.

for a middle

Explain the three pieces: addAutomaticKeepAlives, AutomaticKeepAlive and the mixin, including wantKeepAlive, super.build and updateKeepAlive.

for a senior

Weigh keep-alive against lifting state, keep only expensive items alive, and debug the missing super.build call.

for a principal

Set the rule that editable list data lives in models, reserving keep-alive for media and embedded views, to keep memory predictable.

## Why state disappears `ListView.builder` is lazy in both directions. It creates items as they approach the viewport and **removes** them once they fall outside the visible area plus the cache area (250 logical pixels by default on each side). Removing an item unmounts its element, and a `StatefulWidget`'s `State` goes through `dispose`. When the user scrolls back, `itemBuilder` runs again and a brand-new `State` starts from `initState`. In a car-repair shop's parts catalogue, that means a mechanic types "4" into the quantity field of a brake-pad row, scrolls down to find rotors, scrolls back — and the field is empty. ## The keep-alive machinery Three pieces cooperate: | Piece | Where | Job | |---|---|---| | `addAutomaticKeepAlives` | list constructor, default `true` | wraps every child in an `AutomaticKeepAlive` | | `AutomaticKeepAlive` | around each child | listens for keep-alive notifications from below and marks the child to be kept | | `AutomaticKeepAliveClientMixin` | the item's `State` | sends those notifications when `wantKeepAlive` is true | The default flag only installs the **listener**. Nothing is kept until a descendant asks. ## Using the mixin correctly 1. Mix it into the `State`: `class _PartRowState extends State<PartRow> with AutomaticKeepAliveClientMixin<PartRow>`. 2. Override `bool get wantKeepAlive` — a constant `true`, or a condition such as "the user has edited this row". 3. Call `super.build(context)` at the top of `build` and **ignore** its return value. The mixin's `build` is marked `@mustCallSuper`; it is where the keep-alive request is made. 4. When the condition behind `wantKeepAlive` changes, call `updateKeepAlive()` so the request is added or released without waiting for a rebuild. Forgetting step 3 is the classic bug: the mixin compiles, but nothing is kept. ## Costs - A kept-alive item keeps its element, `State` and render objects in memory even far off screen. - Keep-alive requests pile up: keeping every row of a 5,000-item list alive defeats laziness. - Controllers inside kept rows stay alive too; they are disposed only when the row finally is. ## Better alternatives, most of the time - **Lift the state.** Store quantities in a map keyed by part number in a model or state object above the list; each row reads and writes it. Rows can then be rebuilt freely. - **Keep alive selectively.** Make `wantKeepAlive` true only for rows the user has actually edited. - **Keep scroll positions elsewhere.** Nested scrollables that lose their offset have their own mechanism, owned by the scroll-controller tools. Keep-alive is the right tool when state is **expensive to recreate** and hard to lift: a playing video, a web view, a complex form mid-edit, or a tab page that should not reload. ## The same mechanism elsewhere `GridView` and slivers use the same flag and mixin. Tab pages in a `TabBarView` are a common place to meet the mixin, because each page is an item in a lazy scrollable.

  • A State mixes in AutomaticKeepAliveClientMixin and returns true from wantKeepAlive, but the item still resets. What is the likely cause?
    `build` does not call `super.build(context)`. The mixin makes its keep-alive request from its own `build`, which is annotated `@mustCallSuper`. Call it at the top and ignore the returned widget. Another cause is a list built with `addAutomaticKeepAlives: false`, which removes the listener.
  • Why is keeping every row alive in a 5,000-item list a bad idea?
    Each kept row retains its element, `State` and render objects off screen, so memory grows with every row the user scrolls past and the list stops being lazy. Keep only rows with expensive or unsaved state alive, or lift the state into a model.

saying these in an interview costs you the question

  • addAutomaticKeepAlives: true keeps every item alive by default.
  • Mixing in AutomaticKeepAliveClientMixin is enough without calling super.build.
  • Scrolled-away items are only hidden, never disposed.
  • Keep-alive has no memory cost because widgets are cheap.
  • A GlobalKey on each row is the standard way to keep its state.