flutter
/

Bloc Class: Lifecycle, Methods, and Advanced Usage

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Bloc Class: Lifecycle, Methods, and Advanced Usage

The Bloc class is the core of the BLoC pattern. It manages a stream of states, reacts to events, and ensures predictable state transitions. This guide covers everything from basic usage to advanced patterns, helping you master the Bloc class in Flutter.

What is the Bloc Class?

Bloc<Event, State> is an abstract class that transforms a stream of incoming events into a stream of outgoing states. It provides a structured way to handle business logic, separate from the UI. Each bloc extends this class and defines how events are processed.

Bloc Constructor and Initial State

When extending Bloc, you must call super(initialState) to set the starting state. The constructor is also where you inject dependencies and register event handlers.

DARTRead-only
1
class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<IncrementPressed>(_onIncrement);
    on<DecrementPressed>(_onDecrement);
  }

  void _onIncrement(IncrementPressed event, Emitter<CounterState> emit) {
    emit(CounterState(state.value + 1));
  }

  void _onDecrement(DecrementPressed event, Emitter<CounterState> emit) {
    emit(CounterState(state.value - 1));
  }
}

The on<Event> Method

Use on<Event> to register a handler for a specific event type. The handler receives the event and an Emitter that can be used to emit one or more states. You can also pass an optional transformer to control how events are processed.

DARTRead-only
1
on<Event>(
  handler,
  {EventTransformer<Event>? transformer},
);

Handlers can be synchronous or asynchronous. Use emit inside the handler to output states.

Emitting States

The Emitter<State> passed to handlers is used to emit states. You can call emit multiple times within a handler to produce a sequence of states (e.g., loading → success).

DARTRead-only
1
on<LoginSubmitted>((event, emit) async {
  emit(LoginLoading());
  try {
    final user = await repository.login(event.email, event.password);
    emit(LoginSuccess(user));
  } catch (e) {
    emit(LoginFailure(e.toString()));
  }
});

Bloc Lifecycle

The Bloc class provides lifecycle methods that you can override to perform setup and cleanup.

Called whenever a new state is emitted. Receives the Change<State> containing the previous and next states. Useful for logging or analytics.

DARTRead-only
1
@override
void onChange(Change<State> change) {
  super.onChange(change);
  print('State changed: $change');
}

Called when an uncaught exception occurs inside an event handler. Allows you to handle errors globally.

DARTRead-only
1
@override
void onError(Object error, StackTrace stackTrace) {
  super.onError(error, stackTrace);
  print('Bloc error: $error');
  // Optionally emit an error state
  add(ErrorOccurred(error.toString()));
}

Called when the bloc is closed. Override to dispose of any resources (e.g., streams, controllers).

DARTRead-only
1
@override
Future<void> close() {
  // Dispose resources
  return super.close();
}

Adding Events

Events are added to the bloc using the add method. This can be done from anywhere that has access to the bloc instance.

DARTRead-only
1
// Inside a widget
context.read<CounterBloc>().add(IncrementPressed());

You can also add events from within the bloc itself (e.g., after a successful operation, trigger another event).

Event Transformers

Transformers control the flow of events. You can use the bloc_concurrency package for common strategies: sequential (default), concurrent, droppable, and restartable. Custom transformers can be built using StreamTransformer.

DARTRead-only
1
import 'package:bloc_concurrency/bloc_concurrency.dart';

on<SearchQueryChanged>(
  _onSearch,
  transformer: debounce(const Duration(milliseconds: 300)),
);

Bloc vs Cubit

Cubit is a simplified version of Bloc that doesn't use events. Instead, it exposes methods that directly emit states. Use Cubit for simple state management; use Bloc when you need to leverage event streams, transformers, or complex event handling.

FeatureBlocCubit
Event-basedYesNo
Event transformersYesNo
Emit methodsVia handlersDirectly on cubit
TraceabilityHigh (events logged)Lower
ComplexityHigherLower

Testing the Bloc Class

Use bloc_test to test your bloc's behavior. You can verify that specific events produce expected states.

DARTRead-only
1
blocTest<CounterBloc, CounterState>(
  'emits [1] when increment is added',
  build: () => CounterBloc(),
  act: (bloc) => bloc.add(IncrementPressed()),
  expect: () => [const CounterState(1)],
);

Best Practices

  • Keep blocs focused – Each bloc should handle one feature or domain.
  • Use dependency injection – Pass repositories and services via constructor for testability.
  • Handle errors gracefully – Catch exceptions in handlers and emit error states.
  • Avoid emitting unrelated states – Don't emit states that don't belong to the bloc's responsibility.
  • Use Equatable for states and events – Prevents unnecessary rebuilds and simplifies equality.
  • Override onError for global error handling – Log errors or trigger fallback states.
  • Dispose blocs properly – Use BlocProvider which automatically calls close.

Common Mistakes

  • ❌ Calling emit after close – This will throw an error. Ensure you don't emit after bloc is closed.
  • ❌ Not handling exceptions – Uncaught exceptions stop the bloc; always catch and emit an error state.
  • ❌ Creating blocs inside build – Use BlocProvider to ensure proper lifecycle.
  • ❌ Mutating state objects – Always emit a new state object instead of modifying the existing one.
  • ❌ Overusing global blocs – Provide blocs only where needed to avoid memory leaks.

What's Next?

Now that you understand the Bloc class, explore how to structure events effectively, or dive into testing and architecture patterns.

Next, learn about Bloc events and Bloc architecture.

Test Your Knowledge

Q1
of 3

Which method is used to register an event handler in a Bloc?

A
register
B
on<Event>
C
addHandler
D
listenEvent
Q2
of 3

What happens when you emit a state that is equal to the current state?

A
The state is emitted and UI rebuilds
B
The emission is ignored
C
An exception is thrown
D
The bloc is closed
Q3
of 3

Which lifecycle method is called when a bloc is about to be disposed?

A
dispose
B
onDispose
C
close
D
destroy

Frequently Asked Questions

Can I use both Bloc and Cubit in the same app?

Yes, flutter_bloc supports both. You can mix them according to complexity: use Cubit for simple features and Bloc for those that need event transformers or more structured event handling.

How do I know when to use `Bloc` vs `Cubit`?

Use Cubit for simple state (e.g., counter, form fields). Use Bloc when you need to log events, use transformers (debounce, throttle), or when your business logic benefits from an event-driven architecture.

What happens if I emit a state that is equal to the current state?

Bloc will ignore the emission. This is why you should use Equatable for states – it ensures proper equality checks and prevents unnecessary rebuilds.

How do I handle long‑running operations inside a bloc?

Use asynchronous handlers. You can emit intermediate states (like loading) before and after the operation. Also consider using event transformers like restartable to cancel previous pending operations if needed.

Do I always need to override `onError`?

Not always, but it's good practice for logging and handling unexpected errors gracefully. You can also use a global BlocObserver to catch errors across all blocs.

Previous

bloc states

Next

bloc cubit

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.