In Flutter, how do you lift state to a common ancestor, and how do child widgets read and change it through constructor values and callbacks?
answer
- two widgets need one value
- the nearest common ancestor owns it
- values down, events up
- ValueChanged<T> callbacks
- Switch and Checkbox hold no value
basics
~20 sMove the value into the State of the nearest widget above everyone who reads it, pass it down as constructor arguments, and pass callbacks such as ValueChanged<bool> down so children report changes; the owner updates the value with setState.
solid answer
~50 sWhen two widgets need the same value — a notes list screen whose header shows "3 pinned" and whose tiles toggle pinning — the value moves to the **nearest common ancestor**, here the screen's `State`. Children become mostly stateless: a `NoteTile` receives `note` and `onPinnedChanged` (`ValueChanged<bool>`) in its constructor, shows the value it was given, and calls the callback on tap; the screen runs `setState`, rebuilds, and constructs new tiles with the new values. Data flows down through constructors, events flow up through callbacks, and there is one copy of the truth. Flutter's own `Switch` and `Checkbox` are built this way: they hold no value, and a `null` `onChanged` renders them disabled. The docs' guidance: user data such as a checkbox's value belongs to the parent, purely visual state such as an animation to the widget itself, and when in doubt, start with the parent.
code
dart · 57 linesimport 'package:flutter/material.dart';
class Note {
const Note({required this.id, required this.title, this.pinned = false});
final String id;
final String title;
final bool pinned;
Note copyWith({bool? pinned}) => Note(id: id, title: title, pinned: pinned ?? this.pinned);
}
class NotesScreen extends StatefulWidget {
const NotesScreen({super.key, required this.initialNotes});
final List<Note> initialNotes;
@override
State<NotesScreen> createState() => _NotesScreenState();
}
class _NotesScreenState extends State<NotesScreen> {
late List<Note> _notes = widget.initialNotes;
void _setPinned(Note note, bool pinned) {
setState(() {
_notes = [for (final n in _notes) n.id == note.id ? n.copyWith(pinned: pinned) : n];
});
}
@override
Widget build(BuildContext context) {
final int pinnedCount = _notes.where((n) => n.pinned).length; // derived, not stored
return Scaffold(
appBar: AppBar(title: Text('$pinnedCount pinned')),
body: ListView(
children: [
for (final note in _notes)
NoteTile(note: note, onPinnedChanged: (pinned) => _setPinned(note, pinned)),
],
),
);
}
}
class NoteTile extends StatelessWidget {
const NoteTile({super.key, required this.note, required this.onPinnedChanged});
final Note note;
final ValueChanged<bool> onPinnedChanged;
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: Text(note.title),
value: note.pinned,
onChanged: onPinnedChanged,
);
}
}go deeper
Remember the shape: the owner holds the value in State, passes it down in constructors, and passes callbacks for children to report changes.
Walk through finding the nearest common ancestor, choosing callback types, and why Switch and Checkbox need the parent to rebuild them.
Catch duplicated or derived state and stale copies in child State, and lift only as far as the readers require.
Set conventions for controlled versus self-managed widgets across a codebase, so reusable components behave predictably for every team.
## Why state has to move up Flutter widgets are **immutable**: you do not call `noteTile.setPinned(true)` on an existing widget. To change what a widget shows, its parent builds a **new** widget with new constructor arguments. The docs put it as "`MyCart(contents)` (a constructor), not `MyCart.updateWith(somethingNew)` (a method call)". Because only a parent's `build` can construct a widget, any value that should change what a widget shows must live in that widget or above it. When **two or more widgets** need the same value, it must live above all of them: in the **nearest common ancestor**. That is lifting state up. ## A worked example A notes app shows a list of notes and a header reading "3 pinned". Each tile has a pin toggle. 1. **Find every reader and writer.** The header reads the pinned count; each tile reads and writes one note's `pinned` flag. 2. **Find their nearest common ancestor.** Both sit under `NotesScreen`. 3. **Move the data there.** `_NotesScreenState` holds `List<Note> _notes`. 4. **Pass values down.** Each `NoteTile` gets its `Note`; the header gets the count, computed in `build` from `_notes` rather than stored separately. 5. **Pass callbacks down.** Each tile gets `ValueChanged<bool> onPinnedChanged`; the screen implements it with `setState`. ```dart NoteTile( note: note, onPinnedChanged: (pinned) => setState(() { _notes = [for (final n in _notes) n.id == note.id ? n.copyWith(pinned: pinned) : n]; }), ) ``` `NoteTile` can now be a `StatelessWidget`: it renders what it was given and reports taps. ## The callback types Flutter's foundation library defines the usual signatures: | Typedef | Signature | Typical use | |---|---|---| | `VoidCallback` | `void Function()` | a tap with no data (`onPressed`) | | `ValueChanged<T>` | `void Function(T value)` | a new value chosen by the user (`onChanged`) | | `ValueSetter<T>` | `void Function(T value)` | a value set by some other means | | `ValueGetter<T>` | `T Function()` | reading a value lazily | Using them keeps your widgets consistent with the framework's own. ## Flutter's widgets follow the same pattern - **`Switch` and `Checkbox`** hold no value of their own. The parent passes `value` and `onChanged`; when the user taps, the widget calls `onChanged` and the parent must rebuild it with the new value. A `null` `onChanged` renders the control disabled. - **`TextField`** is the mixed case: pass a `TextEditingController` to own the text from above, or pass none and the field creates its own internally. The interactivity docs describe the three options explicitly — the widget manages its own state, the parent manages it, or a mix — with the rule of thumb that **user data** (a checkbox's checked state, a slider's position) is best managed by the parent, **aesthetic** state (an animation, a pressed highlight) by the widget itself, and "if in doubt, start by managing state in the parent widget". ## Pitfalls 1. **Copying the value into the child's own State.** A child that stores `widget.note.pinned` in a field during `initState` keeps showing the old value after the parent changes it. Read from `widget` in `build` instead. 2. **Storing derived values.** A separate `_pinnedCount` field drifts from `_notes`; compute it in `build`. 3. **Mutating in place without `setState`.** Changing `_notes` without `setState` schedules no rebuild. 4. **Lifting too far.** Lift to the *nearest* common ancestor, not the app root, so unrelated screens do not rebuild. ## Designing a reusable child Lifting state changes how you write the child widget, not just the parent: - Take the **value** and a **nullable callback** in the constructor, like `Switch` does, so a `null` callback can mean read-only or disabled. - Keep the child **stateless** unless it also has purely visual state of its own, such as a pressed highlight; that part stays local, which is the docs' mix-and-match approach. - Name callbacks for **what happened**, not what the parent should do: `onPinnedChanged`, not `updateParentList`, so the child stays usable in other screens. ## Where lifting stops Lifting works well while the owner is a level or two above its readers. When callbacks must be threaded through many widgets that do not use them, or across screens, Flutter's inherited-widget mechanism, notifier objects and state libraries take over — each a topic of its own.
- A NoteTile copies widget.note.pinned into its own State in initState, and stops updating when the parent changes it. Why?`initState` runs once, so the child's copy is frozen at the first value while the parent keeps rebuilding the tile with new data. There are now two copies of the truth. Read `widget.note.pinned` directly in `build`, or, if the child truly needs local state derived from it, update that state when the widget's configuration changes.
- Why does a Flutter Switch with onChanged set but no setState in the parent never visibly toggle?`Switch` holds no value of its own; it always draws the `value` its parent passes. Tapping calls `onChanged`, but if the parent does not store the new value and rebuild with `setState`, the next build passes the old `value` again and the switch stays where it was.
- The header shows the pinned count. Should the screen store it in a separate field?No. Compute it in `build` from the notes list. A stored count is a second copy that must be updated everywhere the list changes, and any missed path leaves the header wrong. Lifting state works best when each fact is held once and everything else is derived.
Lifting state up works like a vet clinic's front desk: the desk keeps the one appointment book, each exam room shows the slip it was handed, and when a room needs a change it phones the desk instead of editing its own copy. The desk updates the book and hands out fresh slips.
saying these in an interview costs you the question
- A child should call a method on its sibling widget to update it.
- Flutter's Switch keeps its own on/off value internally.
- State should always be lifted to the root of the app to be safe.
- Copying a constructor value into State in initState keeps it in sync.
- Callbacks are only for events; children should mutate the shared list directly.