Flutter Lesson 42 of 83 4 min read
Bloc and Cubit in Flutter
Learn the Bloc pattern in Flutter with flutter_bloc: Cubit, Bloc with events, BlocProvider, BlocBuilder, BlocListener and state classes.
On this page
Bloc (Business Logic Component) is a state management pattern built on a strict idea: the UI sends events in, and gets states out. Nothing else passes between them. That strictness makes large apps predictable and easy to test, which is why Bloc is popular with bigger teams.
flutter pub add flutter_bloc
The package offers two tools. Start with the simpler one.
Cubit #
A Cubit holds a state and has methods that emit new states.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0); // the initial state
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
void main() {
runApp(
MaterialApp(
home: BlocProvider(
create: (context) => CounterCubit(),
child: const CounterPage(),
),
),
);
}
class CounterPage extends StatelessWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cubit counter')),
body: Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, count) {
return Text('$count', style: const TextStyle(fontSize: 48));
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'add',
onPressed: () => context.read<CounterCubit>().increment(),
child: const Icon(Icons.add),
),
const SizedBox(height: 12),
FloatingActionButton(
heroTag: 'remove',
onPressed: () => context.read<CounterCubit>().decrement(),
child: const Icon(Icons.remove),
),
],
),
);
}
}
| Piece | Role |
|---|---|
Cubit<int> | Holds an int state |
emit(newState) | Publishes a new state |
BlocProvider | Creates the cubit, makes it available below, and closes it when removed |
BlocBuilder | Rebuilds its builder on every new state |
context.read<CounterCubit>() | Gets the cubit to call a method |
State classes #
Real state is rarely one number. Model each situation the screen can be in. A sealed class makes the compiler check that the UI handles every case.
sealed class ProductsState {}
class ProductsLoading extends ProductsState {}
class ProductsLoaded extends ProductsState {
ProductsLoaded(this.products);
final List<String> products;
}
class ProductsFailed extends ProductsState {
ProductsFailed(this.message);
final String message;
}
class ProductsCubit extends Cubit<ProductsState> {
ProductsCubit(this._repository) : super(ProductsLoading());
final ProductRepository _repository;
Future<void> load() async {
emit(ProductsLoading());
try {
emit(ProductsLoaded(await _repository.fetchAll()));
} catch (e) {
emit(ProductsFailed('Could not load products'));
}
}
}
BlocBuilder<ProductsCubit, ProductsState>(
builder: (context, state) {
return switch (state) {
ProductsLoading() => const Center(child: CircularProgressIndicator()),
ProductsFailed(:final message) => Center(child: Text(message)),
ProductsLoaded(:final products) => ListView(
children: [for (final p in products) ListTile(title: Text(p))],
),
};
},
)
A new state is emitted only if it is not equal to the current one. For classes with fields, either emit new instances as above, or give them value equality with the equatable package.
Bloc: events in, states out #
A Bloc adds one more layer. The UI does not call methods. It adds events, and the bloc registers a handler for each event type.
// Events
sealed class CounterEvent {}
class Incremented extends CounterEvent {}
class Decremented extends CounterEvent {}
// Bloc
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<Incremented>((event, emit) => emit(state + 1));
on<Decremented>((event, emit) => emit(state - 1));
}
}
// In the UI
context.read<CounterBloc>().add(Incremented());
Why bother with the extra code?
- Every change has a name, so logs read like a story:
Incremented,LoginSubmitted,CartCleared. - Events can be transformed: debounce a search field, drop taps while a request is running.
- The same event can come from the UI, a timer or a push notification.
| Cubit | Bloc | |
|---|---|---|
| Triggered by | Method calls | Events |
| Code | Less | More |
| Traceability | States only | Events and states |
| Good default | Yes | When you need event handling features |
Start with Cubit. Move a class to Bloc when you feel the need.
Reacting without rebuilding: BlocListener #
Builders are for drawing. For one-off actions such as a snackbar, a dialog or navigation, use a listener. It runs once per state change.
BlocListener<LoginCubit, LoginState>(
listener: (context, state) {
if (state is LoginFailed) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.message)));
}
if (state is LoginSucceeded) {
Navigator.of(context).pushReplacementNamed('/home');
}
},
child: const LoginForm(),
)
BlocConsumer combines a listener and a builder.
Rebuilding less #
// Rebuild only when a condition holds.
BlocBuilder<CartCubit, CartState>(
buildWhen: (previous, current) => previous.count != current.count,
builder: (context, state) => Text('${state.count}'),
)
// Or select a single value.
final count = context.select((CartCubit cubit) => cubit.state.count);
Several blocs #
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => AuthCubit()),
BlocProvider(create: (context) => ProductsCubit(context.read<ProductRepository>())..load()),
],
child: const MyApp(),
)
RepositoryProvider supplies plain objects such as repositories to the blocs below it.
Testing #
Blocs contain no UI, so they are tested as plain Dart. The bloc_test package makes it short.
blocTest<CounterCubit, int>(
'emits [1] when increment is called',
build: () => CounterCubit(),
act: (cubit) => cubit.increment(),
expect: () => [1],
);
Try it yourself #
Build a login screen with a LoginCubit whose states are LoginInitial, LoginLoading, LoginSucceeded and LoginFailed. Disable the button while loading, show a snackbar on failure with a BlocListener, and navigate on success.