I have been using Flutter since 2020.
I tried many state management solutions: Provider, Riverpod, BLoC, MobX, etc.
I finally returned to the traditional Provider.
Recently the Flutter community seems to talk a lot about state management and the verbosity of Flutter.
Let me share my opinion about it.
For me, I prefer to use Provider and Hooks.
Why?
Less frequent breaking changes and more stability.
You don’t need to worry about breaking changes — just focus on building your app.
With flutter_hooks, you can write less code and make it more readable.
Here’s how I use Provider and Hooks.
For state, I consider two types:
- App state: state that needs to be shared across the app.
- Page state: state that is used only within a single page.
All state is represented by a simple class extending ChangeNotifier.
class HomeState with ChangeNotifier {
String? errorMessage; // You can create getter/setter if needed. For sample, I keep it simple.
String? query;
List<String> items = [];
void search(String text) {
query = text;
notifyListeners();
// Simulate a search operation
items = List.generate(5, (index) => '$text result $index');
notifyListeners();
}
}
I also have some custom extensions to make usage easier.
T get<T>() {
final context = useContext();
return useMemoized(() => context.read<T>());
}
extension ListenableX<Notifier extends Listenable> on Notifier {
T watch<T>(
T Function(Notifier $) selector,
) {
final notifier = this;
final result = useState(selector(notifier));
listen(
selector,
(previous, next) {
Future.microtask(() => result.value = next);
},
);
return result.value;
}
void listen<T>(
T Function(Notifier $) selector,
void Function(T? previous, T next) listener,
) {
final notifier = this;
final previousValue = useRef<T?>(null);
final isFirst = useRef(true);
useOnListenableChange(
notifier,
() {
final previous = previousValue.value;
final next = selector(notifier);
if (isFirst.value || previous != next) {
isFirst.value = false;
previousValue.value = next;
listener(previous, next);
}
},
);
}
}
Key points:
- get — retrieve the ViewModel from the widget’s build method.
- watch — observe a value from the ViewModel and rebuild when it changes.
- listen — listen to a value from the ViewModel and trigger side effects when it changes.
Watch and listen accept a selector so you only observe the value(s) you care about.
Now, in a page you can use it like this.
class HomePage extends HookWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => HomeState()),
],
child: HomeView(),
);
}
}
class HomeView extends HookWidget {
@override
Widget build(BuildContext context) {
// Get the HomeState instance
final homeState = get<HomeState>();
// Watch the values you care about.
// You should put the watch inside the widget that actually needs it
final errorMessage = homeState.watch((s) => s.errorMessage);
final items = homeState.watch((s) => s.items);
// Listen for side effects
homeState.listen<String?>(
(s) => s.errorMessage,
(previous, next) {
if (next != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(next)),
);
}
},
);
return View();
}
}