skip to content

In Flutter, what does IndexedStack do with the children it is not showing, and when is it the right tool for switching views?

level: middleimportance: should knowfreq 44%

answer

  1. one child shown by index
  2. all children built and laid out
  3. State survives switching
  4. sized by the largest child
  5. tickers are not muted

basics

~20 s

IndexedStack keeps every child mounted and laid out but paints, hit-tests and exposes semantics only for the child at index, so hidden children keep their state. It sizes to the largest child and suits a few state-keeping views.

solid answer

~50 s

`IndexedStack` is a `Stack` that shows one child, chosen by `index` (default 0; `null` shows none). All children stay in the tree: they are built, keep their `State` and are laid out on every layout pass, which the render object documents as O(N) like an ordinary stack. Only the selected child is painted, hit-tested and included in semantics, and the others are wrapped so they cannot take focus and `Visibility.of` reports them hidden. Because all non-positioned children are laid out, the widget is as big as the largest one, not the visible one; `sizing` (default `StackFit.loose`) sets their constraints. It does not mute tickers, so a repeating animation in a hidden child keeps running unless you add `TickerMode`. It fits a small, fixed set of views, such as profile tabs, where keeping scroll position and form input matters more than memory.

code

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

class ProfileTabs extends StatefulWidget {
  const ProfileTabs({super.key});

  @override
  State<ProfileTabs> createState() => _ProfileTabsState();
}

class _ProfileTabsState extends State<ProfileTabs> {
  int _tab = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SegmentedButton<int>(
          segments: const [
            ButtonSegment(value: 0, label: Text('Posts')),
            ButtonSegment(value: 1, label: Text('Photos')),
            ButtonSegment(value: 2, label: Text('About')),
          ],
          selected: {_tab},
          onSelectionChanged: (Set<int> s) => setState(() => _tab = s.first),
        ),
        Expanded(
          child: IndexedStack(
            index: _tab,
            children: const [PostsTab(), PhotosTab(), AboutTab()],
          ),
        ),
      ],
    );
  }
}

class PostsTab extends StatelessWidget {
  const PostsTab({super.key});
  @override
  Widget build(BuildContext context) => const Placeholder();
}

class PhotosTab extends StatelessWidget {
  const PhotosTab({super.key});
  @override
  Widget build(BuildContext context) => const Placeholder();
}

class AboutTab extends StatelessWidget {
  const AboutTab({super.key});
  @override
  Widget build(BuildContext context) => const Placeholder();
}

go deeper

for a junior

Recall that IndexedStack shows one child by index and keeps the others alive with their state.

for a middle

Explain what hidden children still do (build, layout, tickers) and skip (paint, hit testing, semantics, focus), and why the widget is as big as the largest child.

for a senior

Decide between IndexedStack, conditional children and external state for tabbed screens, and mute hidden work with TickerMode when it costs frames.

for a principal

Weigh memory and background work against state retention across an app's navigation shell, and set a policy for which screens keep state alive.

## What IndexedStack is `IndexedStack` is a `Stack` variant that displays exactly one of its children, the one at `index`. Its parameters mirror `Stack`'s, with one rename: - `index` — which child to show; defaults to `0`; `null` shows nothing. - `sizing` — the `StackFit` for non-positioned children; defaults to `StackFit.loose` (on `Stack` this is called `fit`). - `alignment` — defaults to `AlignmentDirectional.topStart`. - `clipBehavior` — defaults to `Clip.hardEdge`. The interview point is what happens to the children you cannot see. ## Hidden children: what is kept and what is skipped | Aspect | Selected child | Other children | |---|---|---| | Built and mounted | yes | yes | | `State` preserved | yes | yes | | Laid out | yes | yes | | Painted | yes | no | | Hit-tested | yes | no | | In the semantics tree | yes | no | | Can receive focus | yes | no (excluded) | | `Visibility.of` reports | visible | hidden | | Tickers | running | still running | The render object's documentation notes that although only one child is displayed, **layout is still O(N)**. Every child is laid out whenever the stack lays out, and it is therefore sized by the largest child. A short 'About' panel next to a tall 'Photos' grid makes the whole IndexedStack as tall as the grid. The widget wraps each child in two helpers: one that makes `Visibility.of` report non-selected children as hidden, and `ExcludeFocus` so keyboard focus cannot land on an invisible field. It does **not** add `TickerMode`, so an `AnimationController` that repeats in a hidden child keeps ticking and scheduling frames. Wrap children in `TickerMode(enabled: i == index, child: ...)` if that matters. ## When it is the right tool Use `IndexedStack` when **state must survive switching** and the set of views is small and fixed: 1. Bottom-navigation or segmented-control tabs on a profile screen, such as posts, photos and about, where each tab has its own scroll position, loaded data or half-typed text. 2. Wizard steps where the user can go back and find inputs intact. 3. Swapping between a view and an editor for the same item without re-creating controllers. ## When it is not - **Many or heavy children.** All of them are built up front and laid out on every pass; memory and layout cost grow with the count. - **Content that should not run while hidden.** Streams, timers and animations keep going unless you pause them. - **State that should reset.** If returning to a tab should start fresh, switch the child directly (a `switch` on the index), which unmounts the old one. - **Transitions.** `IndexedStack` switches instantly; an animated switch needs a different widget. ## Alternatives in one line each - Plain conditional child: cheapest, loses state on switch. - `IndexedStack`: keeps everything, pays for everything. - `Offstage` or `Visibility(maintainState: true)` on individual children: finer control over which children stay alive. - Restoration or an external state holder: keep the data outside the widgets so views can be rebuilt cheaply.

  • In Flutter, how would you stop a looping animation in a hidden IndexedStack child from wasting frames?
    `IndexedStack` does not mute tickers, so wrap each child in `TickerMode(enabled: i == index, child: child)`. Tickers created through `SingleTickerProviderStateMixin` or `TickerProviderStateMixin` observe `TickerMode` and stop scheduling frames while disabled, then resume when the tab is selected again.
  • Why is an IndexedStack taller than its visible child?
    Every non-positioned child is laid out on each pass, and a Stack sizes itself to the largest of them. The visible child does not get special treatment for sizing, so a short tab next to a tall one inherits the tall one's height. Give the IndexedStack a fixed or `Expanded` slot, or use `StackFit.expand` so every child fills it.

saying these in an interview costs you the question

  • IndexedStack only builds the child that is currently shown.
  • Hidden IndexedStack children are paused automatically.
  • IndexedStack sizes itself to the visible child.
  • Switching the index disposes the previous child's State.
  • Hidden children can still receive keyboard focus.