BLoC Pattern Overview
BLoC Pattern Overview
The Business Logic Component (BLoC) pattern separates presentation (UI) from business logic in Flutter
apps. It treats the UI as a view that dispatches events and rebuilds on states, with the BLoC sitting in
between. The BLoC is a standalone class that uses Streams under the hood: you add events into the bloc,
and it emits new states via a state stream. This means your UI reacts only to changes in state, making the
code more modular and testable 1 2 .
• Events are inputs to the BLoC, typically user interactions (button taps, form submissions). By
convention they are named in the past tense (e.g. CounterIncrementPressed ) because they
represent actions that have occurred 3 .
• States are the outputs of the BLoC, representing the app’s state at a moment in time. States should
be nouns (e.g. CounterLoadSuccess ) and often use class subclasses or an enum to indicate
status 4 .
• Streams: Internally, a BLoC listens for added events and transforms them (often asynchronously)
into states which are emitted on its state stream. Flutter’s flutter_bloc package provides
BlocBuilder (like a StreamBuilder ) to rebuild UI when new states arrive 5 .
The flutter_bloc package wires BLoC classes into the widget tree. A BlocProvider (or
MultiBlocProvider ) injects a bloc into a widget subtree 6 , and widgets like BlocBuilder and
BlocListener rebuild or trigger callbacks on state changes. For example, BlocBuilder<MyBloc,
MyState>(builder: (context, state) { /* build UI based on state */ }) automatically
looks up the nearest provided MyBloc and rebuilds whenever its state changes 5 . ( BlocListener is
similar but used for side-effects like navigation or dialogs 7 .)
1. Define Event classes: Create a Dart file (e.g. counter_event.dart ) with an abstract base and
subclasses for each event. For example:
// counter_event.dart
abstract class CounterEvent {} // base class
class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}
These events represent user actions. Name them clearly (e.g. CounterIncrementPressed ),
following recommended conventions 3 .
1
2. Define State classes: Create a file (e.g. counter_state.dart ) with an abstract base and state
subclasses. Each state is a snapshot of the UI. For example:
// counter_state.dart
abstract class CounterState {} // base state
class CounterInitial extends CounterState {} // initial state
class CounterLoadSuccess extends CounterState {
final int count;
CounterLoadSuccess(this.count);
}
States should be nouns. In this example, CounterLoadSuccess holds the current counter value.
The initial state ( CounterInitial ) could represent count = 0 or loading. (You can also define
states like CounterLoadInProgress , CounterLoadFailure , etc. to reflect loading or error
status.)
3. Create the BLoC class: In a file (e.g. counter_bloc.dart ), extend Bloc<Event, State> from
flutter_bloc . Pass the initial state to super() , and use the on<Event> handlers to map
events to states. For example:
// counter_bloc.dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterInitial()) {
// Handle increment event
on<CounterIncrementPressed>((event, emit) {
// Determine current count (from state or default 0)
final current = (state is CounterLoadSuccess)
? (state as CounterLoadSuccess).count
: 0;
// Emit a new state with incremented count
emit(CounterLoadSuccess(current + 1));
});
2
and then calls emit(...) with a new state. This causes the counterBloc.state stream to emit
the new state.
(Tip: All event handlers can be async, so you can await calls and then emit after an API or database call.)
1. Provide the BLoC to the UI: Wrap your widget tree with BlocProvider so that child widgets can
access the bloc. For example, in main.dart :
void main() {
runApp(
BlocProvider(
create: (_) => CounterBloc(), // create and provide CounterBloc
child: MyApp(),
),
);
}
3
child: Icon(Icons.remove),
onPressed: () => context.read<CounterBloc>()
.add(CounterDecrementPressed()),
),
],
),
);
}
}
In the above, BlocBuilder rebuilds its child widget ( Text ) whenever the bloc’s state changes
5 . The context.read<CounterBloc>().add(...) call dispatches an event to the bloc.
As a more realistic example, consider an authentication flow. You might define events like “app started”,
“user logged in”, “user logged out”, and states like “authenticated” or “unauthenticated”:
// auth_event.dart
abstract class AuthEvent {}
class AuthStarted extends AuthEvent {}
class AuthLoggedIn extends AuthEvent {
final User user;
AuthLoggedIn(this.user);
}
class AuthLoggedOut extends AuthEvent {}
// auth_state.dart
abstract class AuthState {}
class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
AuthAuthenticated(this.user);
}
class AuthUnauthenticated extends AuthState {}
// auth_bloc.dart
class AuthBloc extends Bloc<AuthEvent, AuthState> {
AuthBloc() : super(AuthInitial()) {
on<AuthStarted>((event, emit) async {
emit(AuthLoading());
// e.g. check token or session
try {
final user = await AuthService.getCurrentUser();
if (user != null) {
4
emit(AuthAuthenticated(user));
} else {
emit(AuthUnauthenticated());
}
} catch (_) {
emit(AuthUnauthenticated());
}
});
on<AuthLoggedIn>((event, emit) {
emit(AuthAuthenticated(event.user));
});
on<AuthLoggedOut>((event, emit) {
emit(AuthUnauthenticated());
});
}
}
• On App Start ( AuthStarted ), the bloc might check a repository or secure storage for a logged-in
user. It first emits AuthLoading , then either AuthAuthenticated(user) or
AuthUnauthenticated() depending on the result.
• When the user successfully logs in ( AuthLoggedIn event), the bloc emits AuthAuthenticated .
• On logout ( AuthLoggedOut ), the bloc emits AuthUnauthenticated .
UI widgets would use BlocListener to navigate (e.g. show login screen if unauthenticated) and
BlocBuilder / BlocConsumer to rebuild parts of the UI based on auth state.
For an API-driven feature, define events for “fetch data” and states for loading, success, or failure. For
example, a weather feature:
// weather_event.dart
abstract class WeatherEvent {}
class FetchWeather extends WeatherEvent {
final String city;
FetchWeather(this.city);
}
// weather_state.dart
abstract class WeatherState {}
class WeatherInitial extends WeatherState {}
class WeatherLoadInProgress extends WeatherState {}
class WeatherLoadSuccess extends WeatherState {
final WeatherData weather;
WeatherLoadSuccess(this.weather);
}
5
class WeatherLoadFailure extends WeatherState {}
// weather_bloc.dart
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
final WeatherRepository repository;
WeatherBloc(this.repository) : super(WeatherInitial()) {
on<FetchWeather>((event, emit) async {
emit(WeatherLoadInProgress());
try {
final weather = await repository.fetchWeather(event.city);
emit(WeatherLoadSuccess(weather));
} catch (_) {
emit(WeatherLoadFailure());
}
});
}
}
Here, when FetchWeather is added, the bloc emits WeatherLoadInProgress , calls the repository,
then emits WeatherLoadSuccess with the data or WeatherLoadFailure on error (see example
pattern 8 ). In the UI, you’d dispatch FetchWeather(city) (e.g. on a form submit) and use
BlocBuilder<WeatherBloc, WeatherState> to show a loading indicator, the weather data, or an error
message based on the current state.
lib/
features/ // (or 'blocs'/'presentation' as top-level)
counter/
bloc/
counter_bloc.dart
counter_event.dart
counter_state.dart
data/
counter_repository.dart
ui/
counter_page.dart
auth/
bloc/
auth_bloc.dart
6
auth_event.dart
auth_state.dart
data/
auth_repository.dart
ui/
login_page.dart
main.dart
lib/
blocs/ // all blocs grouped
counter_bloc/
counter_bloc.dart
counter_event.dart
counter_state.dart
auth_bloc/
auth_bloc.dart
...
data/ // data layer (models, repositories)
presentation/ // UI layer (widgets/screens)
main.dart
In either case, keep related files together and use subdirectories for clarity.
• Equatable for models. Use the Equatable package on state or event classes (and data models) to
simplify value equality. For example:
7
List<Object?> get props => [id];
}
This ensures that identical states aren’t considered different simply by object reference (important
for avoiding unnecessary rebuilds) 10 .
blocTest<CounterBloc, CounterState>(
'emits [CounterLoadSuccess(1)] when CounterIncrementPressed is added',
build: () => CounterBloc(),
act: (bloc) => bloc.add(CounterIncrementPressed()),
expect: () => [CounterLoadSuccess(1)],
);
This verifies that after an increment event, the bloc emits a state with count 1 (similar to documentation
example 12 ). Write tests for each event/state transition, mocking any external dependencies (e.g.
repositories) so tests run predictably. Group related tests and use descriptive names. Testing BLoCs helps
ensure your business logic is correct and remains so during refactoring.
Clean Code Practices: - Single Responsibility: Each Bloc class should handle one feature’s logic. Keep UI
logic out of the BLoC; it should only process events and emit states. - Immutability: Make state classes
immutable (use final fields). If a state has many fields, you can provide a copyWith method to create
modified copies rather than mutating state. - Equatable: As noted, extending Equatable on states and
events makes comparisons easy and prevents emitting duplicate states (which avoids unnecessary rebuilds)
10 . - Minimize Logic in UI: UI widgets should trigger events but not compute business logic. BLoCs
encapsulate logic (e.g. sorting lists, performing calculations, handling retries, etc.), keeping widgets simpler.
Performance Optimization: - Avoid Excessive State Emissions: Don’t emit a new state for every tiny
change. Instead, batch related updates. For example, if multiple properties change together, emit one state
with all updates 13 . This reduces rebuild thrash. - Throttle/Debounce Rapid Events: For events that fire
quickly (e.g. on text input or scrolling), use debouncing/throttling. The flutter_bloc package supports
transforming events (e.g. via transformEvents ) or you can use RxDart. This ensures the bloc processes
only as often as needed 14 . - Selective UI Updates: Use widgets like BlocSelector to rebuild only when
a specific slice of state changes. For example, if a state has many fields, you can select just one field for a
particular widget. This prevents unnecessary rebuilds of the whole subtree 15 . - Resource Cleanup: When
using BlocProvider , closing the bloc is automatic if the bloc was created inside it 6 . If you manually
create blocs or streams, be sure to call .close() in dispose() to free resources. - Efficient Streams:
Only listen/subscribe where needed. Avoid multiple identical BlocBuilder s listening to the same bloc if
one would suffice. Also, ensure async calls (like API requests) use await so you emit states in order.
8
By following these practices—proper testing, clear naming/structure, and mindful performance strategies—
your BLoC-based Flutter app will remain maintainable, efficient, and robust. Remember that the strength of
the BLoC pattern is in its clarity: events go in, states come out, and everything in between is testable logic
2 1 .
Sources: Concepts and examples above are based on the official Bloc documentation and community best
practices 2 1 3 4 5 6 12 .
11 12 Testing | Bloc
https://bloclibrary.dev/testing/