With bloc_concurrency, how do the concurrent, sequential, droppable and restartable transformers differ, and when would you pick each?
answer
- default: every event concurrently
- per on<E>, via transformer:
- sequential queues in order
- droppable ignores while busy
- restartable cancels the running run
basics
~20 sThey decide how overlapping events of one handler run: concurrent runs them all at once (the default), sequential queues them in order, droppable ignores new events while one runs, and restartable cancels the running one in favour of the newest.
solid answer
~40 sSince `on<E>` arrived in bloc 7.2, events are processed **concurrently** by default, so two slow `CitySearchQueryChanged` runs can finish out of order. bloc_concurrency supplies `EventTransformer`s you pass per handler: `on<E>(handler, transformer: restartable())`. `concurrent()` makes the default explicit. `sequential()` queues events and runs them one at a time in arrival order — for writes such as saving then removing a favourite city. `droppable()` ignores events that arrive while a run is in progress; dropped events never reach the handler — right for a book button or load-more. `restartable()` cancels the running handler when a new event arrives; its later `emit` calls are ignored, so only the newest result lands — right for search and filters. A transformer governs one `on<E>` registration only; different handlers still overlap each other.
code
dart · 13 linesimport 'package:bloc/bloc.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
class TripBloc extends Bloc<TripEvent, TripState> {
TripBloc() : super(const TripState()) {
on<TripCitySearched>(_onCitySearched, transformer: restartable());
on<TripBookPressed>(_onBookPressed, transformer: droppable());
on<TripFavouriteToggled>(_onFavouriteToggled, transformer: sequential());
on<TripPhotosRequested>(_onPhotosRequested, transformer: concurrent());
}
// handlers omitted
}go deeper
Recall the four names and one line each: concurrent is the default, sequential queues, droppable ignores while busy, restartable keeps only the newest.
Explain how each maps to a stream operation, what restartable's cancellation does to the Emitter, and why transformers apply per handler.
Pick transformers from the side-effect profile: droppable for once-only actions, restartable only where cancelled work is harmless, sequential for ordered writes.
Decide whether a team-wide Bloc.transformer default is worth it, weighing safer ordering against slower independent work.
## The default, and why it bites In the **bloc** package (bloc 9.2) every `on<E>` registration subscribes to the Bloc's event stream through an **event transformer**. If you pass none, the Bloc uses `Bloc.transformer`, whose default merges handler runs: every event starts its handler immediately and runs **concurrently** with any still in progress. For a city search that means typing 'Li' then 'Lis' starts two requests, and if 'Li' is slower its results arrive last and overwrite the correct ones. **bloc_concurrency** (0.3, built for bloc 9) packages four transformers so you can choose per handler. ## The four transformers | Transformer | Overlap | Order | Events lost? | Typical use | |---|---|---|---|---| | `concurrent()` | yes | completion order | no | independent work, e.g. prefetching city photos | | `sequential()` | no | arrival order | no | writes that must apply in order | | `droppable()` | no | first wins | yes, while busy | submit, book, load-more | | `restartable()` | no | newest wins | older runs cancelled | search, filters | Under the hood: `concurrent` uses stream_transform's `concurrentAsyncExpand`, `sequential` uses `asyncExpand`, `restartable` uses `switchMap`, and `droppable` ignores incoming events while a mapped run is still active. ## What cancel means for restartable When a newer event arrives, `restartable()` cancels the subscription to the older run. bloc responds by **cancelling that run's Emitter**: - every later `emit` from the old run is ignored, silently; - `emit.isDone` becomes `true`, so the handler can skip expensive post-processing; - any `emit.forEach` or `emit.onEach` inside it stops listening. What cancellation does **not** do is stop the Dart code: an awaited HTTP call still completes and the handler resumes. Only its effect on state is discarded. If the request itself must be aborted, pass a cancellation mechanism to the client from inside the handler. ## Scope: one handler, not the whole Bloc 1. Each `on<E>` has its own subscription and its own transformer. A `sequential()` handler for `CityFavourited` and another for `CityUnfavourited` still overlap **each other**. 2. To serialise several event types together, register one handler for their common base type with `sequential()` and `switch` on the event inside it. 3. `Bloc.transformer` is a static you can set once, for example to `sequential()`, to change the default for every handler without its own transformer. Each Bloc reads it when constructed, so set it before any Bloc is created. ## Choosing in practice - **Search as you type** — `restartable()`, usually combined with a debounce so fewer runs start. - **Book trip** — `droppable()`: a double tap must not start a second booking. - **Favourite, then unfavourite, a city** — `sequential()`: the final state must reflect the last tap. - **Loading photos for several cities** — `concurrent()`: runs are independent. The mistake to avoid is treating `restartable()` as a way to prevent duplicate side effects. It prevents duplicate **states**; the cancelled request may still have reached the server. For side effects that must happen once, `droppable()` or `sequential()` is the safer choice.
- Does sequential() on two different handlers stop them from overlapping each other?No. Each `on<E>` registration has its own subscription and transformer, so `sequential()` only orders events of that one type. To serialise several types together, register one handler for their shared base type with `sequential()` and branch on the event inside it.
- Why can restartable() still cause a duplicate booking?Cancelling a run only cancels its Emitter: later emits are ignored, but the Dart code keeps running and any request already sent completes on the server. restartable guarantees the newest state, not a single side effect; use droppable() for actions that must not run twice.
Think of a travel agent's desk. Concurrent: several agents serve every caller at once. Sequential: one agent, callers wait in line. Droppable: one agent who hangs up on anyone calling while busy. Restartable: one agent who, when you call again, abandons your earlier request and starts the new one, although the earlier booking query may already have gone out.
saying these in an interview costs you the question
- A Bloc processes events one at a time by default
- restartable aborts the pending HTTP request of the older run
- droppable queues ignored events and runs them later
- A transformer on one handler serialises every handler in the Bloc
- sequential drops events that arrive while one is running