flutter
/

BLoC Concurrency: Managing Event Handlers with bloc_concurrency

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

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).

Setting Up bloc_concurrency

Add bloc_concurrency to your pubspec.yaml:

YAMLRead-only
1
dependencies:
  flutter_bloc: ^8.1.5
  bloc_concurrency: ^0.2.5

Then import it in your BLoC files.

Default Behavior: Sequential

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';

class MyBloc extends Bloc<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
class SubmitBloc extends Bloc<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 call
    emit(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
class SearchBloc extends Bloc<SearchEvent, SearchState> {
  SearchBloc() : super(SearchInitial()) {
    on<SearchQueryChanged>(
      _onSearch,
      transformer: restartable(),
    );
  }

  Future<void> _onSearch(SearchQueryChanged event, Emitter<SearchState> emit) async {
    emit(SearchLoading());
    try {
      final results = await searchApi(event.query);
      emit(SearchSuccess(results));
    } catch (e) {
      emit(SearchError(e.toString()));
    }
  }
}

// Each new query cancels the previous in-flight request.

Concurrent: Allow Multiple Handlers Simultaneously

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
class DownloadBloc extends Bloc<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:

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

on<SearchQueryChanged>(
  _onSearch,
  transformer: (events, mapper) => events
      .debounceTime(Duration(milliseconds: 300))
      .transform(restartable())
      .flatMap(mapper),
);

Creating Custom Transformers

You can build your own transformer by implementing the EventTransformer typedef. For instance, a transformer that drops events after a certain number of retries.

DARTRead-only
1
EventTransformer<T> retry<T>(int maxRetries) {
  return (events, mapper) => events
      .transform(sequential())
      .flatMap((event) => mapper(event).retryWhen((errors, stackTrace) => errors.take(maxRetries)));
}

Best Practices

  • 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.

Previous

bloc event transformers

Next

bloc error handling

Related Content

Need help?

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