
Choosing the right state management package is one of the most important decisions in any Flutter project. In 2025, there’s no shortage of options—but some clearly stand out for their balance of power, performance, and developer experience. The wrong choice can lead to spaghetti code, difficult testing, and maintenance nightmares. The right choice enables clean architecture, easy testing, and scalable growth.
Here are the top 5 Flutter state management packages to consider in 2025, with comprehensive code examples showing how each one works in practice.
1. Riverpod – The Community Favorite
Riverpod continues to dominate in 2025—and for good reason. Created by the same developer behind Provider, Riverpod is a complete rethinking of state management in Flutter. It removes widget-tree dependency, supports code modularization, and scales incredibly well. With Riverpod 2.0’s code generation features, it’s become even more powerful.
Why it’s on top:
- Stateless and globally accessible providers
- Excellent testability with ProviderContainer
- Async support via AsyncNotifier and FutureProvider
- Code generation for reduced boilerplate
- Type-safe and compile-time verified
Here’s a complete Riverpod example with code generation:
// pubspec.yaml dependencies:
// riverpod_annotation: ^2.3.0
// riverpod_generator: ^2.3.0 (dev)
// build_runner: ^2.4.0 (dev)
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'products_provider.g.dart';
// Domain model
class Product {
final String id;
final String name;
final double price;
final int quantity;
const Product({
required this.id,
required this.name,
required this.price,
required this.quantity,
});
Product copyWith({int? quantity}) {
return Product(
id: id,
name: name,
price: price,
quantity: quantity ?? this.quantity,
);
}
}
// API service provider
@riverpod
ProductRepository productRepository(ProductRepositoryRef ref) {
return ProductRepository();
}
class ProductRepository {
Future> fetchProducts() async {
await Future.delayed(const Duration(seconds: 1));
return [
const Product(id: '1', name: 'Laptop', price: 999.99, quantity: 10),
const Product(id: '2', name: 'Phone', price: 699.99, quantity: 25),
const Product(id: '3', name: 'Tablet', price: 449.99, quantity: 15),
];
}
}
// Async provider for fetching products
@riverpod
Future> products(ProductsRef ref) async {
final repository = ref.watch(productRepositoryProvider);
return repository.fetchProducts();
}
// State notifier for cart management
@riverpod
class CartNotifier extends _$CartNotifier {
@override
List build() => [];
void addToCart(Product product) {
final existingIndex = state.indexWhere((p) => p.id == product.id);
if (existingIndex >= 0) {
state = [
for (int i = 0; i < state.length; i++)
if (i == existingIndex)
state[i].copyWith(quantity: state[i].quantity + 1)
else
state[i],
];
} else {
state = [...state, product.copyWith(quantity: 1)];
}
}
void removeFromCart(String productId) {
state = state.where((p) => p.id != productId).toList();
}
void clearCart() {
state = [];
}
}
// Computed provider for cart total
@riverpod
double cartTotal(CartTotalRef ref) {
final cart = ref.watch(cartNotifierProvider);
return cart.fold(0.0, (sum, product) => sum + (product.price * product.quantity));
}
// UI Widget
class ProductsScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final productsAsync = ref.watch(productsProvider);
final cartTotal = ref.watch(cartTotalProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
Chip(
label: Text('\$${cartTotal.toStringAsFixed(2)}'),
),
],
),
body: productsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
trailing: IconButton(
icon: const Icon(Icons.add_shopping_cart),
onPressed: () {
ref.read(cartNotifierProvider.notifier).addToCart(product);
},
),
);
},
),
),
);
}
}
2. BLoC (Business Logic Component) – Enterprise-Ready Power
BLoC remains a staple in serious Flutter apps, especially those adopting Clean Architecture. Its event-driven structure and state immutability make it a favorite among enterprise devs and large teams. The separation between events, states, and business logic creates highly testable and maintainable code.
Why it’s still a top choice:
- Predictable state management with clear event-state flow
- Strong architecture principles enforced by design
- flutter_bloc and bloc_test make testing straightforward
- Great DevTools integration for debugging
// pubspec.yaml: flutter_bloc: ^8.1.0, equatable: ^2.0.0
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
// Events
sealed class AuthEvent extends Equatable {
const AuthEvent();
@override
List
3. Provider – Simple and Beginner-Friendly
Provider may no longer be the most cutting-edge solution, but it’s still widely used—especially for small to medium projects or beginners learning Flutter. Its simplicity makes it easy to grasp, and it’s the foundation that Riverpod builds upon.
Why it’s still relevant:
- Easy to learn and integrate
- Great for local and shared state
- Minimal setup and boilerplate
- Flutter team recommended
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// Simple counter with ChangeNotifier
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
if (_count > 0) {
_count--;
notifyListeners();
}
}
}
// More complex example: Todo list
class Todo {
final String id;
final String title;
final bool completed;
Todo({required this.id, required this.title, this.completed = false});
Todo copyWith({bool? completed}) {
return Todo(id: id, title: title, completed: completed ?? this.completed);
}
}
class TodoProvider extends ChangeNotifier {
final List _todos = [];
List get todos => List.unmodifiable(_todos);
List get completedTodos => _todos.where((t) => t.completed).toList();
List get pendingTodos => _todos.where((t) => !t.completed).toList();
void addTodo(String title) {
_todos.add(Todo(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
));
notifyListeners();
}
void toggleTodo(String id) {
final index = _todos.indexWhere((t) => t.id == id);
if (index != -1) {
_todos[index] = _todos[index].copyWith(
completed: !_todos[index].completed,
);
notifyListeners();
}
}
void removeTodo(String id) {
_todos.removeWhere((t) => t.id == id);
notifyListeners();
}
}
// Setup multiple providers
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterProvider()),
ChangeNotifierProvider(create: (_) => TodoProvider()),
// ProxyProvider for dependent providers
ProxyProvider(
update: (_, todos, __) => TodoStats(todos),
),
],
child: MaterialApp(
home: HomeScreen(),
),
);
}
}
class TodoStats {
final TodoProvider _todoProvider;
TodoStats(this._todoProvider);
int get totalCount => _todoProvider.todos.length;
int get completedCount => _todoProvider.completedTodos.length;
double get completionRate =>
totalCount > 0 ? completedCount / totalCount : 0;
}
// UI with Consumer and Selector
class TodoScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todos'),
actions: [
// Selector rebuilds only when specific value changes
Selector(
selector: (_, provider) => provider.pendingTodos.length,
builder: (context, pendingCount, child) {
return Badge(
label: Text('$pendingCount'),
child: const Icon(Icons.list),
);
},
),
],
),
body: Consumer(
builder: (context, todoProvider, child) {
final todos = todoProvider.todos;
if (todos.isEmpty) {
return const Center(child: Text('No todos yet'));
}
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return CheckboxListTile(
title: Text(
todo.title,
style: TextStyle(
decoration: todo.completed
? TextDecoration.lineThrough
: null,
),
),
value: todo.completed,
onChanged: (_) => todoProvider.toggleTodo(todo.id),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context),
child: const Icon(Icons.add),
),
);
}
void _showAddDialog(BuildContext context) {
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Add Todo'),
content: TextField(
controller: controller,
autofocus: true,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
if (controller.text.isNotEmpty) {
context.read().addTodo(controller.text);
Navigator.pop(context);
}
},
child: const Text('Add'),
),
],
),
);
}
}
4. Signals – The New Contender
Signals have emerged as a powerful alternative in 2025, bringing fine-grained reactivity to Flutter. Inspired by Solid.js and Angular signals, this approach offers automatic dependency tracking and minimal rebuilds without the complexity of streams.
Why Signals are gaining traction:
- Fine-grained reactivity with automatic tracking
- Simple API with minimal boilerplate
- Efficient updates – only affected widgets rebuild
- Works well with existing Flutter patterns
// Using signals package
import 'package:signals/signals_flutter.dart';
// Define signals
final counter = signal(0);
final doubleCounter = computed(() => counter.value * 2);
// Signal-based state class
class UserState {
final name = signal('');
final email = signal('');
final isLoggedIn = signal(false);
// Computed values
late final displayName = computed(() {
return isLoggedIn.value ? name.value : 'Guest';
});
// Effects for side effects
late final dispose = effect(() {
if (isLoggedIn.value) {
print('User ${name.value} logged in');
}
});
void login(String userName, String userEmail) {
// Batch updates
batch(() {
name.value = userName;
email.value = userEmail;
isLoggedIn.value = true;
});
}
void logout() {
batch(() {
name.value = '';
email.value = '';
isLoggedIn.value = false;
});
}
}
final userState = UserState();
// UI with Watch widget
class UserProfile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Watch((context) => Text(userState.displayName.value)),
),
body: Column(
children: [
// Only rebuilds when isLoggedIn changes
Watch((context) {
if (!userState.isLoggedIn.value) {
return const Center(child: Text('Please log in'));
}
return Column(
children: [
Text('Welcome, ${userState.name.value}!'),
Text('Email: ${userState.email.value}'),
],
);
}),
// Counter example
Watch((context) => Text('Count: ${counter.value}')),
Watch((context) => Text('Double: ${doubleCounter.value}')),
ElevatedButton(
onPressed: () => counter.value++,
child: const Text('Increment'),
),
],
),
);
}
}
5. GetX – Lightning Fast and Opinionated
GetX continues to spark debate in the Flutter community. Loved for its performance and minimal boilerplate, but criticized for being too opinionated and tightly coupled. Still, it’s fast and effective for rapid development.
Why it makes the list:
- Extremely easy to use with reactive variables
- Fast performance and small bundle size
- All-in-one: routing, DI, and state management
- Great for MVPs and prototypes
import 'package:get/get.dart';
// Controller with reactive state
class ShoppingController extends GetxController {
// Observable variables
final products = [].obs;
final cart = [].obs;
final isLoading = false.obs;
// Computed property
double get cartTotal => cart.fold(
0.0,
(sum, item) => sum + (item.product.price * item.quantity),
);
int get cartItemCount => cart.fold(0, (sum, item) => sum + item.quantity);
@override
void onInit() {
super.onInit();
fetchProducts();
}
Future fetchProducts() async {
isLoading.value = true;
try {
final result = await ProductApi.fetchAll();
products.assignAll(result);
} finally {
isLoading.value = false;
}
}
void addToCart(Product product) {
final existingIndex = cart.indexWhere(
(item) => item.product.id == product.id,
);
if (existingIndex >= 0) {
cart[existingIndex] = cart[existingIndex].copyWith(
quantity: cart[existingIndex].quantity + 1,
);
} else {
cart.add(CartItem(product: product, quantity: 1));
}
}
void removeFromCart(String productId) {
cart.removeWhere((item) => item.product.id == productId);
}
void updateQuantity(String productId, int quantity) {
if (quantity <= 0) {
removeFromCart(productId);
return;
}
final index = cart.indexWhere((item) => item.product.id == productId);
if (index >= 0) {
cart[index] = cart[index].copyWith(quantity: quantity);
}
}
}
// Dependency injection
class ShoppingBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut(() => ShoppingController());
}
}
// UI with Obx
class ShoppingScreen extends GetView {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Shop'),
actions: [
// Reactive badge
Obx(() => Badge(
label: Text('${controller.cartItemCount}'),
child: IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () => Get.toNamed('/cart'),
),
)),
],
),
body: Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: controller.products.length,
itemBuilder: (context, index) {
final product = controller.products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
trailing: IconButton(
icon: const Icon(Icons.add),
onPressed: () => controller.addToCart(product),
),
);
},
);
}),
);
}
}
// GetX routing
void main() {
runApp(GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(
name: '/',
page: () => ShoppingScreen(),
binding: ShoppingBinding(),
),
GetPage(
name: '/cart',
page: () => CartScreen(),
),
],
));
}
Comparison Table
| Feature | Riverpod | BLoC | Provider | Signals | GetX |
|---|---|---|---|---|---|
| Learning Curve | Medium | High | Low | Low | Low |
| Boilerplate | Low | High | Low | Minimal | Minimal |
| Testability | Excellent | Excellent | Good | Good | Moderate |
| Scalability | Excellent | Excellent | Moderate | Good | Moderate |
| Performance | Excellent | Good | Good | Excellent | Excellent |
| Code Generation | Yes | Optional | No | No | No |
Common Mistakes to Avoid
Mixing Multiple State Management Solutions
Pick one approach and stick with it. Mixing BLoC with GetX or Provider with Riverpod creates confusion and maintenance nightmares.
Putting Business Logic in Widgets
Regardless of which solution you choose, keep business logic out of your widgets. Use controllers, blocs, or notifiers for logic.
Over-Engineering Simple Apps
If you’re building a simple app, you don’t need BLoC’s full event-state architecture. setState or Provider might be enough.
Ignoring Testing
Choose a solution that makes testing easy. BLoC and Riverpod excel here with dedicated testing utilities.
Final Thoughts
There’s no one-size-fits-all state management solution in Flutter. Each package shines in different use cases:
- Want flexibility and testability? Go with Riverpod.
- Need structured and predictable flows? Choose BLoC.
- Prefer simplicity? Provider or GetX might be enough.
- Want fine-grained reactivity? Try Signals.
Your app’s complexity, team size, and future scalability should guide your choice. For a deeper dive into why I personally prefer Riverpod, check out our article on why I use Riverpod in 2025. For beginners, start with our comparison of setState vs Provider. And for the official documentation on these packages, visit pub.dev.