DartFlutter

Top 5 Flutter State Management Packages in 2025

20250404 1412 Flutter State Management 2025 Remix 01jr0be7r5fhhrvqaakrb09ajx 1024x683

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 get props => [];
}

final class LoginRequested extends AuthEvent {
  final String email;
  final String password;
  
  const LoginRequested({required this.email, required this.password});
  
  @override
  List get props => [email, password];
}

final class LogoutRequested extends AuthEvent {
  const LogoutRequested();
}

final class AuthCheckRequested extends AuthEvent {
  const AuthCheckRequested();
}

// States
sealed class AuthState extends Equatable {
  const AuthState();
  
  @override
  List get props => [];
}

final class AuthInitial extends AuthState {
  const AuthInitial();
}

final class AuthLoading extends AuthState {
  const AuthLoading();
}

final class AuthAuthenticated extends AuthState {
  final User user;
  
  const AuthAuthenticated(this.user);
  
  @override
  List get props => [user];
}

final class AuthUnauthenticated extends AuthState {
  const AuthUnauthenticated();
}

final class AuthError extends AuthState {
  final String message;
  
  const AuthError(this.message);
  
  @override
  List get props => [message];
}

// BLoC
class AuthBloc extends Bloc {
  final AuthRepository _authRepository;
  
  AuthBloc({required AuthRepository authRepository})
      : _authRepository = authRepository,
        super(const AuthInitial()) {
    on(_onLoginRequested);
    on(_onLogoutRequested);
    on(_onAuthCheckRequested);
  }
  
  Future _onLoginRequested(
    LoginRequested event,
    Emitter emit,
  ) async {
    emit(const AuthLoading());
    
    try {
      final user = await _authRepository.login(
        email: event.email,
        password: event.password,
      );
      emit(AuthAuthenticated(user));
    } catch (e) {
      emit(AuthError(e.toString()));
    }
  }
  
  Future _onLogoutRequested(
    LogoutRequested event,
    Emitter emit,
  ) async {
    await _authRepository.logout();
    emit(const AuthUnauthenticated());
  }
  
  Future _onAuthCheckRequested(
    AuthCheckRequested event,
    Emitter emit,
  ) async {
    final user = await _authRepository.getCurrentUser();
    if (user != null) {
      emit(AuthAuthenticated(user));
    } else {
      emit(const AuthUnauthenticated());
    }
  }
}

// UI with BlocBuilder and BlocListener
class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => AuthBloc(
        authRepository: context.read(),
      ),
      child: BlocListener(
        listener: (context, state) {
          if (state is AuthError) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          } else if (state is AuthAuthenticated) {
            Navigator.of(context).pushReplacementNamed('/home');
          }
        },
        child: BlocBuilder(
          builder: (context, state) {
            return Scaffold(
              body: Padding(
                padding: const EdgeInsets.all(24),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    // Form fields here
                    ElevatedButton(
                      onPressed: state is AuthLoading
                          ? null
                          : () {
                              context.read().add(
                                const LoginRequested(
                                  email: 'user@example.com',
                                  password: 'password',
                                ),
                              );
                            },
                      child: state is AuthLoading
                          ? const CircularProgressIndicator()
                          : const Text('Login'),
                    ),
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }
}

// Testing BLoC
void main() {
  blocTest(
    'emits [AuthLoading, AuthAuthenticated] when login succeeds',
    build: () => AuthBloc(authRepository: MockAuthRepository()),
    act: (bloc) => bloc.add(
      const LoginRequested(email: 'test@test.com', password: 'password'),
    ),
    expect: () => [
      const AuthLoading(),
      isA(),
    ],
  );
}

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.

Leave a Comment