BLoC Concurrency: Managing Event Handlers with bloc_concurrency
Last Sync: Today
flutter
BLoC Concurrency: Managing Event Handlers with bloc_concurrency
What is BLoC Concurrency?
In BLoC, concurrency refers to how multiple events are processed when a handler is already running. By default, BLoC processes events sequentially—one after the other. However, real‑world apps often need more control: you may want to drop new events while a handler is busy, cancel the current handler and restart with the latest event, or allow multiple handlers to run concurrently. The bloc_concurrency package provides ready‑made transformers for these scenarios.
Why Concurrency Matters
Performance – Avoid unnecessary work by dropping or cancelling stale events.
User Experience – Ensure the UI stays responsive even during long operations.
Data Consistency – Prevent race conditions where events complete in unexpected order.
Resource Management – Limit the number of simultaneous operations (e.g., API calls).
By default, BLoC uses sequential() from bloc_concurrency. This queues events and processes them one at a time. If an event handler is running, new events wait in a queue.
DARTRead-only
1
import'package:bloc_concurrency/bloc_concurrency.dart';classMyBlocextendsBloc<MyEvent, MyState>{MyBloc():super(MyState()){
on<MyEvent>(
_onEvent,transformer:sequential(),// This is the default);}
Future<void>_onEvent(MyEvent event, Emitter<MyState> emit) async {// Long operation}}
Droppable: Ignore New Events While Busy
droppable() ignores any new events that arrive while the handler is already processing an event. Only the first event is processed; subsequent ones are dropped.
DARTRead-only
1
classSubmitBlocextendsBloc<SubmitEvent, SubmitState>{SubmitBloc():super(SubmitInitial()){
on<SubmitPressed>(
_onSubmitPressed,transformer:droppable(),);}
Future<void>_onSubmitPressed(SubmitPressed event, Emitter<SubmitState> emit) async {emit(Submitting());await Future.delayed(Duration(seconds:2));// Simulate network callemit(Submitted());}}// If user taps submit button 5 times quickly, only the first tap is processed.
Restartable: Cancel Previous and Start New
restartable() cancels the current running handler (if any) and starts a new one with the latest event. This is perfect for search where you want the latest query to cancel the previous search.
DARTRead-only
1
classSearchBlocextendsBloc<SearchEvent, SearchState>{SearchBloc():super(SearchInitial()){
on<SearchQueryChanged>(
_onSearch,transformer:restartable(),);}
Future<void>_onSearch(SearchQueryChanged event, Emitter<SearchState> emit) async {emit(SearchLoading());try{
final results =awaitsearchApi(event.query);emit(SearchSuccess(results));}catch(e){emit(SearchError(e.toString()));}}}// Each new query cancels the previous in-flight request.
concurrent() allows multiple handlers to run at the same time. Each event is processed without waiting for previous ones to finish. Use this when operations are independent and you want them all to proceed.
DARTRead-only
1
classDownloadBlocextendsBloc<DownloadEvent, DownloadState>{DownloadBloc():super(DownloadInitial()){
on<StartDownload>(
_onStartDownload,transformer:concurrent(),);}
Future<void>_onStartDownload(StartDownload event, Emitter<DownloadState> emit) async {emit(DownloadProgress(event.id,0));for(int i =0; i <=100; i +=10){await Future.delayed(Duration(milliseconds:100));emit(DownloadProgress(event.id, i));}emit(DownloadComplete(event.id));}}// Multiple downloads can run concurrently.
Combining Transformers with RxDart
You can compose bloc_concurrency transformers with rxdart operators. For example, debounce and then restartable:
You can build your own transformer by implementing the EventTransformer typedef. For instance, a transformer that drops events after a certain number of retries.
Choose the right transformer – sequential for ordered operations, droppable for non‑critical events, restartable for search/autocomplete, concurrent for independent tasks.
Use restartable with cancellation – Ensure your async operations support cancellation (e.g., using CancelToken in Dio) to truly cancel in‑flight requests.
Test concurrency – Simulate rapid events in tests and verify state transitions.
Document transformer choices – Explain why you chose a specific transformer for maintainability.
Avoid over‑nesting – Keep transformers simple; combine only when necessary.
Common Mistakes
❌ Using concurrent when you need ordering – Can lead to state inconsistencies.
✅ Use sequential or restartable if order matters.
❌ Not handling cancellations – restartable doesn’t automatically cancel the underlying async operation; you must implement cancellation (e.g., using CancelToken).
❌ Forgetting to import bloc_concurrency – The transformers won’t be available.
❌ Applying transformers to events that don’t need them – Adds complexity without benefit.
Conclusion
Managing concurrency is essential for responsive Flutter apps. The bloc_concurrency package provides simple, declarative transformers that let you control event processing with minimal code. By choosing the right strategy—sequential, droppable, restartable, or concurrent—you can build robust BLoCs that handle user interactions gracefully.
Try it yourself
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
void main() => runApp(MyApp());
// Events
abstract class CounterEvent {}
class Increment extends CounterEvent {}
class Decrement extends CounterEvent {}
// States
class CounterState {
final int value;
CounterState(this.value);
}
// BLoC with droppable (ignores new events while processing)
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterState(0)) {
on<Increment>(
_onIncrement,
transformer: droppable(),
);
on<Decrement>(
_onDecrement,
transformer: droppable(),
);
}
Future<void> _onIncrement(Increment event, Emitter<CounterState> emit) async {
await Future.delayed(Duration(seconds: 1)); // Simulate async work
emit(CounterState(state.value + 1));
}
Future<void> _onDecrement(Decrement event, Emitter<CounterState> emit) async {
await Future.delayed(Duration(seconds: 1));
emit(CounterState(state.value - 1));
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => CounterBloc(),
child: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('BLoC Concurrency Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Text('Value: ${state.value}', style: TextStyle(fontSize: 32));
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(Increment()),
child: Text('+ (droppable)'),
),
SizedBox(width: 20),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(Decrement()),
child: Text('- (droppable)'),
),
],
),
SizedBox(height: 20),
Text('Try tapping multiple times quickly. Only the first tap will take effect until the operation completes.',
textAlign: TextAlign.center),
],
),
),
);
}
}
Test Your Knowledge
Q1
of 3
Which transformer would you use to cancel the current handler and process only the latest event?
A
sequential
B
droppable
C
restartable
D
concurrent
Q2
of 3
What is the default event transformer in flutter_bloc?
A
concurrent
B
restartable
C
droppable
D
sequential
Q3
of 3
Which transformer should you use when you want to ignore any new events while a handler is busy?
A
sequential
B
droppable
C
restartable
D
concurrent
Frequently Asked Questions
What is the difference between sequential and restartable?
sequential queues events; each event waits for the previous one to finish. restartable cancels the current handler and starts a new one immediately with the latest event.
Does `restartable` automatically cancel the previous API request?
No, it only stops the event handler from continuing. You must implement cancellation in your async operation (e.g., using Dio’s CancelToken or AbortController).
Can I use `bloc_concurrency` without `flutter_bloc`?
Yes, the transformers work with any BLoC or Cubit implementation that uses on<Event> with a transformer parameter.
How do I test a BLoC with a custom transformer?
Add events in quick succession and verify the emitted states match your expectations. You can use blocTest from flutter_bloc_test and control timing with skip and wait.
What happens if I use `concurrent` with events that modify the same data?
Race conditions can occur. Use sequential or restartable when state mutations need to be ordered.