Skip to main content

One post tagged with "mocktail"

View All Tags

Mastering BLoC for Flutter Apps: Advanced Architecture & What's New in BLoC 9

Published: · 6 min read
Appxiom Team
Mobile App Performance Experts

Flutter applications have evolved rapidly over the past few years, but one thing remains constant - state management can make or break your application architecture. While Flutter offers several state management approaches, Flutter BLoC continues to be the preferred choice for teams building scalable, testable, and enterprise-grade applications.

With BLoC 9, the library introduces meaningful architectural improvements rather than simply adding new APIs. Several legacy patterns have been removed, widget safety has improved, testing has become easier, and the framework now better aligns with modern Flutter development practices.

In this guide, you'll learn:

  • What's new in BLoC 9 State Management
  • How to migrate from BLoC 8
  • Why BlocOverrides is gone
  • How BlocListener Mounted Checks eliminate common navigation crashes
  • Better testing using bloc_test and mocktail
  • Clean Architecture patterns for production Flutter applications

Why Flutter BLoC Still Matters

As Flutter applications grow, state becomes increasingly difficult to manage. Screens communicate with repositories, services, APIs, authentication layers, local storage, and background tasks.

Without a proper architecture, developers often face:

  • Business logic inside widgets
  • Difficult debugging
  • Tight coupling
  • Poor testability
  • Unexpected rebuilds
  • Memory leaks

Flutter BLoC solves these problems by separating:

  • Presentation
  • Business Logic
  • Data Layer

This separation makes applications predictable, maintainable, and easy to scale.

What's New in BLoC 9 State Management

BLoC 9 is less about introducing new features and more about simplifying existing workflows while improving developer experience.

The biggest changes include:

  • Removal of BlocOverrides
  • Direct global configuration using Bloc.observer
  • Built-in mounted checks inside BlocListener
  • Improved testing abstractions
  • Simplified mocking through EmittableStateStreamableSource

Let's explore each of these changes.

Goodbye BlocOverrides

One of the biggest migration changes is the removal of BlocOverrides.

Before (BLoC 8)

Developers typically wrapped their application like this:

void main() {
BlocOverrides.runZoned(
() => runApp(MyApp()),
blocObserver: MyBlocObserver(),
);
}

Although functional, this added unnecessary boilerplate around application startup.

After (BLoC 9)

Configuration is now much simpler.

void main() {
Bloc.observer = MyBlocObserver();

runApp(MyApp());
}

You can also configure a global event transformer directly.

Bloc.transformer = sequential();

Bloc.observer = MyBlocObserver();

runApp(MyApp());

This makes initialization cleaner while removing an entire abstraction layer.

Benefits

  • Less boilerplate
  • Easier onboarding
  • Simpler application startup
  • Cleaner global configuration

Using Bloc.observer Effectively

Bloc.observer remains one of the most powerful debugging tools in Flutter BLoC.

A custom observer can monitor:

  • Events
  • State transitions
  • Errors
  • Bloc creation
  • Bloc disposal

Example:

class MyBlocObserver extends BlocObserver {

@override
void onEvent(
Bloc bloc,
Object? event,
) {
super.onEvent(bloc, event);

debugPrint(event.toString());
}

@override
void onTransition(
Bloc bloc,
Transition transition,
) {
super.onTransition(bloc, transition);

debugPrint(transition.toString());
}

@override
void onError(
BlocBase bloc,
Object error,
StackTrace stackTrace,
) {
super.onError(
bloc,
error,
stackTrace,
);
}
}

This is particularly useful when diagnosing production issues or understanding complex event flows.

BlocListener Mounted Checks

One of the most practical additions in BLoC 9 State Management is automatic mounted checking.

The Problem

Many Flutter applications crashed because asynchronous events completed after a widget had already been disposed.

Typical code looked like:

listener: (context, state) {

if (!context.mounted) return;

Navigator.push(
context,
MaterialPageRoute(
builder: (_) => HomePage(),
),
);
}

Developers frequently forgot this check, leading to:

  • Navigation exceptions
  • Dialog errors
  • SnackBar failures
  • Context-related crashes

BLoC 9 Solution

BlocListener and BlocConsumer now perform mounted checks internally before executing listeners.

Your listener becomes much cleaner:

BlocListener<AuthBloc, AuthState>(
listener: (context, state) {

Navigator.push(
context,
MaterialPageRoute(
builder: (_) => HomePage(),
),
);

},
child: LoginPage(),
)

The framework ensures the widget is still mounted before invoking the callback.

Advantages

  • Fewer crashes
  • Cleaner listener code
  • Less defensive programming
  • Safer navigation

EmittableStateStreamableSource

Testing has also improved significantly.

BLoC 9 introduces the EmittableStateStreamableSource interface.

Although many developers won't interact with it directly, it simplifies:

  • Mocking
  • Fake blocs
  • Stream-based testing
  • Shared abstractions

Instead of relying on multiple internal interfaces, testing tools now work against a more consistent contract.

This makes mocking significantly easier across packages.

Flutter Clean Architecture with BLoC

Flutter BLoC works best when combined with Flutter Clean Architecture.

A recommended project structure looks like:

lib/

├── presentation/
│ ├── pages/
│ ├── widgets/
│ └── bloc/

├── domain/
│ ├── repositories/
│ ├── usecases/
│ └── entities/

├── data/
│ ├── repositories/
│ ├── models/
│ └── datasource/

└── core/

Each layer has a single responsibility.

Presentation handles UI.

Domain contains business rules.

Data communicates with APIs and local storage.

Repository + BLoC Pattern

A common production architecture looks like this:

UI

Bloc

Use Case

Repository

API / Database

The UI never communicates directly with repositories.

Instead:

  • Widgets dispatch events.
  • BLoC processes events.
  • Repository fetches data.
  • BLoC emits new states.
  • UI rebuilds.

This keeps every layer independent.

Managing State Emission Correctly

One common anti-pattern is emitting states after asynchronous operations without considering lifecycle.

Instead of mixing business logic inside widgets:

on<LoginRequested>((event, emit) async {

emit(LoginLoading());

final user = await repository.login();

emit(LoginSuccess(user));

});

Keep business rules inside repositories and use cases while BLoC focuses on state transitions.

Testing with bloc_test

Testing remains one of Flutter BLoC's strongest advantages.

A typical test looks like:

blocTest<LoginBloc, LoginState>(

'emits loading then success',

build: () => LoginBloc(
repository,
),

act: (bloc) {

bloc.add(
LoginRequested(),
);

},

expect: () => [

LoginLoading(),

LoginSuccess(),

],

);

This makes behavior predictable and easy to verify.

Mocking with mocktail

Repositories are usually mocked using mocktail.

class MockRepository
extends Mock
implements UserRepository {}

Then stub responses.

when(
() => repository.login(),
).thenAnswer(
(_) async => user,
);

This isolates business logic from external dependencies.

Best Practices for BLoC 9

When building production Flutter applications:

  • Keep blocs focused on a single responsibility.
  • Use repositories for data access.
  • Keep widgets free from business logic.
  • Configure Bloc.observer globally.
  • Write tests using bloc_test.
  • Mock dependencies using mocktail.
  • Avoid large, monolithic blocs.
  • Prefer immutable state classes.
  • Separate events and states clearly.
  • Keep UI reactive instead of imperative.

Common Migration Checklist

Migrating from BLoC 8 is straightforward.

  • Remove BlocOverrides.runZoned()
  • Configure Bloc.observer directly
  • Configure global transformers directly
  • Update dependencies
  • Remove unnecessary context.mounted checks inside BlocListener
  • Update unit tests if relying on older mocking abstractions

Production Tips

For larger applications:

  • Split feature modules into independent blocs.
  • Keep repositories injectable.
  • Log transitions with Bloc.observer.
  • Test every event path.
  • Avoid creating blocs inside frequently rebuilding widgets.
  • Dispose resources properly.
  • Prefer feature-first architecture over layer-first organization for very large codebases.

Monitoring State Management Issues Beyond Testing

Even with robust architecture and comprehensive tests, some issues only surface under real user conditions. Race conditions, unexpected event sequences, network latency, and device-specific behaviors can lead to state inconsistencies that are difficult to reproduce locally.

To improve observability in production, consider monitoring:

  • Unhandled exceptions during state transitions
  • Navigation failures triggered by asynchronous events
  • Widget rebuild performance
  • API latency affecting state updates
  • Memory usage and app responsiveness
  • User journeys that fail due to state-related issues

Combining well-tested BLoCs with production monitoring helps identify issues that automated tests may not cover, allowing teams to prioritize fixes based on their impact on real users.

Conclusion

BLoC 9 refines an already mature state management library by removing outdated APIs, improving widget safety, and making testing more consistent. The migration from previous versions is relatively simple, with most changes focused on cleaner configuration and better developer ergonomics.

By adopting Flutter BLoC alongside Flutter Clean Architecture, using Bloc.observer for application-wide insights, leveraging the built-in BlocListener Mounted Checks, and writing reliable tests with bloc_test and mocktail, you can build Flutter applications that are easier to maintain, scale, and debug as they grow in complexity.