flutter
/

Bloc Events: Definition, Handling, and Best Practices

Last Sync: Today

On this page

9
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Bloc Events: Definition, Handling, and Best Practices

In the Bloc pattern, events are the input that trigger state changes. They represent user actions, lifecycle events, or any external input that should cause the application state to evolve. This guide covers everything you need to know about defining, handling, and optimising events in Flutter Bloc.

What Are Bloc Events?

Events are plain Dart classes that describe what happened. They are dispatched to the Bloc, which then processes them and emits new states. Events are the only way to interact with a Bloc – the UI never modifies state directly.

Defining Events

DARTRead-only
1
abstract class CounterEvent {}

class IncrementPressed extends CounterEvent {}

class DecrementPressed extends CounterEvent {}

While this works, it’s better to use Equatable for proper equality checks. Without it, Bloc may not know when two events are identical, leading to unnecessary handlers being called.

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

abstract class CounterEvent extends Equatable {
  const CounterEvent();

  @override
  List<Object> get props => [];
}

class IncrementPressed extends CounterEvent {}

class DecrementPressed extends CounterEvent {}
DARTRead-only
1
class LoginSubmitted extends AuthEvent {
  final String email;
  final String password;

  const LoginSubmitted(this.email, this.password);

  @override
  List<Object> get props => [email, password];
}

Handling Events

Inside your Bloc, you define event handlers using the on<Event> method. Each handler receives the event and an Emitter to emit new states.

DARTRead-only
1
class LoginBloc extends Bloc<LoginEvent, LoginState> {
  final AuthRepository repository;

  LoginBloc(this.repository) : super(LoginInitial()) {
    on<LoginSubmitted>(_onLoginSubmitted);
  }

  Future<void> _onLoginSubmitted(
    LoginSubmitted event,
    Emitter<LoginState> emit,
  ) async {
    emit(LoginLoading());
    try {
      final user = await repository.login(event.email, event.password);
      emit(LoginSuccess(user));
    } catch (e) {
      emit(LoginFailure(e.toString()));
    }
  }
}

Event Transformers

Event transformers allow you to control how events are processed. They are especially useful for handling concurrent events, debouncing, or throttling. The most common transformers come from the bloc_concurrency package.

YAMLRead-only
1
dependencies:
  bloc_concurrency: ^0.2.0
DARTRead-only
1
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:stream_transform/stream_transform.dart';

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  SearchBloc() : super(SearchInitial()) {
    on<SearchQueryChanged>(
      _onSearchQueryChanged,
      transformer: debounce(const Duration(milliseconds: 300)),
    );
  }

  void _onSearchQueryChanged(
    SearchQueryChanged event,
    Emitter<SearchState> emit,
  ) async {
    // perform search
  }
}

EventTransformer<Event> debounce<Event>(Duration duration) {
  return (events, mapper) => events.debounce(duration).switchMap(mapper);
}

Common transformers:

  • sequential() – processes events one after another (default)
  • concurrent() – allows multiple events to be processed simultaneously
  • droppable() – drops new events if a previous handler is still running
  • restartable() – cancels the current handler and starts a new one

Advanced Event Patterns

Sometimes you want to dispatch multiple events in response to a single action. You can do this inside the handler using add().

DARTRead-only
1
on<FormSubmitted>((event, emit) {
  // Validate fields
  add(ValidateName(event.name));
  add(ValidateEmail(event.email));
});

Use the where method on the event stream to filter events based on conditions.

DARTRead-only
1
on<CounterEvent>(
  (event, emit) {
    // handle only if event is relevant
  },
  where: (event) => event is IncrementPressed,
);

Testing Events

Use the bloc_test package to test that your Bloc reacts correctly to events.

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

void main() {
  group('CounterBloc', () {
    blocTest<CounterBloc, CounterState>(
      'emits [1] when increment is added',
      build: () => CounterBloc(),
      act: (bloc) => bloc.add(IncrementPressed()),
      expect: () => [const CounterState(1)],
    );
  });
}

Best Practices for Events

  • Use descriptive names – LoginSubmitted is better than LoginEvent.
  • Make events immutable – Use final fields and Equatable.
  • Keep events small – Each event should represent one logical action.
  • Avoid bloated events – Don't put UI logic or heavy data in events.
  • Use transformers for async flows – Prevent race conditions and unnecessary calls.
  • Document events – Explain what each event does, especially in larger projects.

Common Mistakes

  • ❌ Using the same event for different actions – Leads to complex conditional logic inside handlers.
  • ❌ Not using Equatable – Causes event equality issues and potential bugs.
  • ❌ Handling side effects inside events – Side effects belong in the handler, not the event.
  • ❌ Creating events that are too specific – e.g., IncrementByOne and IncrementByTwo – use a single Increment with a parameter.
  • ❌ Forgetting to handle errors – Always catch exceptions and emit appropriate error states.

What's Next?

Now that you understand events, explore how to structure your app using Bloc architecture and how to test your blocs effectively.

Test Your Knowledge

Q1
of 3

What package is used to provide common event transformers?

A
bloc_test
B
bloc_concurrency
C
flutter_bloc
D
equatable
Q2
of 3

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

A
registerHandler
B
on<Event>
C
addHandler
D
listen
Q3
of 3

Why should you use Equatable for events?

A
To make events serializable
B
To enable proper equality comparisons
C
To improve performance by 50%
D
To generate JSON automatically

Frequently Asked Questions

Should I always use Equatable for events?

Yes, it's highly recommended. Without Equatable, Bloc can't determine if two events are the same, which can lead to unnecessary handler calls and performance issues. It also helps in debugging and testing.

How do I debounce events like search input?

Use an event transformer with debounce from the stream_transform package. Combine it with bloc_concurrency to manage event flow. See the 'Event Transformers' section for a working example.

Can I dispatch an event from inside another event handler?

Yes, you can call add() inside a handler to trigger additional events. However, be cautious of infinite loops. Use add only when it makes logical sense (e.g., validating fields after a submit).

What's the difference between `Bloc` and `Cubit` regarding events?

Bloc uses events to trigger state changes, while Cubit exposes methods that directly emit states. Cubit is simpler but less traceable. Use Bloc when you need to log or transform event streams.

Previous

bloc core concepts

Next

bloc states

Related Content

Need help?

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