Skip to content

Widgets and Lifecycle

Flutter’s rendering model is declarative. You do not mutate a view object in place; you describe What the UI should look like for a given state, and the framework diffs that description against the Current tree and applies the minimal set of changes. A widget is the immutable data structure That carries that description.

A widget is a blueprint, not a visual element. When you write const Text('hello')You have Created a configuration object that says “render the string ‘hello’ with default typography.” The Framework then instantiates an Element from that widget, and that Element may in turn create a RenderObject that actually performs layout, hit-testing, and painting.

The critical mental model:

LayerResponsibilityMutability
WidgetImmutable configuration / descriptionAlways immutable
ElementTree node that manages lifecycle, holds stateMutable (internal)
RenderObjectLayout, painting, hit-testingMutable (internal)

When you call setState()The framework marks the owning Element as dirty. On the next frame, The framework calls build() on that Element, which produces a new Widget tree. The framework Then reconciles the new widget tree against the old one at the Element level, reusing Elements where Possible and destroying/recreating them where the widget type or key changes.

This reconciliation is why widget identity matters. If you return a widget of the same runtimeType And key from build()The framework reuses the existing Element. If either changes, the old Element is unmounted and a new one is created. This is the foundational reason why const Constructors, keys, and widget splitting all affect performance.

The tree structure is a tripartite forest:

Widget tree (transient, rebuilt every frame)
Element tree (persistent across rebuilds, owns lifecycle)
RenderObject tree (persistent, handles layout/paint/hit-test)

The Widget tree exists only as a momentary configuration snapshot. The Element tree is the actual Persistent tree that the framework walks during layout and paint. The RenderObject tree is a subset Of the Element tree (only elements that need layout/paint create RenderObjects; structural elements Like Column``BuilderOr InheritedWidget do not).

An immutable widget whose configuration is fully determined by its constructor arguments. It has no Mutable internal state. The framework calls build(BuildContext) once per configuration change.

class TemperatureDisplay extends StatelessWidget {
const TemperatureDisplay({super.key, required this.celsius});
final double celsius;
@override
Widget build(BuildContext context) {
return Text('${celsius.toStringAsFixed(1)}°C');
}
}

Use StatelessWidget when the widget’s output depends solely on its constructor arguments and data Obtained from ancestor widgets (via BuildContextE.g., Theme.of(context)). The build method Should be a pure function of its inputs.

A widget that delegates its mutable state to a separate State object. The createState() factory Method is called once when the widget is inserted into the tree. The State object persists across Rebuilds even when the parent supplies a new StatefulWidget instance with updated fields.

class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Text('Count: $_count');
}
}

The separation between StatefulWidget (immutable config) and State (mutable lifecycle) is Deliberate: it allows the framework to swap a new widget config into an existing State object when The parent rebuilds. This is why didUpdateWidget receives the old widget — the current widget is Already available as widget.

A widget that establishes a shared data scope accessible by all descendants. When the InheritedWidget rebuilds, the framework efficiently notifies only those descendants that registered A dependency (via context.dependOnInheritedWidgetOfExactType).

class ThemeConfiguration extends InheritedWidget {
const ThemeConfiguration({
super.key,
required this.primaryColor,
required super.child,
});
final Color primaryColor;
static ThemeConfiguration of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeConfiguration>()!;
}
@override
bool updateShouldNotify(ThemeConfiguration oldWidget) {
return primaryColor != oldWidget.primaryColor;
}
}

The updateShouldNotify method is the critical optimization gate. Return true only when the data That descendants care about has actually changed. If you return true unnecessarily, every Dependent descendant rebuilds on every ancestor rebuild, which cascades into severe performance Degradation.

A widget whose child is provided by another source. The canonical examples are InheritedWidget (child comes from the constructor) and ParentDataWidget (which modifies the parent data of its Child in a MultiChildRenderObjectElement). You rarely implement ProxyWidget directly.

An interface (PreferredSizeWidget) implemented by widgets like AppBar that communicate their Preferred size to parent widgets ( Scaffold). This allows Scaffold to reserve the Correct amount of space in the layout without the AppBar needing to measure itself during the layout Phase.

When a widget constructor is invoked with constThe Dart VM canonicalizes the instance at compile Time. Every const SizedBox(height: 8) in your codebase resolves to the same object identity in Memory. This means the framework can skip the entire == / runtimeType check during Reconciliation — it performs a pointer identity check, which is O(1) and branch-predictor friendly.

BuildContext is not a class you instantiate. It is an abstract interface implemented by Element. When you receive a BuildContext inside build()You are holding a handle to the Element that owns the widget currently being built. This handle represents that element’s position In the tree.

  • Ancestor lookups: Theme.of(context)``MediaQuery.of(context)``Navigator.of(context) — all walk up the Element tree to find the nearest InheritedWidget of the target type.
  • Dependency registration: dependOnInheritedWidgetOfExactType both looks up the ancestor and registers the current element as a dependent, so the element rebuilds when the inherited widget changes.
  • Finding RenderObject: findRenderObject() returns the RenderObject associated with this element (or null if the element has no render object).

BuildContext is valid only while the Element is active in the tree. After deactivate() or dispose()The context is no longer valid. If you store a context and use it in an async gap (e.g., after await), the element may have been unmounted, causing a debugWidgetBuilder assertion Or, in release mode, silent corruption.

class _BadState extends State<Widget> {
late BuildContext _savedContext;
@override
void initState() {
super.initState();
_savedContext = context;
Future.delayed(const Duration(seconds: 5), () {
// context may be invalid here if the widget was removed
ScaffoldMessenger.of(_savedContext).showSnackBar(
const SnackBar(content: Text('This may crash')),
);
});
}
}

Instead, check mounted before using a stored context:

if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Safe')),
);
}

The mounted property on State returns true only while the State object is attached to an Element in the tree. Once dispose() is called, mounted becomes false and never reverts.

In Flutter 3.10+, context.lookup<T>() provides a non-depended-upon lookup of the nearest ancestor Of type T in the Element tree. Unlike dependOnInheritedWidgetOfExactTypeThis does not Register a dependency — meaning the calling element will not rebuild when the ancestor changes. Use this when you need to read a value once without subscribing to updates.

The lifecycle of a StatelessWidget is trivial because there is no state to manage:

Constructor called (with optional const)
Element created (StatelessElement)
build() called
Widget removed from tree
Element unmounted (no dispose hook)

There is no initState``disposeOr didUpdateWidget. The build() method is called every time The parent provides a new widget configuration with different constructor arguments (or the Framework cannot prove equivalence via ==).

If you can construct a StatelessWidget with constThe framework can skip the reconciliation Step entirely when the same const instance appears in consecutive builds. This is because pointer Identity (identical(oldWidget, newWidget)) is checked first, before any runtimeType or == Comparison.

// Good: same instance every time
const child = SizedBox(height: 8);
Column(children: [child, child, child]);
// Bad: three different instances
Column(children: [
SizedBox(height: 8),
SizedBox(height: 8),
SizedBox(height: 8),
]);

Use StatelessWidget when:

  • The widget’s visual output is a pure function of its constructor arguments.
  • The widget does not own any mutable resources (controllers, animations, timers).
  • The widget reads data from ancestors via BuildContext but does not modify them.

If you find yourself wanting an initState or disposeYou need a StatefulWidget.

This is the full lifecycle, in execution order:

createState() ← called once when widget is inserted
initState() ← first method on the State object
didChangeDependencies() ← called when inherited dependencies change
build() ← called every time state/dependencies change
didUpdateWidget(covariant oldWidget) ← called when parent rebuilds with new config
setState() called ← marks element dirty, schedules rebuild
deactivate() ← element removed from tree (may be reinserted)
dispose() ← permanent removal, release resources

Called by the framework when the StatefulWidget is inserted into the tree. This is the only place Where the State object is instantiated. The State object is then associated with the Element And persists as long as that Element remains in the tree — even across parent rebuilds that supply A new StatefulWidget instance.

Called exactly once, immediately after the State object is created and before the first build(). This is where you:

  • Initialize controllers (TextEditingController``AnimationController``ScrollController).
  • Subscribe to streams or notification listeners.
  • Set up timers.
  • Perform one-time computations.

You cannot use context.dependOnInheritedWidgetOfExactType here because the element has not yet Been registered as a dependency. Use didChangeDependencies() for that.

Called immediately after initState() and subsequently whenever a dependency obtained via BuildContext changes. A “dependency” is an InheritedWidget that was looked up using dependOnInheritedWidgetOfExactType (which is what Theme.of``MediaQuery.ofEtc. Do Internally).

@override
void didChangeDependencies() {
super.didChangeDependencies();
_mediaQuery = MediaQuery.of(context);
}

If you do not call any of(context) methods, this method is still called once after initState() But will not be triggered again unless you later register a dependency.

Called after initState()``didChangeDependencies()And after every setState(). Must return a Widget. Should be a pure function of this.state and this.widget (and any inherited data). Must Not have side effects — do not call setState() inside build().

Called when the parent rebuilds and provides a new StatefulWidget instance. The framework has Already updated widget to the new instance. oldWidget is the previous configuration. Use this to React to configuration changes that require state adjustments:

@override
void didUpdateWidget(covariant MyWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.dataSource != oldWidget.dataSource) {
_controller.clear();
_loadData(widget.dataSource);
}
}

Called when the element is removed from the tree. The element might be reinserted (e.g., when Moved to a different position in the tree via a key change). If reinserted, the framework calls initState() again (actually, it reuses the State object and calls didChangeDependencies() then build()). Do not release resources here — use dispose().

Called when the element is permanently removed from the tree. This is your last chance to release Resources:

  • Dispose controllers (_controller.dispose()).
  • Cancel subscriptions (_subscription.cancel()).
  • Cancel timers (_timer.cancel()).
  • Remove listeners.

After dispose()``mounted is false. You must not call setState() after dispose().

When you call setState()The following occurs:

  1. The callback is executed synchronously (state is mutated immediately).
  2. The framework calls markNeedsBuild() on the owning Element.
  3. markNeedsBuild() adds the element to the _dirtyElements list on WidgetsBinding.
  4. WidgetsBinding schedules a frame via SchedulerBinding.scheduleFrame().
  5. On the next frame, buildScope iterates over dirty elements in depth-first order and calls rebuild() on each.
  6. rebuild() invokes performRebuild() which calls your build() method.

The rebuild is deferred — it does not happen synchronously inside setState(). This means you can Call setState() multiple times within a single event handler, and only one rebuild occurs.

Calling setState after dispose:

_timer = Timer(Duration(seconds: 5), () {
if (!mounted) return; // Must check
setState(() { _value = 42; });
});

Calling setState during build:

@override
Widget build(BuildContext context) {
setState(() { _x = 1; }); // Throws: setState() or markNeedsBuild() called during build
return Container();
}

This causes an infinite loop because setState() triggers build()Which calls setState() Again.

setState callback vs imperative state changes:

The setState callback should only contain synchronous state mutations. If you need to await an Async operation, call setState after the await completes:

Future<void> _fetch() async {
final result = await _api.getData();
if (!mounted) return;
setState(() { _data = result; });
}

The State object associated with a StatefulWidget persists across parent rebuilds. When the Parent rebuilds and constructs a new MyWidget(foo: 2) to replace MyWidget(foo: 1)The framework Reuses the existing State object and calls didUpdateWidget. The State object is only discarded If the Element is removed from the tree or if the widget’s runtimeType or key changes.

This is why fields in State survive rebuilds while fields in the widget do not. The widget is Re-instantiated every build; the state persists.

Keys provide a secondary identity criterion for widgets. During reconciliation, the framework Matches old and new children by (runtimeType, key). If two widgets have the same runtimeType but Different keys, the framework treats them as different widgets — the old Element is unmounted and a New one is created.

Identity by value equality. The T must implement == and hashCode. Use when the widget’s Identity is tied to a data value:

ListView(
children: items.map((item) => ListTile(
key: ValueKey&lt;String&gt;(item.id),
title: Text(item.name),
)).toList(),
)

Use ValueKey<String> for IDs, ValueKey<int> for indices (only when the list order is stable).

Generates a unique identity every time it is constructed. Every rebuild creates a new UniqueKey Which forces the framework to create a new Element. This is almost never what you want in a Production list — it defeats element reuse entirely. Use sparingly, to force a widget to Fully rebuild (e.g., resetting animation state).

Provides identity that is global across the entire widget tree. Only one widget in the tree can have A given GlobalKey at any time. Global keys enable:

  • Accessing State: globalKey.currentState gives you the State object, allowing you to call methods on it.
  • Accessing Element: globalKey.currentContext gives you the BuildContext.
  • Accessing RenderObject: globalKey.currentContext!.findRenderObject().
  • Moving widgets across subtrees: A widget with a GlobalKey can be moved from one parent to another without losing its state, because the framework re-parents the existing Element.
final _formKey = GlobalKey&lt;FormState&gt;();
Form(
key: _formKey,
child: Column(children: [...]),
);
// Later, in a button callback:
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
}

Global keys are expensive — they require O(1) lookups in a global hash map during every rebuild. Do Not use them inside ListView.builder or other lazily-built lists. They also prevent garbage Collection of the Element until the key is nulled out or reassigned.

Identity by object identity (identical()). Use when the widget should be matched by the exact Object reference, not by value equality. This is rare but useful when your data objects do not Implement ==/hashCode and you want stability tied to the object itself.

ScenarioKey Type
List items with unique IDsValueKey<String>
Stable list with fixed orderValueKey<int> (index)
Force full widget rebuildUniqueKey
Access State from outsideGlobalKey
Cross-subtree reparentingGlobalKey
Objects without ==/hashCodeObjectKey
  • Using index-based keys on reorderable lists: When items are reordered, indices shift, causing the framework to mismatch states. Always use value-based keys for reorderable lists.
  • GlobalKey in a builder: GlobalKey inside ListView.builder causes severe performance issues and state leaks. Use ValueKey instead.
  • Forgetting keys entirely on homogeneous lists: Without keys, inserting at the top of a list causes every element to reuse the wrong state.

The const keyword instructs the Dart compiler to create a canonicalized instance. All const Expressions with the same constructor and arguments evaluate to the identical object in memory:

const a = SizedBox(height: 8);
const b = SizedBox(height: 8);
assert(identical(a, b)); // true — same object

For Flutter’s reconciliation, identical(oldWidget, newWidget) is the cheapest possible check. When It returns trueThe framework skips the entire update process — no didUpdateWidgetNo build() call on the child.

Use const on every widget constructor where all arguments are compile-time constants. This Includes:

  • const SizedBox()``const EdgeInsets.all(16)``const EdgeInsets.symmetric(horizontal: 16)
  • const Text('static string') (only when the string is a literal)
  • const Color(0xFF000000)
  • const Duration(seconds: 1)
  • const Icon(Icons.add)

Do not use const when arguments are runtime values:

  • const Text(dynamicValue) — compilation error
  • const SizedBox(height: computedHeight) — compilation error

The performance impact of const is measurable but often overstated for leaf widgets. The primary Benefit is reducing GC pressure (fewer short-lived widget objects) and skipping reconciliation work. In deeply nested lists or frequently rebuilt subtrees, const on leaf widgets (like SizedBox Padding``Divider) can meaningfully reduce frame build time.

However, const does not eliminate rebuilds of the parent. If a StatefulWidget calls setState()Its build() runs regardless of whether the returned children are const. The Benefit is that the framework can cheaply determine that those children haven’t changed and skip Their subtree reconciliation.

const widgets can have keys, but the key must also be const:

const child = SizedBox(key: ValueKey('spacer'), height: 8);

If the key is not const (e.g., ValueKey(dynamicValue)), the constructor cannot be const.

WidgetPurposeKey Properties
ContainerConvenience box with padding, margin, decoration, constraintspadding``margin``decoration``constraints``alignment``color
PaddingAdds empty space inside a parentpadding (EdgeInsets)
AlignAligns child within itselfalignment``widthFactor``heightFactor
CenterCenters child (shorthand for Align(alignment: Alignment.center))widthFactor``heightFactor
ExpandedForces child to fill remaining space in Flexflex``child
FlexibleLike Expanded but allows child to be smallerflex``fit (tight/loose)
SizedBoxBox with specific dimensionswidth``height``child
WrapFlow layout (wraps to next line)direction``spacing``runSpacing``alignment
StackOverlapping childrenalignment``fit``clipBehavior
PositionedPositions child within Stacktop``right``bottom``left``width``height
RowHorizontal flex layoutmainAxisAlignment``crossAxisAlignment``mainAxisSize
ColumnVertical flex layoutmainAxisAlignment``crossAxisAlignment``mainAxisSize
WidgetPurposeKey Properties
TransformApplies a matrix transform before paintingtransform``alignment``origin
OpacityMakes child partially transparentopacity``alwaysIncludeSemantics
VisibilityHides child but reserves layout space (or collapses)visible``maintainSize``maintainAnimation``maintainState
AbsorbPointerAbsorbs hit-testing (child and descendants don’t receive events)absorbing``child
IgnorePointerIgnores hit-testing on itself but descendants can still receive eventsignoring``child
WidgetPurpose
LayoutBuilderProvides parent constraints via BoxConstraints in a builder callback
MediaQueryAccesses screen size, pixel ratio, text scale factor, padding, view insets
ThemeProvides a ThemeData subtree to descendants
IconRenders an icon from a font (Material or custom)
ImageDisplays an image from asset, network, file, or memory

Container is a composite widget that wraps multiple other widgets. When you set colorIt uses a DecoratedBox with a BoxDecoration(color: ...). When you set paddingIt wraps the child in a Padding. When you set both margin and paddingIt wraps in Padding(margin: ...) then DecoratedBox(padding: ...). If you only need one property, prefer the specific widget (e.g., Padding instead of Container(padding: ...) with no other properties) to avoid unnecessary Nesting.

Expanded is Flexible(fit: FlexFit.tight). A tight FlexFit forces the child to fill the Allocated space. A loose FlexFit allows the child to be smaller than the allocated space. Use Expanded when the child should fill the remaining space. Use Flexible when the child should Participate in flex distribution but may not need all the space (e.g., a Row where one child Should shrink-wrap).

Stack lays children on top of each other. Positioned children are placed at absolute offsets Within the stack. Non-positioned children are laid out by the stack’s alignment property. Positioned.fill() is a convenience for Positioned(top: 0, right: 0, bottom: 0, left: 0).

Flutter maintains three parallel trees. Understanding their relationship is essential for debugging Performance issues, understanding lifecycle methods, and knowing why certain operations are cheap or Expensive.

Widget ──configures──► Element ──owns──► RenderObject
(blueprint) (tree node) (layout/paint)
  • Widget: Immutable, short-lived, created every build. Cheap to allocate. Contains createElement() which creates the corresponding Element.
  • Element: Persistent, mutable. Created once per tree insertion. Manages the lifecycle (initState, dispose, etc.). Implements build() (delegates to widget.build). Owns the RenderObject (if applicable).
  • RenderObject: Handles layout (performLayout), painting (paint), and hit-testing (hitTest). Mutated during layout, painted during the paint phase. Not all Elements have RenderObjects (e.g., ComponentElement subclasses like StatelessElement and StatefulElement do not).
Element TypeCorresponding WidgetHas RenderObject?
StatelessElementStatelessWidgetNo
StatefulElementStatefulWidgetNo
InheritedElementInheritedWidgetNo
SingleChildRenderObjectElementSingleChildRenderObjectWidgetYes
MultiChildRenderObjectElementMultiChildRenderObjectWidgetYes
LeafRenderObjectElementLeafRenderObjectWidgetYes

When build() returns a new widget, the framework compares it against the old widget at each Position in the child list:

  1. Same runtimeType, same key → reuse Element, call update() (which calls didUpdateWidget on StatefulElements, then build()).
  2. Different runtimeType or different key → unmount old Element (calls deactivatedispose), create new Element (calls createstateinitStatebuild).
  3. Child list diff → uses a greedy algorithm (O(n) for most cases) that matches by (runtimeType, key) from the head of both old and new lists, then from the tail.

InheritedElement overrides notifyClients() to iterate over its registered dependents (stored in _dependentsA map of Element → Object?). When an InheritedWidget’s updateShouldNotify Returns trueThe InheritedElement calls notifyClientsWhich marks each dependent element as Dirty. This is the mechanism by which Theme.of(context)``Provider.of(context)And similar APIs Trigger targeted rebuilds.

  • Excessive rebuilds: Profile with Flutter DevTools’ “Repaint Rainbow” or the debugPrintRebuildDirtyFrames flag. If a subtree rebuilds too often, check whether it has unnecessary dependencies on InheritedWidgets (e.g., depending on MediaQuery when you only need the theme).
  • Missing dispose: If a RenderObject-backed widget leaks (e.g., animation controller not disposed), the Element and its RenderObject remain in memory even after the widget is removed from the tree.
  • Deep trees: Each Element in the tree adds overhead to reconciliation. Flattening widget trees (replacing nested Padding widgets with a single Container or Padding with an EdgeInsets that combines margins) reduces traversal cost.

As discussed in Section 3, storing BuildContext beyond the synchronous scope of a method is Unsafe. After deactivate()The context is no longer valid. Always check mounted before using a Context captured in a closure or async continuation.

void _handleAsync() async {
final result = await _service.fetch();
if (!mounted) return;
setState(() { _data = result; });
}

This occurs most frequently with timers, animations, and stream subscriptions that outlive the Widget’s lifetime. The fix is always one of:

  • Cancel the resource in dispose().
  • Check mounted before calling setState().
  • Both.
late final StreamSubscription _sub;
@override
void initState() {
super.initState();
_sub = _stream.listen((data) {
if (!mounted) return;
setState(() { _data = data; });
});
}
@override
void dispose() {
_sub.cancel();
super.dispose();
}

Using StatefulWidgets When StatelessWidget Suffices

Section titled “Using StatefulWidgets When StatelessWidget Suffices”

A common anti-pattern is wrapping every widget in a StatefulWidget “just in case.” This adds Memory overhead (the State object and its Element) and makes the code harder to reason about. Start with StatelessWidget. Convert to StatefulWidget only when you have a concrete need for Mutable state, lifecycle hooks, or controller ownership.

A useful heuristic: if the widget’s build() output changes only in response to parent rebuilds (i.e., the parent passes new constructor arguments), use StatelessWidget. If the widget’s output Can change independently (e.g., an animation tick, a timer, user input), use StatefulWidget.

When using ListView``ColumnOr Row with a list of children of the same type, omitting keys Causes the framework to match children by position. This works fine for static lists but breaks when Items are added, removed, or reordered:

// Bad: inserting at index 0 reuses all existing elements with wrong state
Column(children: [
_buildItem(newItem), // inserted at top
_buildItem(items[0]), // framework thinks this is the same as old items[0]
_buildItem(items[1]),
]);
// Good: value keys ensure correct element reuse
Column(children: [
_buildItem(newItem, key: ValueKey(newItem.id)),
_buildItem(items[0], key: ValueKey(items[0].id)),
_buildItem(items[1], key: ValueKey(items[1].id)),
]);

The primary sources of unnecessary rebuilds:

  1. Not splitting widgets: If a build() method returns a large subtree but only part of it depends on changing state, split the changing part into a separate widget. Only the separate widget rebuilds on setState().
// Bad: entire Column rebuilds on every counter change
class _MyState extends State<MyWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
Text('Count: $_count'),
ExpensiveStaticChild(), // this rebuilds too!
]);
}
}
// Good: only CounterDisplay rebuilds
class _MyState extends State<MyWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
CounterDisplay(count: _count),
ExpensiveStaticChild(), // const-eligible, never rebuilds
]);
}
}
  1. Missing const constructors: Without constEvery rebuild creates new widget instances, forcing the framework to do full reconciliation. With constThe framework can skip reconciliation via pointer identity.

  2. Over-depending on InheritedWidgets: If a widget calls Theme.of(context) inside build() but does not actually use theme data in its output, it still registers as a dependent and rebuilds whenever the theme changes. Extract the theme lookup to a parent widget if possible.

Every State object that creates disposable resources must release them in dispose():

ResourceCleanup Method
TextEditingController_controller.dispose()
ScrollController_scrollController.dispose()
AnimationController_animationController.dispose()
StreamSubscription_subscription.cancel()
Timer_timer.cancel()
FocusNode_focusNode.dispose()
ChangeNotifier (owned)_notifier.dispose()
PageController_pageController.dispose()

Failing to dispose controllers causes memory leaks. The controller holds references to listeners, Which hold references to State objects, which hold references to Element objects, which hold References to RenderObject objects. The entire subtree remains in memory.

Calling setState() during build() throws a FlutterError. This is because the framework is Already in the process of building — marking the element dirty again creates a contradiction. If you Need to trigger a rebuild as a side effect of building, defer it:

WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() { /* ... */ });
});

However, this is almost always a design smell. Re-examine why you need to mutate state during build. Common root causes include:

  • Derived state that should be computed in build() instead of stored.
  • Side effects that belong in didChangeDependencies() or didUpdateWidget().
  • Initialization logic that belongs in initState().

Using context.dependOnInheritedWidgetOfExactType in initState

Section titled “Using context.dependOnInheritedWidgetOfExactType in initState”

This throws because the element is not yet registered in the tree during initState(). Move the Lookup to didChangeDependencies():

@override
void initState() {
super.initState();
// BAD: Theme.of(context) here will throw
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_theme = Theme.of(context); // GOOD
}

Note that in modern Flutter (2.3+), this restriction is somewhat relaxed — you can use context in initState() for non-dependent lookups, but Theme.of``MediaQuery.ofEtc. Still fail because They internally call dependOnInheritedWidgetOfExactType.

Building blocks of UI: Widgets are like LEGO bricks — they combine to create complex user interfaces. Each widget has a lifecycle that determines when it’s created, updated, and destroyed.

Why it matters: Understanding widget lifecycle helps you manage resources efficiently and avoid memory leaks in Flutter apps.

The key insight: StatefulWidget has state that can change; StatelessWidget is immutable — choose based on whether the widget needs to update.

This topic covers the core concepts of widgets and lifecycle, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • core concepts and terminology
  • algorithms and computational thinking
  • practical implementation
  • security and ethical considerations
  • applications in the real world

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.