When you first start building a Flutter app, managing state feels simple. But as your app grows, passing data between screens can get messy. You end up with code that's hard to read and even harder to debug. This is where state management solutions come in.
Think of it like organizing a shared kitchen. At first, with just one person, you can leave things anywhere. But when more people start using it, you need a system—labels, shelves, designated areas—to keep things from becoming chaotic.
In this post, we'll look at three popular systems for Flutter: Provider, Riverpod, and BLoC. We'll use simple examples to see how they work and help you decide which one is right for your project.
What is State Anyway?
In simple terms, state is just data that can change over time. It’s the value of a counter, whether a user is logged in, or the list of items in a shopping cart. When this data changes, your UI needs to update to reflect it.
Without a proper system, you might find yourself passing data through many layers of widgets that don't even need it, a problem often called "prop drilling." A good state management solution gives you a clean way to access and modify your data from anywhere in your app.
Provider The Simple Starter
Provider is often the first state management tool Flutter developers learn. It’s built on top of Flutter's InheritedWidget
but makes it much easier to use.
Imagine Provider as a central notice board in an office. Instead of asking every colleague for a piece of information, you just look at the board. Any widget that needs access to the state can get it directly, as long as it's a descendant of where the state is "provided."
How Provider Works
You create a model class that holds your state, usually with ChangeNotifier
. Then you use a ChangeNotifierProvider
to make that model available to the widgets below it.
Here’s a simple counter example.
1. Create the Counter Model
import 'package:flutter/material.dart'; class CounterModel extends ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); // This tells widgets to rebuild } }
2. Provide the Model
Wrap a widget high up in your tree (like MaterialApp
) with ChangeNotifierProvider
.
ChangeNotifierProvider( create: (context) => CounterModel(), child: MyApp(), ),
3. Use the State
Now, any widget within MyApp
can access the CounterModel
.
// To display the count Text('Count: ${context.watch<CounterModel>().count}'), // To call the increment function ElevatedButton( onPressed: () => context.read<CounterModel>().increment(), child: Text('Increment'), )
When to use Provider: It’s great for small to medium apps or when you're just starting. It’s simple to understand and gets the job done.
Riverpod The Modern Upgrade
Riverpod was created by the same author as Provider to fix some of its common issues. The biggest difference is that Riverpod is compile-safe and independent of the widget tree.
This means you get errors at compile time instead of runtime, and you don't need a BuildContext
to access your state. It’s like getting information delivered directly to you via a dedicated messenger instead of having to walk to the central notice board.
A Simple Riverpod Example
With Riverpod, you define global "providers" that hold a piece of state.
1. Define a Provider
We'll use a StateNotifierProvider
, which is perfect for managing state that can change.
// 1. Create a Notifier class class CounterNotifier extends StateNotifier<int> { CounterNotifier() : super(0); // Initial state is 0 void increment() => state++; } // 2. Create the provider (a global variable) final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) { return CounterNotifier(); });
2. Set Up Your App
Wrap your entire application in a ProviderScope
.
void main() { runApp(ProviderScope(child: MyApp())); }
3. Use the State in a Widget
To use the provider, you change your StatelessWidget
to a ConsumerWidget
.
class CounterText extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // Rebuilds the widget when the counter changes final count = ref.watch(counterProvider); return Text('Count: $count'); } } // In another widget, for the button class IncrementButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return ElevatedButton( onPressed: () { // Get the notifier and call the increment method ref.read(counterProvider.notifier).increment(); }, child: Text('Increment'), ); } }
When to use Riverpod: It’s an excellent choice for new projects of any size. It’s more scalable and less error-prone than Provider.
BLoC The Predictable Powerhouse
BLoC (Business Logic Component) is a design pattern that separates your app's business logic from the UI. It's more structured and a bit more complex than Provider or Riverpod.
Think of BLoC as a strict assembly line. A request (an Event) goes in one end. It's processed according to a set of rules. A result (a State) comes out the other end. This flow is unidirectional and very predictable, which makes it great for testing and managing complex logic.
The BLoC Flow
- UI sends an Event: A button press, text input, etc.
- BLoC receives the Event: It processes the event and its logic.
- BLoC emits a new State: The BLoC updates its state.
- UI rebuilds: The UI listens for state changes and rebuilds itself accordingly.
BLoC Counter Example
Using the flutter_bloc
package simplifies this pattern.
1. Define Events and States
// Events abstract class CounterEvent {} class IncrementEvent extends CounterEvent {} // States (just an int for this simple case) // For more complex features, this would be a class.
2. Create the BLoC
import 'package:bloc/bloc.dart'; class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { // Initial state is 0 on<IncrementEvent>((event, emit) { emit(state + 1); }); } }
3. Provide and Use the BLoC
// Provide the BLoC at the top of the tree BlocProvider( create: (context) => CounterBloc(), child: CounterPage(), ), // Use BlocBuilder to listen to state changes and rebuild the UI BlocBuilder<CounterBloc, int>( builder: (context, count) { return Text('Count: $count'); }, ), // Add an event to the BLoC ElevatedButton( onPressed: () => context.read<CounterBloc>().add(IncrementEvent()), child: Text('Increment'), )
When to use BLoC: BLoC shines in large, complex applications where state logic is intricate and testability is a top priority. It has a steeper learning curve but provides a very robust structure.
So, Which One Should You Choose?
There is no single "best" solution. It depends on your project.
- Provider: Use it for smaller apps or if you're a beginner. It's simple and effective.
- Riverpod: A great default for most new projects. It’s powerful, flexible, and fixes the main drawbacks of Provider.
- BLoC: Choose it for large-scale applications where you need a strict architecture and predictable state changes.
The most important thing is to pick a solution and use it consistently. Start simple, and don’t be afraid to try a different approach as your app and your skills grow.
Originally published at https://muhabbat.dev/post/flutter-state-management-guide-provider-riverpod-bloc on September 25, 2025.
Top comments (0)