Flutter State Management: A Complete Guide
Flutter State Management: A Complete Guide
State management is one of the most crucial aspects of Flutter development. Choosing the right state management solution can make the difference between a maintainable, scalable app and a nightmare to maintain codebase.
1. Provider - The Foundation
Provider is the recommended state management solution by the Flutter team. It's built on top of InheritedWidget and offers a simple, flexible approach.
Basic Provider Usage
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
}
// In your main.dart
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
)
// In your widget
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text('Count: ${counter.count}');
},
)
Pros and Cons
Pros:
- Simple to learn and implement
- Great performance
- Official recommendation
- Good for small to medium apps
Cons:
- Can become complex with multiple providers
- Requires careful provider tree management
- Runtime errors for missing providers
2. Riverpod - Provider Evolved
Riverpod is the evolution of Provider, addressing many of its limitations while providing additional features.
Basic Riverpod Usage
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
// In your widget
class CounterWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: Text('Increment'),
),
],
);
}
}
Advanced Riverpod Features
// Auto-dispose providers
final userProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) {
return userRepository.getUser(userId);
});
// Provider dependencies
final filteredTodosProvider = Provider<List<Todo>>((ref) {
final todos = ref.watch(todosProvider);
final filter = ref.watch(filterProvider);
return todos.where((todo) => filter.apply(todo)).toList();
});
3. Bloc - Predictable State Management
Bloc (Business Logic Components) provides a predictable way to manage state using events and states.
Basic Bloc Implementation
// Events
abstract class CounterEvent {}
class CounterIncremented extends CounterEvent {}
class CounterDecremented extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncremented>((event, emit) => emit(state + 1));
on<CounterDecremented>((event, emit) => emit(state - 1));
}
}
// In your widget
BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Text('Count: $count');
},
)
Complex State with Bloc
// States
abstract class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
UserError(this.message);
}
// Events
abstract class UserEvent {}
class UserRequested extends UserEvent {
final String userId;
UserRequested(this.userId);
}
// Bloc
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository userRepository;
UserBloc(this.userRepository) : super(UserInitial()) {
on<UserRequested>(_onUserRequested);
}
Future<void> _onUserRequested(
UserRequested event,
Emitter<UserState> emit,
) async {
emit(UserLoading());
try {
final user = await userRepository.getUser(event.userId);
emit(UserLoaded(user));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
4. GetX - All-in-One Solution
GetX is a powerful, lightweight solution that combines state management, route management, and dependency injection.
GetX State Management
class CounterController extends GetxController {
final _count = 0.obs;
int get count => _count.value;
void increment() => _count.value++;
}
// In your widget
class CounterView extends StatelessWidget {
final CounterController controller = Get.put(CounterController());
@override
Widget build(BuildContext context) {
return Obx(() => Text('Count: ${controller.count}'));
}
}
Choosing the Right Solution
When to Use Provider/Riverpod
- Small to medium applications
- Simple state requirements
- Team prefers functional approach
- Need official Flutter team support
When to Use Bloc
- Large, complex applications
- Need predictable state changes
- Team familiar with reactive programming
- Require extensive testing
- Need clear separation of business logic
When to Use GetX
- Rapid prototyping
- Small teams
- Need all-in-one solution
- Want minimal boilerplate
Performance Considerations
Widget Rebuilds
// Bad - Entire widget rebuilds
Consumer<UserModel>(
builder: (context, user, child) {
return Column(
children: [
Text(user.name),
ExpensiveWidget(), // Rebuilds unnecessarily
],
);
},
)
// Good - Only necessary parts rebuild
Column(
children: [
Consumer<UserModel>(
builder: (context, user, child) => Text(user.name),
),
ExpensiveWidget(), // Doesn't rebuild
],
)
Testing State Management
Testing Provider
testWidgets('Counter increments', (tester) async {
await tester.pumpWidget(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: MyApp(),
),
);
expect(find.text('0'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);
});
Testing Bloc
group('CounterBloc', () {
late CounterBloc counterBloc;
setUp(() {
counterBloc = CounterBloc();
});
tearDown(() {
counterBloc.close();
});
test('initial state is 0', () {
expect(counterBloc.state, 0);
});
blocTest<CounterBloc, int>(
'emits [1] when CounterIncremented is added',
build: () => counterBloc,
act: (bloc) => bloc.add(CounterIncremented()),
expect: () => [1],
);
});
Conclusion
The choice of state management solution depends on your project requirements, team expertise, and long-term maintainability goals. Start simple with Provider or Riverpod for most projects, consider Bloc for complex business logic, and use GetX for rapid development needs.
Remember: the best state management solution is the one your team can understand, maintain, and scale effectively.