Event transformers are a powerful feature in flutter_bloc that allow you to modify the stream of events before they are processed by your BLoC. They give you control over how events are queued, delayed, filtered, or transformed – enabling advanced scenarios like debouncing search inputs, throttling button presses, or deduplicating events. Transformers are applied using the transformer parameter in the on<Event> method.
Why Use Event Transformers?
Improve Performance – Prevent unnecessary expensive operations (e.g., API calls) by debouncing rapid events.
Enhance User Experience – Throttle fast button taps to avoid multiple submissions.
Avoid Duplicate Work – Ignore consecutive identical events with distinct.
Control Concurrency – Manage how overlapping events are handled (e.g., cancel previous, restart, or queue).
Reduce Noise – Filter out unwanted events before they reach your business logic.
Basic Event Transformer
Every event handler in BLoC can accept a transformer parameter. By default, BLoC uses sequential() from bloc_concurrency, which processes events one at a time. You can override it with custom transformers from rxdart or your own implementation.
The transformer receives an events stream (the incoming events) and a mapper function that maps each event to a Stream<State>. Your transformer should return a Stream<State> that will be emitted to the BLoC.
Debouncing Search Inputs
Debouncing waits until the user pauses typing before triggering a search. This avoids making an API call on every keystroke.
Sometimes you want to ignore events that are identical to the previous one, especially when the event carries the same data (e.g., same search query).
DARTRead-only
1
classSettingsBlocextendsBloc<SettingsEvent, SettingsState>{SettingsBloc():super(SettingsInitial()){
on<ThemeChanged>(
_onThemeChanged,transformer:distinct(),);}void_onThemeChanged(ThemeChanged event, Emitter<SettingsState> emit) async {// Only called if event.theme is different from the previous event's themeemit(SettingsUpdated(theme: event.theme));}
EventTransformer<T> distinct<T>(){return(events, mapper)=> events.distinct().flatMap(mapper);}}
Combining Transformers
You can compose multiple transformers. For example, debounce and then distinct to avoid searching for the same query after a pause.
The bloc_concurrency package provides ready‑made transformers like sequential, droppable, restartable, and concurrent. These control how events are queued when a handler is already running.
DARTRead-only
1
import'package:bloc_concurrency/bloc_concurrency.dart';classMyBlocextendsBloc<MyEvent, MyState>{MyBloc():super(MyState()){
on<MyEvent>(
_onEvent,transformer:restartable(),// Drops previous and keeps latest);}void_onEvent(MyEvent event, Emitter<MyState> emit) async {// long operation}}
Custom Event Transformers
You can build your own transformers by manipulating the event stream. For example, a transformer that ignores events after a certain count.
Define transformers as reusable functions – Keep them outside the BLoC for easier testing and reuse.
Use bloc_concurrency for concurrency control – It’s battle‑tested and covers common patterns.
Apply transformers only when needed – Not every event needs transformation; simple events can use the default sequential.
Keep transformers simple – Complex stream logic can be extracted into helper functions.
Test transformers separately – Verify that your transformer behaves as expected using rxdart’s TestStream.
Common Mistakes
❌ Using debounce without canceling previous work – If you have long‑running operations, a restartable transformer may be better.
✅ Combine debounce with restartable to cancel previous requests.
❌ Applying distinct without proper equality – For custom events, implement == or use Equatable so distinct works correctly.
❌ Forgetting to import rxdart – Many transformers like debounceTime and throttleTime come from rxdart.
❌ Over‑transforming – Adding too many transformations can make the flow hard to debug.
✅ Keep it minimal and documented.
Conclusion
Event transformers give you fine‑grained control over how events are processed in your BLoC. By using debounce, throttle, distinct, and concurrency controls, you can significantly improve app performance and user experience. Combining these transformers with rxdart and bloc_concurrency allows you to handle complex real‑world scenarios elegantly.
Try it yourself
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
// Events
abstract class SearchEvent {}
class SearchQueryChanged extends SearchEvent {
final String query;
SearchQueryChanged(this.query);
}
// States
abstract class SearchState {}
class SearchInitial extends SearchState {}
class SearchLoading extends SearchState {}
class SearchSuccess extends SearchState {
final List<String> results;
SearchSuccess(this.results);
}
class SearchError extends SearchState {
final String message;
SearchError(this.message);
}
// BLoC with debounced transformer
class SearchBloc extends Bloc<SearchEvent, SearchState> {
SearchBloc() : super(SearchInitial()) {
on<SearchQueryChanged>(
_onSearchQueryChanged,
transformer: debounce(const Duration(milliseconds: 500)),
);
}
void _onSearchQueryChanged(SearchQueryChanged event, Emitter<SearchState> emit) async {
emit(SearchLoading());
// Simulate an API call
await Future.delayed(Duration(milliseconds: 300));
if (event.query.isEmpty) {
emit(SearchSuccess([]));
} else {
final results = List.generate(5, (i) => '${event.query} result $i');
emit(SearchSuccess(results));
}
}
EventTransformer<T> debounce<T>(Duration duration) {
return (events, mapper) => events.debounceTime(duration).flatMap(mapper);
}
}
// UI
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => SearchBloc(),
child: SearchPage(),
),
);
}
}
class SearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Debounced Search')),
body: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextField(
decoration: InputDecoration(
labelText: 'Search',
border: OutlineInputBorder(),
),
onChanged: (query) => context.read<SearchBloc>().add(SearchQueryChanged(query)),
),
SizedBox(height: 20),
Expanded(
child: BlocBuilder<SearchBloc, SearchState>(
builder: (context, state) {
if (state is SearchLoading) {
return Center(child: CircularProgressIndicator());
} else if (state is SearchSuccess) {
if (state.results.isEmpty) {
return Center(child: Text('No results'));
}
return ListView.builder(
itemCount: state.results.length,
itemBuilder: (_, i) => ListTile(title: Text(state.results[i])),
);
} else if (state is SearchError) {
return Center(child: Text('Error: ${state.message}'));
}
return Container();
},
),
),
],
),
),
);
}
}
Test Your Knowledge
Q1
of 3
Which transformer would you use to wait for the user to stop typing before triggering a search?
A
throttle
B
debounce
C
distinct
D
restartable
Q2
of 3
What package provides the `debounceTime` and `throttleTime` operators used in event transformers?
A
async
B
bloc
C
rxdart
D
stream_transform
Q3
of 3
How do you apply a custom transformer to a specific event handler in a BLoC?
A
By overriding the `transformEvents` method
B
By passing a `transformer` parameter to `on<Event>`
C
By wrapping the event in a `TransformedEvent`
D
By using a `StreamTransformer` globally
Frequently Asked Questions
What is the difference between debounce and throttle?
Debounce waits for a pause in events before processing the last one. Throttle processes the first event and then ignores subsequent ones for a period. Debounce is great for search inputs; throttle is ideal for rate‑limiting button taps.
Do I need to add rxdart to use event transformers?
Yes, most transformers like debounceTime, throttleTime, and distinct come from rxdart. You can also write custom transformers without rxdart using StreamTransformer but it’s more verbose.
How do I cancel a previous request when a new event arrives?
Use a restartable transformer from bloc_concurrency. It cancels the previous handler and starts a new one with the latest event.
Can I apply different transformers to different events in the same BLoC?
Yes, each on<Event> registration can have its own transformer parameter, allowing per‑event customization.
How do I test an event transformer?
You can test by feeding events into a StreamController and using rxdart’s TestStream to verify the output order and timing.