Event transformers in flutter_bloc allow you to control how incoming events are processed by the BLoC. By default, events are processed sequentially: each event is passed to mapEventToState or the event handler, and the next event waits until the previous one completes. Event transformers let you modify this behavior – you can debounce rapid events, throttle them, or even process them concurrently. They are a powerful tool for optimizing user experience and preventing unnecessary state changes.
Why Use Event Transformers?
Debounce – Wait for a pause before processing (ideal for search inputs).
Throttle – Limit processing to at most once per interval (great for button taps or scroll events).
Concurrent – Process events in parallel (e.g., multiple file uploads).
Sequential – Default behavior; ensure order but can block.
Custom Logic – Combine transformers or implement custom flow control.
Setting Up Dependencies
Add flutter_bloc and optionally rxdart for advanced transformers like debounce and throttle.
In modern flutter_bloc (v8+), you can attach a transformer directly to an event handler using the transformer parameter. The transformer is a function that takes a Stream<Event> and returns a Stream<Event>.
Debouncing ensures that only the last event after a quiet period is processed. This is perfect for search boxes where you want to wait until the user stops typing.
Throttling limits the rate of processing: at most one event per time window. Useful for buttons that should not trigger too fast (e.g., like button, refresh button).
By default, events are processed sequentially (each handler waits for the previous to complete). For operations that can run in parallel, you can use concurrent transformer.
flutter_bloc provides a few built‑in transformers: sequential(), concurrent(), and droppable(). You can import them from package:flutter_bloc/flutter_bloc.dart.
DARTRead-only
1
import'package:flutter_bloc/flutter_bloc.dart';// Default – events processed one after another
on<MyEvent>(_onMyEvent,transformer:sequential());// Concurrent – new events start even if previous is still running
on<MyEvent>(_onMyEvent,transformer:concurrent());// Droppable – if an event is already being processed, new events are dropped
on<MyEvent>(_onMyEvent,transformer:droppable());
Comparison of Transformers
Transformer
Behavior
Use Case
sequential (default)
Events processed one by one, waiting for completion
Any ordered operations, form submissions
concurrent
New events start immediately, without waiting for previous
Independent tasks like multiple file uploads
droppable
If an event is being processed, new ones are dropped
Idempotent actions like 'refresh' where only the first matters
debounce
Delays processing until a pause occurs
Search input, real‑time filtering
throttle
At most one event per time window
Button taps, scroll events, rate limiting
Real‑World Example: Search with Debounce
Here’s a complete example of a search bloc with debounce and error handling.
DARTRead-only
1
import'package:flutter_bloc/flutter_bloc.dart';import'package:rxdart/rxdart.dart';import'package:equatable/equatable.dart';// States
abstract classSearchStateextendsEquatable{constSearchState();
@override List<Object?>getprops=>[];}classSearchInitialextendsSearchState{}classSearchLoadingextendsSearchState{}classSearchSuccessextendsSearchState{
final List<String> results;constSearchSuccess(this.results);
@override List<Object?>getprops=>[results];}classSearchErrorextendsSearchState{
final String message;constSearchError(this.message);
@override List<Object?>getprops=>[message];}// Events
abstract classSearchEventextendsEquatable{constSearchEvent();
@override List<Object?>getprops=>[];}classSearchQueryChangedextendsSearchEvent{
final String query;constSearchQueryChanged(this.query);
@override List<Object?>getprops=>[query];}// BLoCclassSearchBlocextendsBloc<SearchEvent, SearchState>{
final SearchRepository repository;SearchBloc({required this.repository}):super(SearchInitial()){
on<SearchQueryChanged>(
_onSearchQueryChanged,transformer:debounce(constDuration(milliseconds:500)),);}
Future<void>_onSearchQueryChanged(
SearchQueryChanged event,
Emitter<SearchState> emit,) async {if(event.query.isEmpty){emit(SearchInitial());return;}emit(SearchLoading());try{
final results =await repository.search(event.query);emit(SearchSuccess(results));}catch(e){emit(SearchError(e.toString()));}}}// Transformer helper
EventTransformer<Event> debounce<Event>(Duration duration){return(events, mapper)=> events.debounceTime(duration).flatMap(mapper);}
Best Practices
Use transformers only when needed – Default sequential is fine for most cases. Add transformers for high‑frequency events.
Keep transformers pure – Avoid side effects inside transformers; they should only modify the event stream.
Combine transformers with caution – The order of operators matters (e.g., debounce before throttle).
Test event processing – Use bloc_test to verify that events are transformed as expected (e.g., only last debounced event triggers state change).
Handle errors inside event handlers – Transformers don’t catch errors; errors will still stop the stream unless caught in the handler.
Prefer concurrent for independent tasks – Avoid blocking UI for long‑running operations that can run in parallel.
Common Mistakes
❌ Applying debounce to the wrong event – e.g., debouncing a login button leads to slow response.
✅ Use throttle for buttons, debounce for search.
❌ Not disposing of subscriptions – Transformers using RxDart need no manual disposal; the bloc handles it, but ensure you don’t create long‑living streams in the transformer itself.
❌ Forgetting to import rxdart when using debounceTime – This causes compile errors.
❌ Using concurrent for state that depends on order – Concurrent events may cause race conditions; use sequential for ordered operations.
❌ Assuming transformer affects all events – Each on<> can have its own transformer; they don’t share across different event types.
Conclusion
Event transformers give you fine‑grained control over event flow in your BLoCs. They help improve performance, reduce unnecessary state changes, and create a smoother user experience. Whether you need debounce for search, throttle for buttons, or concurrent processing for independent tasks, transformers are an essential tool in any Flutter developer’s BLoC toolkit.
Which transformer would you use to prevent a button from being pressed too quickly?
A
debounce
B
throttle
C
concurrent
D
sequential
Q2
of 4
What does the debounce transformer do?
A
Processes events at most once per time window
B
Delays processing until events pause for a specified duration
C
Processes events in parallel
D
Drops events while another is being processed
Q3
of 4
Which package is required to use debounceTime in an event transformer?
A
flutter_bloc
B
rxdart
C
bloc_concurrency
D
async
Q4
of 4
What is the default event transformer in flutter_bloc?
A
concurrent
B
droppable
C
sequential
D
debounce
Frequently Asked Questions
Do I need RxDart to use event transformers?
For basic sequential, concurrent, and droppable, you don’t need RxDart – they are built into flutter_bloc. For debounce, throttle, or custom transformations, you’ll need RxDart (or you can implement your own with StreamTransformer).
Can I use multiple transformers on the same event handler?
Yes, by composing them using RxDart operators. For example, debounce followed by throttle. Just be careful with the order.
What happens if an event handler throws an error with a transformer?
The error will be emitted and the stream will continue (unless you use async* with try‑catch). Transformers don’t catch errors; you must handle them inside the event handler.
Can I use transformers with Cubits?
No, Cubits don’t have event handlers. Transformers are specific to BLoCs (using on<Event>). For Cubits, you’d need to handle debouncing/throttling manually in your UI or use a separate service.
How do I test that debounce is working correctly?
Use blocTest and simulate rapid events. You can also use fakeAsync from the fake_async package to control time. Verify that only the last event triggers state changes after the debounce delay.