Flutter / Dart, widgets, and local ownership / Lesson 4
Local state and lifecycle ownership
Keep state outside build, request rebuilds synchronously, and release resources at the owner boundary.
On this page
Lesson goals
- Separate a description from its persistent state
- Keep initialization out of build
- Make a synchronous state transition
Separate a description from its persistent state
StatefulWidget remains an immutable configuration. Its separate State object holds mutable information across compatible rebuilds at the same identity. A local expansion flag or draft selection often belongs there because its lifetime follows a particular UI occurrence. Information shared across screens may need a broader owner. Choose the owner by who needs the information and how long it should survive, rather than by a rule that every value must be global. A rebuild does not itself initialize a fresh State object, but removing the identity can end that stateโs lifetime.
Keep initialization out of build
Initialize one-time resources in the appropriate lifecycle boundary and read current widget configuration through widget. initState is useful for initialization that does not depend on inherited lookups requiring dependency tracking. didChangeDependencies covers those inherited dependencies, while didUpdateWidget can react when a compatible widget supplies a different external object. Each hook has a different trigger. Starting the same timer in build would register another timer after unrelated visual changes. Whenever a subscription follows an input object, plan both how to detach from the old object and how to attach to the new one.
Make a synchronous state transition
The first fragment toggles only a local boolean. setState runs the callback synchronously and tells Flutter that the State needs rebuilding; it does not perform the domain operation for you. Keep the callback small and free of await. If the handler requires asynchronous work, await outside the callback and then commit the resulting state deliberately. The label is derived from the boolean rather than stored as a second mutable string, which prevents contradictory combinations such as an open panel whose button still says Show.
class DetailsPanel extends StatefulWidget {
const DetailsPanel({super.key});
@override State<DetailsPanel> createState() => _DetailsPanelState();
}
class _DetailsPanelState extends State<DetailsPanel> {
bool open = false;
@override Widget build(BuildContext context) => Column(children: [
TextButton(onPressed: () => setState(() { open = !open; }),
child: Text(open ? 'Hide' : 'Show')),
if (open) const Text('Local details'),
]);
}
Initially Show; after activation Hide and Local details.
The host supplies the material import and application context. Each activation requests a render of the newly derived description.
Dispose resources where they are owned
A controller can outlive a single build but must not outlive its owning State without an explicit transfer of ownership. The second fragment creates a TextEditingController once and disposes it when the owner leaves the tree. If a parent supplies a controller, the child normally must not dispose it because the parent still owns it. Apply the same reasoning to focus nodes, animation controllers, timers, and subscriptions, using their documented cancellation methods. Cleanup is about stopping ongoing work and releasing references, not merely suppressing an exception after the screen disappears.
class DraftField extends StatefulWidget {
const DraftField({super.key});
@override State<DraftField> createState() => _DraftFieldState();
}
class _DraftFieldState extends State<DraftField> {
final text = TextEditingController();
@override void dispose() { text.dispose(); super.dispose(); }
@override Widget build(BuildContext context) => TextField(controller: text);
}
A text field retains its draft across owner rebuilds and releases its controller on disposal.
Constructing the controller in build would replace the input owner repeatedly. Disposing a borrowed controller would violate the callerโs ownership.
Treat mounted as a lifetime check
After awaiting work, a State may already have been disposed. Checking mounted before touching its UI state prevents committing into a dead owner, but it does not cancel the underlying work or prove the result is still current. A search can remain mounted while its older query becomes obsolete. Later lessons add request identity for that case. When feasible, cancel the resource or subscription at its lifecycle boundary. Do not retain a context solely to display a delayed message; first establish that the intended UI owner still exists and still owns that interaction.
Derive values that do not need independent ownership
A subtotal computed from current line items should generally be derived, not maintained as another mutable value updated by every button. Duplicated state introduces synchronization obligations. Conversely, an editable draft intentionally differing from saved data deserves its own owner and save or cancel rules. Ask which source is authoritative and what event commits a change. These questions clarify whether setState is sufficient or a separate model is appropriate. A state-management package cannot answer ownership questions automatically; it offers mechanisms after the application contract has been decided.
Common errors and fixes
A draft resets whenever the parent rebuilds
- Why it happens
- The controller or draft is recreated inside build.
- How to fix it
- Store the resource in its State owner and release it in dispose.
- Prevent it next time
- Test a parent rebuild while the field contains input.
setState is called after dispose
- Why it happens
- A delayed callback outlived the State.
- How to fix it
- Cancel owned work and check lifetime before UI commits.
- Prevent it next time
- Exercise navigation away during pending work.
A borrowed controller stops working elsewhere
- Why it happens
- A child disposed a resource it did not own.
- How to fix it
- Let the creating owner dispose it and document borrowing.
- Prevent it next time
- Make resource ownership explicit in widget contracts.
Check your understanding
1. Should a locally opened help panel survive logging out?
Show worked solution
Define the intended lifetime first. If the panel belongs to the current screen, its local State can end when that screen leaves. If restoring it is a product requirement, persist a deliberately scoped preference rather than accidentally retaining the entire old screen or context.
2. A total is stored separately from the item list. What inconsistency can arise?
Show worked solution
An edit can update the list but forget to update the total, leaving two competing truths. Compute the total from immutable current items when practical. If caching is necessary, invalidate it under the same owner and verify every mutation path.
Key takeaways
- Separate a description from its persistent state
- Derive values that do not need independent ownership