Introduction
As a Flutter application grows, state management becomes an architectural decision rather than a matter of choosing between setState and another widget. Applications may need to coordinate network requests, cached data, authentication, form state, and complex user workflows.
Provider, Riverpod, and BLoC are popular options, but they offer different abstractions and trade-offs. The best choice depends on the team’s needs, the complexity of the application, and how much structure is helpful.
1. Start with local state
Not every state change requires a state management package. A simple toggle or a temporary field value may belong directly in a widget.
class FavoriteButton extends StatefulWidget {
const FavoriteButton({super.key});
@override
State<FavoriteButton> createState() =>
_FavoriteButtonState();
}
class _FavoriteButtonState extends State<FavoriteButton> {
bool isFavorite = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
),
onPressed: () {
setState(() {
isFavorite = !isFavorite;
});
},
);
}
}
This is simple and easy to maintain because the state has one clear owner. Introducing a global state system for every local interaction often adds complexity without delivering meaningful benefits.
2. Provider: dependency injection and shared state
Provider builds on Flutter’s widget tree to expose objects to descendants. It is often a practical choice for applications that need shared state and straightforward dependency injection.
A ChangeNotifier can expose state and notify listeners when it changes:
import 'package:flutter/foundation.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
The model can be provided above the widgets that need it, and consumers can listen for changes. Provider is useful when the team wants a relatively simple approach with explicit state objects.
One limitation is that a poorly designed notifier can grow into a large class responsible for unrelated concerns. Keep state ownership focused and split responsibilities when needed.
3. Riverpod: explicit providers and testability
Riverpod offers a provider system designed to make dependencies and state accessible without relying on the same BuildContext lookup pattern as Provider.
A basic counter can use a Notifier:
import 'package:flutter_riverpod/flutter_riverpod.dart';
final counterProvider =
NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() {
state++;
}
}
A consumer widget can watch the provider’s value and read its notifier to trigger updates. Riverpod also offers useful patterns for asynchronous state and dependency overrides in tests.
Its provider and notifier APIs differ across major versions, so follow the documentation for the version selected by the project.
4. BLoC: explicit events and state transitions
BLoC separates incoming events from emitted states. This can be valuable when a feature has complex transitions, such as a checkout workflow or a multi-step onboarding process.
A conceptual event/state flow might be:
User action
↓
Event dispatched
↓
BLoC processes event
↓
New state emitted
↓
UI rebuilds
The explicit flow helps developers reason about how an action leads to a state change. It can also make complex behavior easier to test.
The trade-off is additional structure and boilerplate. A simple counter does not usually need an event class, state class, and BLoC implementation.
5. Compare the approaches by application needs
| Approach | Often suitable for | Main consideration |
|---|---|---|
| Local setState | Small, widget-owned state | Keep state close to its owner |
| Provider | Shared state and dependency injection | Avoid oversized notifiers |
| Riverpod | Explicit dependencies and testable state | Learn the provider model and version-specific APIs |
| BLoC | Complex workflows and explicit transitions | More structure and boilerplate |
These are guidelines, not strict rules. Teams should also consider developer experience, existing code, test strategy, and the availability of maintainers who understand the chosen library.
6. Keep business logic separate from widgets
Regardless of the state management package, avoid placing all business logic inside build() methods. Widgets should primarily describe the UI and translate user interactions into application actions.
Network access, business rules, and persistence can be represented through repositories and services. State management then coordinates these dependencies and exposes the resulting state to the UI.
This separation makes features easier to test and reduces the risk of state logic becoming entangled with presentation details.
Conclusion
Provider, Riverpod, and BLoC can all support maintainable Flutter applications. Start with the simplest approach that fits the feature, introduce more structure as workflows become complex, and keep state ownership and business responsibilities clear.