Sparround

BLoC's evolution and mixed codebases

BLoC's API has gone through significant changes over the years. When you land in older code you need to recognise them — from the official migration guide:

bloc v8.0.0:

  • mapEventToState was removed in favour of the on<Event> API.
  • transformEvents was removed in favour of the EventTransformer API.
  • The TransitionFunction typedef was removed.
  • listen was removed in favour of stream.listen.
  • Calling emit within a closed bloc now throws a StateError, reported as an uncaught exception and propagated to onError. The docs' rationale: previously nothing happened in that case and it was hard to debug what went wrong.

bloc v9.0.0:

  • All previously deprecated APIs were removed; BlocOverrides was dropped in favour of Bloc.observer and Bloc.transformer.
  • A new EmittableStateStreamableSource interface was introduced: bloc_test used to be tightly coupled to BlocBase, and this interface decouples blocTest from that concrete implementation.

Other packages in the ecosystem follow their own version lines: bloc_test 10.x and hydrated_bloc 11.x (where HydratedCubit's storage override became a named parameter).

dart
// ❌ Köhnə API (bloc v8-də silindi): generator funksiya ilə state yayımı.
class CounterBlocOld extends Bloc<CounterEvent, int> {
  CounterBlocOld() : super(0);

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    if (event is Increment) {
      yield state + 1;
    } else if (event is Decrement) {
      yield state - 1;
    }
  }
}

// ✅ Aktual API: hər event tipi üçün ayrı handler.
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
    on<Decrement>((event, emit) => emit(state - 1));
  }
}

// Nə qazanılır:
// - `if (event is ...)` zənciri aradan qalxır, tip yoxlaması handler-in imzasındadır
// - hər handler-ə ayrı EventTransformer vermək mümkün olur (debounce, droppable)
// - handler-lər ayrı metodlara çıxarılıb test edilə bilər

Recognising the old API: `mapEventToState` → `on<Event>`.

The reality of a mixed codebase. In practice two (sometimes three) approaches coexist in one project: older screens on Provider, newer ones on BLoC or Riverpod. That is not a technical problem — flutter_bloc itself is built on package:provider — but it must be managed.

Rules that work:

  • Document the boundary: which part uses which approach. Without a written rule, every PR reopens the debate.
  • No mixing inside a feature: one screen containing both a BlocBuilder and a Consumer is the worst case — readability and debugging both suffer.
  • New code in one approach: the boy-scout rule — old code migrates as it is touched.
  • Shared layers: keep the repository/service layer independent of the approach; that single decision is what makes migration cheap.
  • An onboarding note: a short document telling a new developer what to expect on which screen.
SituationRecommendation
An old screen works and no change is plannedLeave it — migrating working code adds no value
A new feature is added to an old screenMigrate that screen first, then add (writing tests first)
An old screen produces bugs regularlyThe best migration candidate — the investment pays off
Two approaches on one screenLog it as technical debt and unify it soon
The repository layer is tied to an approachDecouple that first — the cheapest step in any migration

The strongest answer to the mixed-codebase question in an interview is not "I'd migrate everything" but a prioritisation criterion: the value of migrating is measured by bug frequency, change frequency and the likelihood the team will work in that area. Migrating a stable screen nobody touches is taking risk for no return.

📚 Sources and documentation