TL;DR
Provider is simple and a good way to learn the concepts. Riverpod is the more modern option and the one I recommend for new projects. Bloc is more verbose but gives large teams a strict structure. For a new app today, start with Riverpod.
State management is where most Flutter beginners get stuck. Applying one approach consistently matters more than which one you pick, but choosing one that fits the size of your project saves a lot of refactoring later.
What is "state" in Flutter?
State is any data that can change and affects what the UI shows. Whether the user is logged in, how many items are in the cart, whether a button is loading: all of that is state. Managing it means deciding where each piece of data lives and how changes reach the widgets that depend on it.
Provider: the simple option
Provider is a thin layer over Flutter's InheritedWidget, written by Remi Rousselet. The official Flutter docs use it to introduce app state management, and it works well for small and medium apps. It's simple and well documented.
// 1. Define your state class
class CounterState extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Tells Flutter to rebuild
}
}
// 2. Wrap your app with the provider
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterState(),
child: MyApp(),
),
);
}
// 3. Read and update from any widget
Consumer<CounterState>(
builder: (context, state, _) => Text('${state.count}'),
)
// Update:
context.read<CounterState>().increment();
Riverpod: the modern option
Riverpod comes from the same author and fixes most of Provider's limitations. Providers are declared outside the widget tree, the 'provider not found' errors Provider throws at runtime are caught at compile time, and tests are easier because any provider can be overridden. It's what I recommend for new projects.
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Define a Notifier
class CounterNotifier extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
// 2. Create a provider
final counterProvider = NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);
// 3. Use in widget (ConsumerWidget instead of StatelessWidget)
// The app root must be wrapped once: runApp(ProviderScope(child: MyApp()));
class MyWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(children: [
Text('$count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('Increment'),
),
]);
}
}
Bloc: for large codebases
Bloc (Business Logic Component) separates UI from logic with explicit events and states. You write more boilerplate, but every state change is traceable and easy to test. It's common in large codebases such as banking and e-commerce apps, where many developers work on the same project.
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
// States (just an int here)
// In real apps, states are classes with data
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Increment>((event, emit) => emit(state + 1));
on<Decrement>((event, emit) => emit(state - 1));
}
}
// In widget
BlocBuilder<CounterBloc, int>(
builder: (context, count) => Text('$count'),
)
// Dispatch event:
context.read<CounterBloc>().add(Increment());
- Provider
- Riverpod
- Bloc
- Small apps, learning, quick projects
- New projects, medium to large apps, teams
- Large apps and teams that want a strict structure
My recommendation: learn the concepts with Provider, then use Riverpod for any serious project. Learn Bloc when a team or a client requires it.
Bottom line
There's no single right answer; it depends on the size of the project and of the team. For a new Flutter app, I'd pick Riverpod: it's light enough for a small app and holds up as the codebase grows.