Flutter State Management: GetX, Bloc, and Riverpod
Since I started building Flutter apps in 2020, I’ve always appreciated how easy Flutter makes UI development. But when it comes to state management, it’s a whole different story—there are tons of options, and it’s a common hot topic in the community.
In this article, I’ll walk through three popular choices — GetX, Bloc, and Riverpod—based on how I’ve used them in both personal and production projects. Each has its strengths, and I’ll include code examples to show how they work in practice.
GetX: The Quick and Dirty Path to ProductivityPermalink
GetX is often the go-to for beginners or rapid prototyping—and for good reason:
- It’s easy to set up
- Comes with minimal boilerplate
- Includes navigation, dependency injection, and state management all in one package
But that last point is where it can become a double-edged sword. GetX tends to violate the Single Responsibility Principle (SRP). By combining too many responsibilities—routing, DI, and state—it can lead to tight coupling and hidden dependencies in larger codebases. While it’s great for small apps or hackathons, it may not age well as your app grows in complexity.
🧪 Example (GetX Controller)Permalink
class CounterController extends GetxController {
var count = 0.obs;
void increment() => count++;
}
// In UI:
final controller = Get.put(CounterController());
Obx(() => Text("Count: \${controller.count}"))
Great for getting an MVP up and running fast, but I wouldn’t recommend it for production. The package has had some serious issues on GitHub lately, and the maintainer doesn’t seem very active anymore.
Riverpod: Lightweight and FlexiblePermalink
Riverpod is a modern and powerful alternative to the old Provider package. It’s declarative, compile-safe, and fits nicely with Flutter’s reactive style. You define your state as providers and use hooks or the ConsumerWidget to read and update values.
Pros:
- Simple and clean syntax
- Works great with functional programming patterns
- Encourages separation of logic from UI
But there’s a catch: global access to providers can lead to misuse. Since providers can be accessed from anywhere, it’s easy for developers to use a provider without realizing if they should. This can hurt modularity if not handled carefully.
There are workarounds like:
- Scoping with
ProviderScope - Using private providers
- Creating custom wrapper widgets to simulate widget-tree scoping
🧪 Example (Riverpod)Permalink
final counterProvider = StateProvider((ref) => 0);
// In UI:
Consumer(
builder: (context, ref, _) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: \$count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('Increment'),
)
],
);
},
)
Ideal for small to medium projects, as long as you keep things organized. One tip: take advantage of the new provider annotations with code generation—it’s optional, but it can really simplify things and make your setup much cleaner.
Bloc: Structured and ScalablePermalink
Bloc (Business Logic Component) is a well-established solution, especially popular in the enterprise and larger Flutter apps. It enforces unidirectional data flow, making it easy to reason about the app state.
While it’s more boilerplate-heavy than GetX or Riverpod, Bloc shines in larger apps where structure matters.
Key points:
- Best suited for large and complex projects
- Promotes clean architecture and testability
- Cubit, a simpler version of Bloc, is recommended for less complex use cases
So don’t jump straight into full Bloc if you don’t need it—start with Cubit and scale up only if necessary.
🧪 Example (Cubit)Permalink
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
// In UI:
BlocProvider(
create: (_) => CounterCubit(),
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('Count: \$count'),
),
)
Definitely worth the extra structure in large apps, especially when working in a team. From my experience, it’s best to start with Cubit to avoid the overhead of defining Events unless truly needed. Pairing it with the Freezed package gives you a powerful combo for clean and maintainable state control in Flutter.
ConclusionPermalink
Flutter gives you the freedom to architect your app however you like—but with great power comes great responsibility.
- GetX gets you going fast, but may cost you long-term flexibility
- Riverpod offers the sweet spot of simplicity and control—just don’t go global wild
- Bloc (or Cubit) brings the architecture you need for serious projects
Choosing the right tool depends on your project’s size, your team’s experience, and how long you plan to maintain the app. Start small, and evolve your state management strategy as your app scales.
There’s no one-size-fits-all solution in Flutter state management. If you’re prototyping or building something small, GetX might get the job done quickly. For apps that need more structure and predictability, Riverpod offers a great balance—just be disciplined about how you scope and use providers. And if you’re working on a large codebase or collaborating with a bigger team, Bloc (or Cubit) will likely save you from architectural headaches down the line.
Start with what makes the most sense for your app today, and stay flexible to evolve with your app’s needs tomorrow.
Like what you're reading?
If this article hit the spot, why not subscribe to the weekly newsletter?
Every weekend, you'll get a short and sweet summary of the latest posts and tips—free and easy, no strings
attached!
You can unsubscribe anytime.