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:
mapEventToStatewas removed in favour of theon<Event>API.transformEventswas removed in favour of theEventTransformerAPI.- The
TransitionFunctiontypedef was removed. listenwas removed in favour ofstream.listen.- Calling
emitwithin a closed bloc now throws aStateError, reported as an uncaught exception and propagated toonError. 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;
BlocOverrideswas dropped in favour ofBloc.observerandBloc.transformer. - A new
EmittableStateStreamableSourceinterface was introduced:bloc_testused to be tightly coupled toBlocBase, and this interface decouplesblocTestfrom 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).
// ❌ 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ərRecognising 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
BlocBuilderand aConsumeris 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.
| Situation | Recommendation |
|---|---|
| An old screen works and no change is planned | Leave it — migrating working code adds no value |
| A new feature is added to an old screen | Migrate that screen first, then add (writing tests first) |
| An old screen produces bugs regularly | The best migration candidate — the investment pays off |
| Two approaches on one screen | Log it as technical debt and unify it soon |
| The repository layer is tied to an approach | Decouple 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
- Bloc migration guideofficialbloclibrary.dev
The official source for the v8 and v9 breaking changes and the removal of mapEventToState.
- Bloc conceptsofficialbloclibrary.dev
- package:blocofficialpub.dev
The current version and changelog.
- package:hydrated_blocofficialpub.dev