Sparround

EventTransformer and observability (BlocObserver)

The official docs call Bloc's second advantage over Cubit advanced event transformations: the ability to use reactive operators such as buffer, debounce and throttle.

The mechanism: you pass a transformer parameter to on<Event>. An EventTransformer<T> changes how the incoming event stream is processed by the bloc.

Ready-made transformers live in package:bloc_concurrency:

  • `concurrent` — processes events concurrently.
  • `sequential` — processes events one after another.
  • `droppable`ignores events added while another event is being processed.
  • `restartable` — processes only the latest event and cancels previous handlers.
ScenarioTransformerReason
Typing in a search field`restartable` (plus debounce)Only the latest query's result matters; older requests are cancelled
Double-tapping a "Pay" button`droppable`The second request is dropped — no duplicate transaction
Pagination: "load more"`droppable`Prevents duplicate requests while scrolling
Sending analytics events`sequential`Order matters and no event may be lost
Independent loads`concurrent`Running in parallel is fastest
dart
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:stream_transform/stream_transform.dart';

EventTransformer<E> debounceRestartable<E>(Duration duration) {
  return (events, mapper) =>
      restartable<E>().call(events.debounce(duration), mapper);
}

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  SearchBloc(this._repository) : super(const SearchState()) {
    on<SearchQueryChanged>(
      _onQueryChanged,
      // Sürətli yazılışda yalnız son sorğu işlənir, əvvəlkilər ləğv olunur.
      transformer: debounceRestartable(const Duration(milliseconds: 300)),
    );
  }

  final SearchRepository _repository;

  Future<void> _onQueryChanged(
    SearchQueryChanged event,
    Emitter<SearchState> emit,
  ) async {
    if (event.query.isEmpty) {
      emit(const SearchState());
      return;
    }
    emit(state.copyWith(status: SearchStatus.loading));
    final results = await _repository.search(event.query);
    emit(state.copyWith(status: SearchStatus.success, results: results));
  }
}

Search: debounce plus restartable.

Observability: BlocObserver. To watch the lifecycle of every bloc and cubit from one place, you override BlocObserver:

  • onCreate — a bloc was created
  • onEvent — an event was added to a bloc
  • onChange — the state changed (cubits too)
  • onTransition — the state → event → state transition; per the docs a Transition consists of the current state, the event and the next state, and onTransition is invoked before onChange
  • onError — an error occurred
  • onClose — the bloc was closed

Registration: Bloc.observer = MyObserver();, or Bloc.observer = MultiBlocObserver(observers: [...]); to combine several.

This is the concrete proof of BLoC's traceability advantage in an interview: the log shows which event caused which state transition — information a Cubit cannot provide, because there is no event object.

dart
class AppBlocObserver extends BlocObserver {
  const AppBlocObserver();

  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    // Audit tələb olunan axınlarda event jurnalı buradan yazılır.
    log('${bloc.runtimeType} ← $event');
  }

  @override
  void onTransition(Bloc bloc, Transition transition) {
    super.onTransition(bloc, transition);
    // Transition: cari state + event + növbəti state.
    log('${bloc.runtimeType} ${transition.currentState.runtimeType} '
        '--${transition.event.runtimeType}--> ${transition.nextState.runtimeType}');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    // Crash reporting servisinə göndərmək üçün mərkəzi nöqtə.
    reportError(error, stackTrace, context: bloc.runtimeType.toString());
    super.onError(bloc, error, stackTrace);
  }
}

void main() {
  Bloc.observer = const AppBlocObserver();
  runApp(const MyApp());
}

An observer: logging and crash reporting in one place.

Do not leave the transformer choice to assumption: in critical flows (payments, order creation, pagination) write the transformer parameter explicitly and put it on the code-review checklist. Bugs of the "tapped twice → two orders" kind come precisely from behaviour that was silently assumed.

📚 Sources and documentation