ChangeNotifier State Management in Flutter: The Lightweight Approach
Manage simple app state with ChangeNotifier, notifyListeners, and provider-style wiring without a full state management library.
Published on • November 4, 2026
AI Assistant

Not every app needs Riverpod or Bloc. For local, simple state — a form’s dirty flag, a theme toggle, a shopping cart — ChangeNotifier from package:flutter/foundation.dart is enough. This follows the cited architecture guidance: a plain class extends ChangeNotifier, mutates state, and calls notifyListeners() to rebuild listeners.
The notifier
import 'package:flutter/foundation.dart';
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}
Expose private fields behind getters so external code can read but not accidentally replace the whole value.
Listening
Use AnimatedBuilder or ListenableBuilder to rebuild when the notifier fires:
ListenableBuilder(
listenable: counter,
builder: (context, _) => Text('${counter.count}'),
)
Scoping with InheritedWidget
For app-wide state, wrap the notifier in an InheritedNotifier once near the root:
class CounterScope extends InheritedNotifier<Counter> {
const CounterScope({super.key, required Counter counter, required super.child})
: super(notifier: counter);
static Counter of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<CounterScope>()!.notifier!;
}
Then read it anywhere below: CounterScope.of(context).increment().
Disposing
If your notifier owns resources (controllers, subscriptions), override dispose and cancel them — and make sure you don’t call notifyListeners() after disposal, which throws an assertion in debug mode.
Takeaway: ChangeNotifier plus notifyListeners() covers a surprising amount of real-world state — keep the state private behind getters, listen with ListenableBuilder, scope it with InheritedNotifier, and dispose resources properly.