Skip to content
Boring Doc
Boring Doc explanation Verified against Flutter 3.47.7 3 min readstateephemeral stateapp statelifting state upInheritedWidget

State management

Where data lives and how the UI reacts when it changes.

Flutter is declarative: the UI is a function of state, so instead of mutating widgets you change the data and let the affected widgets rebuild. Keep state that only one widget cares about in a StatefulWidget; lift state that several screens share above them, expose it through an InheritedWidget (or a package that wraps one), and notify listeners with ChangeNotifier so only dependent widgets rebuild.

  1. Ephemeral (local) state lives in one widget and nobody else needs it: a tab index, a text field's focus, an animation's progress. setState is the right tool and nothing more is needed.
  2. App (shared) state is needed by several widgets or must outlive a screen: login info, a cart, preferences. It belongs above the widgets that read it.
  3. Lifting state up means moving data to a common ancestor and passing values down and callbacks up. It works, but passing callbacks through many layers gets tedious.
  4. InheritedWidget lets any descendant find a value through BuildContext; dependents are rebuilt when the inherited widget is replaced with one whose updateShouldNotify returns true. provider and friends are built on this mechanism.
  5. ChangeNotifier is a plain Dart class from foundation: subclass it, mutate fields, call notifyListeners(). ListenableBuilder, ValueListenableBuilder and InheritedNotifier turn those notifications into targeted rebuilds.
  6. Packages (provider, riverpod, bloc, etc.) reduce boilerplate and add tooling; the official docs suggest provider as a sensible default if you have no strong preference, but none of them is required.
Flow diagramUnidirectional data flow with a ChangeNotifier

Widgets never edit each other. An event calls a method on the model, the model mutates its fields and notifies, and only the widgets that subscribed are rebuilt on the next frame.

A notice board with a bell

Shared state is a notice board hung in the hallway (an ancestor in the tree) rather than a note passed hand to hand. Any room (widget) can walk out and read it through its door (BuildContext). Rooms that care sign a subscriber list; when someone updates the board they ring the bell (notifyListeners) and only the rooms on the list go out to read it again. Rooms that just want to pin a new note (call a method) do not have to subscribe at all.

Where the analogy breaks: The bell does not say what changed, so every subscriber re-reads the whole board even if only one line moved; that is why you keep builders small and deep in the tree. The analogy also hides that subscriptions must be cancelled (dispose the notifier, or let ListenableBuilder do it) and that the board is found by walking up the tree, so a widget on a different branch (a pushed route, a dialog) only sees it if the board hangs above the Navigator.

Minimal working example

ChangeNotifier plus ListenableBuilder, no package
dart
import 'package:flutter/material.dart';

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

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

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  final CounterModel _model = CounterModel();

  @override
  void dispose() {
    _model.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ListenableBuilder(
          listenable: _model,
          builder: (context, child) => Text('Count: ${_model.count}'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _model.increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
Expected: Each tap increments the number. Only the Text inside ListenableBuilder is rebuilt; CounterPage's build method itself does not run again.Flutter ≥ 3.10.0Rebuild UI when state changes (learning pathway)
  1. L3ChangeNotifier comes from package:flutter/foundation.dart; it knows nothing about widgets, which makes it easy to unit test.
  2. L9notifyListeners is the only framework-specific call. Everything else in the class is ordinary business logic.
  3. L21The model is owned by the State so it survives rebuilds; create it once, not inside build.
  4. L25A ChangeNotifier holds listener callbacks; dispose it when its owner goes away.
  5. L33ListenableBuilder subscribes to the model and re-runs only its builder when it notifies. The Scaffold and button above are not rebuilt.
  6. L39The button only calls a method. It never needs to rebuild, so it stays outside the builder.

Use cases

  • Use setState for state that one widget owns and nobody else reads (toggles, text field focus, page index).
  • Use lifting state up with callbacks when a parent and one or two children share a value and the tree is shallow.
  • Use InheritedWidget/InheritedNotifier (or provider, which wraps the same idea) when many widgets at different depths need the same object, such as a session, a cart or a theme.
  • Use ValueNotifier + ValueListenableBuilder for a single value that changes often (a progress fraction, a search query) and should rebuild only one small subtree.
  • Reach for a package such as provider, riverpod or bloc when you want less boilerplate, dependency injection for tests, or an event-driven architecture across a large team.

Benefits

  • Declarative rebuilds mean there is one code path per UI state: no imperative 'update this label' calls scattered around.
  • ChangeNotifier models are plain Dart objects, so business logic is unit-testable without pumping widgets.
  • Targeted listeners (ListenableBuilder, Consumer-style widgets placed deep in the tree) keep rebuilds small and cheap.

Limitations

  • InheritedWidget lookups are tree-scoped: a widget can only see providers that are its ancestors, which bites in dialogs and routes if the provider sits too low.
  • notifyListeners is coarse: every dependent rebuilds, even if the field it reads did not change. Splitting models or using InheritedModel/selectors is up to you.
  • ChangeNotifier is not persistent and not restorable by itself; app state that must survive a process restart still needs storage and RestorationMixin or similar.

Alternatives and trade-offs

  • provider — You want ChangeNotifierProvider/Consumer convenience over hand-written InheritedWidgets; the official docs suggest it as a default starting point.
  • riverpod — You want providers that do not depend on BuildContext, are compile-time safe and easy to override in tests.
  • bloc / flutter_bloc — You prefer explicit events and states with traceable transitions, typically on larger teams.
  • StatefulWidget + lifecycle hooks — The state really is local; see widgets and lifecycle before adding any of the above.

Mutating a collection without notifying

Problem: Adding to a List held by a ChangeNotifier changes the data but no listener hears about it, so the UI keeps showing the old list until something else triggers a rebuild.

Fix: Keep fields private, expose read-only views, and make every mutation go through a method that ends with notifyListeners().

✗ Silent mutation
dart
class CartModel extends ChangeNotifier {
  final List<String> items = [];
}

// elsewhere
cart.items.add('Dash'); // nobody is notified
✓ Mutation through the model
dart
class CartModel extends ChangeNotifier {
  final List<String> _items = [];
  List<String> get items => List.unmodifiable(_items);

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }
}

Calling notifyListeners or setState during build

Problem: Triggering a change while the framework is already building (for example fetching data in build and notifying when it completes synchronously, or notifying from a constructor called in build) throws 'setState() or markNeedsBuild() called during build'.

Fix: Start work from initState or an event handler, and let asynchronous results notify later. If you must react to a build-time condition, schedule it with WidgetsBinding.instance.addPostFrameCallback.

✗ Model created and kicked off in build
dart
@override
Widget build(BuildContext context) {
  final vm = ArticleViewModel(ArticleModel()); // fetches and notifies immediately
  return ListenableBuilder(listenable: vm, builder: (_, __) => Text('${vm.summary}'));
}
✓ Model owned by State, started in initState
dart
final ArticleViewModel _vm = ArticleViewModel(ArticleModel());

@override
void initState() {
  super.initState();
  _vm.fetchArticle();
}

@override
Widget build(BuildContext context) {
  return ListenableBuilder(listenable: _vm, builder: (_, __) => Text('${_vm.summary}'));
}

Providing state below the Navigator

Problem: An InheritedWidget placed inside a page's Scaffold is invisible to routes pushed later and to dialogs, because those are built under the Navigator, not under that page. Lookups return null and the app crashes with a null check.

Fix: Place the provider/scope above MaterialApp (or at least above the Navigator) when routes need it. Keep page-only state inside the page.

Wrapping the whole page in one builder

Problem: A single ListenableBuilder or Consumer around the entire screen rebuilds every widget on every notification, including expensive lists and images that did not change.

Fix: Push builders down to the leaves that actually display model data, and use the builder's `child` parameter to pass pre-built subtrees that should not rebuild.

This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.

  • State management (intro) Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/data-and-backend/state-mgmt/intro
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Start thinking declaratively Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/data-and-backend/state-mgmt/declarative
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Ephemeral vs app state Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/data-and-backend/state-mgmt/ephemeral-vs-app
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Simple app state management Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/data-and-backend/state-mgmt/simple
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Approaches to state management Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/data-and-backend/state-mgmt/options
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • State management with ChangeNotifier (learning pathway) Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/learn/pathway/tutorial/change-notifier
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Rebuild UI when state changes (learning pathway) Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/learn/pathway/tutorial/listenable-builder
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
    Last checked
    Oct 21, 2026
Was this useful?
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.