In Flutter, what does IndexedStack do with the children it is not showing, and when is it the right tool for switching views?
answer
- one child shown by index
- all children built and laid out
- State survives switching
- sized by the largest child
- tickers are not muted
basics
~20 sIndexedStack 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 linesimport '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
Recall that IndexedStack shows one child by index and keeps the others alive with their state.
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.
Decide between IndexedStack, conditional children and external state for tabbed screens, and mute hidden work with TickerMode when it costs frames.
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.