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.
- 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.
- 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.
- 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.
- 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.
- ChangeNotifier is a plain Dart class from foundation: subclass it, mutate fields, call notifyListeners(). ListenableBuilder, ValueListenableBuilder and InheritedNotifier turn those notifications into targeted rebuilds.
- 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.
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.
Minimal working example
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),
),
);
}
}- L3ChangeNotifier comes from package:flutter/foundation.dart; it knows nothing about widgets, which makes it easy to unit test.
- L9notifyListeners is the only framework-specific call. Everything else in the class is ordinary business logic.
- L21The model is owned by the State so it survives rebuilds; create it once, not inside build.
- L25A ChangeNotifier holds listener callbacks; dispose it when its owner goes away.
- L33ListenableBuilder subscribes to the model and re-runs only its builder when it notifies. The Scaffold and button above are not rebuilt.
- 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().
class CartModel extends ChangeNotifier {
final List<String> items = [];
}
// elsewhere
cart.items.add('Dash'); // nobody is notifiedclass 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.
@override
Widget build(BuildContext context) {
final vm = ArticleViewModel(ArticleModel()); // fetches and notifies immediately
return ListenableBuilder(listenable: vm, builder: (_, __) => Text('${vm.summary}'));
}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.
- 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
- 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
- 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
- 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
- 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 sourcehttps://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 sourcehttps://docs.flutter.dev/learn/pathway/tutorial/listenable-builder
- Last checked
- Oct 21, 2026
- Source last updated
- 3 weeks ago
- Last synchronized
- 3 weeks ago
- InheritedWidget class API reference Flutter 3.47.7https://api.flutter.dev/flutter/widgets/InheritedWidget-class.html
- Last checked
- Oct 21, 2026
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.