Master Flutter state management with BLoC, Riverpod, Provider, and GetX. Learn when to use each solution and implement scalable state patterns...
Comprehensive guide to state management in Flutter, covering BLoC, Riverpod, Provider, GetX, and other modern patterns for building scalable Flutter applications.
// Event
abstract class CounterEvent {}
class IncrementCounter extends CounterEvent {}
class DecrementCounter extends CounterEvent {}
// State
class CounterState {
final int count;
const CounterState(this.count);
}
// BLoC
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(0)) {
on<IncrementCounter>(_onIncrement);
on<DecrementCounter>(_onDecrement);
}
void _onIncrement(IncrementCounter event, Emitter<CounterState> emit) {
emit(CounterState(state.count + 1));
}
void _onDecrement(DecrementCounter event, Emitter<CounterState> emit) {
emit(CounterState(state.count - 1));
}
}
// UI
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterBloc(),
child: CounterView(),
);
}
}
class CounterView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('${state.count}', style: Theme.of(context).textTheme.headlineLarge);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(IncrementCounter()),
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(DecrementCounter()),
child: const Icon(Icons.remove),
),
],
),
);
}
}
// States with sealed classes
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final List<User> users;
UserLoaded(this.users);
}
class UserError extends UserState {
final String message;
UserError(this.message);
}
// Events
sealed class UserEvent {}
class LoadUsers extends UserEvent {}
class RefreshUsers extends UserEvent {}
// BLoC with API call
class UserBloc extends Bloc<UserEvent, UserState> {
final UserRepository repository;
UserBloc({required this.repository}) : super(UserInitial()) {
on<LoadUsers>(_onLoadUsers);
on<RefreshUsers>(_onRefreshUsers);
}
Future<void> _onLoadUsers(LoadUsers event, Emitter<UserState> emit) async {
emit(UserLoading());
try {
final users = await repository.getUsers();
emit(UserLoaded(users));
} catch (e) {
emit(UserError(e.toString()));
}
}
Future<void> _onRefreshUsers(RefreshUsers event, Emitter<UserState> emit) async {
// Keep current state while refreshing
try {
final users = await repository.getUsers();
emit(UserLoaded(users));
} catch (e) {
emit(UserError(e.toString()));
}
}
}
// UI with BlocBuilder
class UserListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => UserBloc(
repository: context.read<UserRepository>(),
)..add(LoadUsers()),
child: Scaffold(
appBar: AppBar(title: const Text('Users')),
body: BlocBuilder<UserBloc, UserState>(
builder: (context, state) {
return switch (state) {
UserInitial() => const Center(child: Text('Press button to load')),
UserLoading() => const Center(child: CircularProgressIndicator()),
UserLoaded(users: var users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
UserError(message: var message) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $message'),
ElevatedButton(
onPressed: () => context.read<UserBloc>().add(LoadUsers()),
child: const Text('Retry'),
),
],
),
),
};
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<UserBloc>().add(RefreshUsers()),
child: const Icon(Icons.refresh),
),
),
);
}
}
// Provider
final counterProvider = StateProvider<int>((ref) => 0);
// UI
class CounterPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Text('$count', style: Theme.of(context).textTheme.headlineLarge),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: const Icon(Icons.add),
),
);
}
}
// Model
class User {
final String id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
}
// AsyncNotifier
class UserListNotifier extends AsyncNotifier<List<User>> {
@override
Future<List<User>> build() async {
// Load initial data
return _fetchUsers();
}
Future<List<User>> _fetchUsers() async {
final repository = ref.read(userRepositoryProvider);
return repository.getUsers();
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _fetchUsers());
}
Future<void> addUser(User user) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = ref.read(userRepositoryProvider);
await repository.addUser(user);
return _fetchUsers();
});
}
}
// Provider
final userListProvider = AsyncNotifierProvider<UserListNotifier, List<User>>(() {
return UserListNotifier();
});
// UI
class UserListPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final usersAsync = ref.watch(userListProvider);
return Scaffold(
appBar: AppBar(title: const Text('Users')),
body: usersAsync.when(
data: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $error'),
ElevatedButton(
onPressed: () => ref.refresh(userListProvider),
child: const Text('Retry'),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(userListProvider.notifier).refresh(),
child: const Icon(Icons.refresh),
),
);
}
}
// Provider with parameter (family)
final userProvider = FutureProvider.autoDispose.family<User, String>((ref, userId) async {
final repository = ref.watch(userRepositoryProvider);
return repository.getUser(userId);
});
// UI
class UserDetailPage extends ConsumerWidget {
final String userId;
const UserDetailPage({required this.userId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return Scaffold(
appBar: AppBar(title: const Text('User Details')),
body: userAsync.when(
data: (user) => Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${user.name}'),
Text('Email: ${user.email}'),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
);
}
}
// Model
class Counter extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
}
// Main app setup
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => Counter(),
child: MyApp(),
),
);
}
// UI
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Consumer<Counter>(
builder: (context, counter, child) {
return Text('${counter.count}', style: Theme.of(context).textTheme.headlineLarge);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<Counter>().increment(),
child: const Icon(Icons.add),
),
);
}
}
// Controller
class CounterController extends GetxController {
final count = 0.obs;
void increment() => count.value++;
void decrement() => count.value--;
}
// UI
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final controller = Get.put(CounterController());
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Center(
child: Obx(() => Text(
'${controller.count}',
style: Theme.of(context).textTheme.headlineLarge,
)),
),
floatingActionButton: FloatingActionButton(
onPressed: controller.increment,
child: const Icon(Icons.add),
),
);
}
}
void main() {
group('CounterBloc', () {
late CounterBloc bloc;
setUp(() {
bloc = CounterBloc();
});
tearDown(() {
bloc.close();
});
test('initial state is 0', () {
expect(bloc.state.count, 0);
});
blocTest<CounterBloc, CounterState>(
'emits [1] when IncrementCounter is added',
build: () => CounterBloc(),
act: (bloc) => bloc.add(IncrementCounter()),
expect: () => [const CounterState(1)],
);
blocTest<CounterBloc, CounterState>(
'emits [1, 2] when IncrementCounter is added twice',
build: () => CounterBloc(),
act: (bloc) {
bloc.add(IncrementCounter());
bloc.add(IncrementCounter());
},
expect: () => [
const CounterState(1),
const CounterState(2),
],
);
});
}
void main() {
test('counterProvider initial value is 0', () {
final container = ProviderContainer();
addTearDown(container.dispose);
expect(container.read(counterProvider), 0);
});
test('counterProvider can be incremented', () {
final container = ProviderContainer();
addTearDown(container.dispose);
container.read(counterProvider.notifier).state++;
expect(container.read(counterProvider), 1);
});
test('userListProvider loads users', () async {
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(MockUserRepository()),
],
);
addTearDown(container.dispose);
// Wait for the provider to load
await container.read(userListProvider.future);
final users = container.read(userListProvider).value;
expect(users, isNotEmpty);
});
}