Flutter State Management in Practice: Provider, BLoC, and Riverpod Side by Side
If you ask a Flutter developer, "What's the hardest tech stack to choose in Flutter?" nine times out of ten the answer will be — state management. From the official early Provide, to the hugely popular Provider, to the architecturally rigorous BLoC, and now the highly regarded Riverpod, the landscape of competing approaches leaves many beginners at a loss.
Today, we'll compare the three mainstream state management solutions from a practical perspective, helping you find the architecture that best fits your current project.
1. Why do we need state management?
In Flutter, "UI = f(State)". When an application becomes complex and we need to share data across multiple widget levels, relying solely on StatefulWidget to pass callbacks (Callback hell) makes the code extremely coupled and hard to maintain. The core goal of state management tools is to separate UI rendering from business logic.
2. Comparing the three major solutions
2.1 Provider: Classic and intuitive
Provider is a lightweight dependency injection and state management tool once officially recommended. It is built on top of Flutter's native InheritedWidget.
- Pros: Gentle learning curve, suitable for small to medium projects, highly compatible with Flutter.
- Pain points: Depends on the Widget tree structure; trying to fetch data at the wrong level of the tree will throw a
ProviderNotFoundException.
Practical code:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. Define state (Model)
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Notify listeners to rebuild
}
}
// 2. Inject and use
class ProviderApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MaterialApp(
home: Scaffold(
body: Center(
// Use Consumer for local rebuilds
child: Consumer<CounterModel>(
builder: (context, model, child) => Text('${model.count}'),
),
),
floatingActionButton: Builder(
builder: (ctx) => FloatingActionButton(
// listen: false is used to only call methods without listening for state changes
onPressed: () => ctx.read<CounterModel>().increment(),
child: Icon(Icons.add),
),
),
),
),
);
}
}
2.2 BLoC: Rigorous event-driven architecture
BLoC (Business Logic Component) forces developers to think in terms of streams, clearly distinguishing between events and states.
- Pros: Extremely decoupled, very suitable for complex enterprise applications, test-friendly.
- Pain points: Too much boilerplate code (despite plugin assistance), high level of abstraction for beginners.
Concept flow: UI emits Event -> BLoC receives and processes -> BLoC yields State -> UI listens to State and rebuilds
Practical code:
import 'package:flutter_bloc/flutter_bloc.dart';
// 1. Events & States
abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
// 2. BLoC
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
}
// 3. UI
class BlocApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: Scaffold(
body: Center(
child: BlocBuilder<CounterBloc, int>(
builder: (context, count) => Text('$count'),
),
),
floatingActionButton: Builder(
builder: (ctx) => FloatingActionButton(
onPressed: () => ctx.read<CounterBloc>().add(CounterIncrementPressed()),
child: Icon(Icons.add),
),
),
),
);
}
}
2.3 Riverpod: The rebirth of Provider
Riverpod is the next-generation state management library developed by Remi, the author of Provider. It overcomes Provider's dependency on the Flutter tree.
- Pros: Compile-time safe (goodbye ProviderNotFound forever), can read state without depending on BuildContext, easy to combine different states.
- Pain points: Requires introducing new concepts (ProviderScope, WidgetRef), moderate learning cost.
Practical code:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Declare Provider (defined like a global variable, but safe)
final counterProvider = StateProvider<int>((ref) => 0);
// 2. Wrap the app with ProviderScope
void main() {
runApp(ProviderScope(child: RiverpodApp()));
}
// 3. UI inherits ConsumerWidget
class RiverpodApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
// Listen to state changes
final count = ref.watch(counterProvider);
return MaterialApp(
home: Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
// Update state
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
),
);
}
}
3. How to choose? (Decision tree)
- Very small project, only need a little cross-page value passing? -> Use the built-in
ValueNotifierdirectly or the lightweightGetX. - Medium project, pursuing speed of getting started? -> Choose
Provider. - Brand new project, hoping for a modern and safe architecture? -> Strongly recommend
Riverpod. - Large enterprise application, team collaboration, extremely high testability requirements? -> Choose
BLoC.
4. Conclusion
There is no absolutely perfect state management solution, only the choice that best fits the current team and business scenario. Mastering their core ideas — separation of concerns, unidirectional data flow — is the key to improving architectural ability. Which one are you using? Feel free to leave a comment and discuss!
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
signals is so convenient