flutter
/

Multi-Bloc Communication: Coordinating Multiple BLoCs in Flutter

Last Sync: Today

On this page

10
0%
Advanced
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutterAdvanced

Multi-Bloc Communication: Coordinating Multiple BLoCs in Flutter

In complex Flutter apps, you often have multiple BLoCs or Cubits that need to react to each other's state changes. For example, when a user logs out (handled by AuthBloc), all other blocs should clear their data. Or when a product is added to the cart (CartBloc), the UI might need to update a badge in the HomeBloc. This guide explores various strategies for coordinating multiple blocs, with their pros, cons, and best practices.

Why Multi-Bloc Communication Matters

  • Cross‑feature reactions – e.g., logout triggers reset of all feature states.
  • Shared dependencies – e.g., a SettingsBloc changes theme, affecting all screens.
  • UI consistency – e.g., a cart counter updated from CartBloc is reflected in the HomeBloc.
  • Decoupling – Keeping blocs independent but still able to react to changes.

Approach 1: Global Bloc (Shared State)

One of the simplest ways is to have a global bloc that holds state shared across the app (e.g., AuthBloc). Other blocs can access this global bloc via context.read and react to its state changes. However, to automatically react, they need to either:

  • Use BlocListener in the UI to dispatch events to other blocs.
  • Or have the bloc itself subscribe to the global bloc's stream.
DARTRead-only
1
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiBlocListener(
      listeners: [
        BlocListener<AuthBloc, AuthState>(
          listener: (context, state) {
            if (state is AuthLoggedOut) {
              // Clear data in other blocs
              context.read<CartBloc>().add(ClearCart());
              context.read<ProductBloc>().add(ClearProducts());
            }
          },
        ),
      ],
      child: Scaffold(...),
    );
  }
}

You can have a bloc listen to another bloc's stream directly and react to changes. This keeps the logic inside the bloc but requires the bloc to have access to the other bloc instance.

DARTRead-only
1
class CartBloc extends Bloc<CartEvent, CartState> {
  late final StreamSubscription<AuthState> _authSubscription;

  CartBloc({required AuthBloc authBloc}) : super(CartInitial()) {
    _authSubscription = authBloc.stream.listen((authState) {
      if (authState is AuthLoggedOut) {
        add(ClearCart()); // self-event to clear cart
      }
    });
  }

  @override
  Future<void> close() {
    _authSubscription.cancel();
    return super.close();
  }
}

Approach 2: Using a Shared Repository/Service

A cleaner separation is to move shared state out of blocs and into a repository or service that both blocs depend on. For example, an AuthRepository can hold a Stream of user state, and both AuthBloc and CartBloc can listen to that stream.

DARTRead-only
1
class AuthRepository {
  final _userController = BehaviorSubject<User?>();
  Stream<User?> get user => _userController.stream;

  Future<void> login(String email, String password) async {
    final user = await api.login(email, password);
    _userController.add(user);
  }

  void logout() => _userController.add(null);
}

// CartBloc listens to AuthRepository
class CartBloc extends Bloc<CartEvent, CartState> {
  late final StreamSubscription<User?> _userSubscription;

  CartBloc({required AuthRepository authRepository}) : super(CartInitial()) {
    _userSubscription = authRepository.user.listen((user) {
      if (user == null) add(ClearCart());
    });
  }

  @override
  Future<void> close() {
    _userSubscription.cancel();
    return super.close();
  }
}

Approach 3: Bloc-to-Bloc Communication via Events

Another pattern is to have one bloc dispatch an event to another bloc. This can be done in the UI using BlocListener (as shown above) or by injecting a Bloc instance into another bloc and calling its add method. However, injecting a bloc into another bloc can create tight coupling and is generally discouraged.

Approach 4: Using BlocObserver for Global Reactions

For logging, analytics, or global side effects (like showing a snackbar on any error), you can extend BlocObserver. This observer can see all state changes across all blocs and perform actions without injecting dependencies.

DARTRead-only
1
class AppBlocObserver extends BlocObserver {
  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    if (change.nextState is AuthFailure) {
      // Show snackbar globally
      // You need a reference to a root navigator key
      rootScaffoldMessengerKey.currentState?.showSnackBar(
        SnackBar(content: Text('Authentication failed')),
      );
    }
  }
}

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

Comparison of Approaches

ApproachProsConsBest For
UI with BlocListenerSimple, keeps blocs decoupledLogic lives in UI, harder to testOne‑time reactions (logout, navigation)
Bloc subscribes to other blocLogic stays in blocTight coupling, need to manage subscriptionsWhen one bloc is clearly a dependency of another
Shared repository/serviceClean separation, testableMore boilerplateComplex cross‑bloc logic
BlocObserverGlobal side effectsNot suitable for business logicLogging, error reporting

Best Practices

  • Prefer shared services for long‑term dependencies (e.g., auth state).
  • Use UI coordination for one‑time actions like resetting on logout.
  • Avoid injecting one bloc into another unless the dependency is very stable and you manage disposal carefully.
  • Always dispose subscriptions in bloc close() to avoid memory leaks.
  • Keep blocs focused – if two blocs are tightly coupled, consider merging them into one.
  • Use BlocListener at appropriate levels – place it near the UI that needs to react, not at the top of the tree unless necessary.

Common Mistakes

  • ❌ Creating circular dependencies – Bloc A depends on Bloc B, and Bloc B depends on Bloc A. ✅ Use shared services to break the cycle.
  • ❌ Forgetting to dispose stream subscriptions – Leads to memory leaks. ✅ Always cancel subscriptions in close().
  • ❌ Overusing BlocObserver for business logic – It's meant for debugging, not for application logic. ✅ Keep it for logging and analytics.
  • ❌ Injecting blocs into each other's constructors without thinking – Creates tight coupling and hard‑to‑test code. ✅ Prefer event‑based UI coordination or shared services.

Real-World Example: Shopping App

Imagine an e‑commerce app with AuthBloc, CartBloc, and OrderBloc. When the user logs out:

  • AuthBloc emits LoggedOut.
  • CartBloc should clear the cart.
  • OrderBloc should clear order history.
  • UI should show a login screen.

Solution: Use a shared AuthRepository that exposes a stream. Both CartBloc and OrderBloc subscribe to it and clear data when the user becomes null. The UI uses BlocListener on AuthBloc to navigate to login. This keeps blocs independent and testable.

DARTRead-only
1
// In CartBloc
CartBloc({required AuthRepository authRepository}) {
  _userSub = authRepository.user.listen((user) {
    if (user == null) add(ClearCart());
  });
}

// In UI
BlocListener<AuthBloc, AuthState>(
  listener: (context, state) {
    if (state is LoggedOut) {
      Navigator.pushReplacementNamed(context, '/login');
    }
  },
  child: ...,
);

Conclusion

Communicating between multiple blocs is a common requirement in larger Flutter apps. Choose the right approach based on your needs: use shared services for long‑term dependencies, UI coordination for one‑time reactions, and avoid tight coupling between blocs. By following these patterns, you can keep your blocs focused, testable, and maintainable.

Test Your Knowledge

Q1
of 3

What is a potential risk when injecting one bloc into another?

A
Memory leaks
B
Circular dependencies and tight coupling
C
Slower performance
D
Increased code size
Q2
of 3

Which approach is best for resetting multiple blocs on user logout?

A
Injecting all blocs into AuthBloc and calling reset methods
B
Using a shared AuthRepository that exposes a stream for blocs to listen to
C
Hardcoding reset calls in every bloc's constructor
D
Using a global variable
Q3
of 3

Where should you cancel stream subscriptions created inside a bloc?

A
In the bloc's constructor
B
In the `close()` method
C
In a separate cleanup method
D
Automatically by Dart

Frequently Asked Questions

Can I have one bloc listen to another bloc's state changes?

Yes, you can subscribe to another bloc's stream in the Bloc constructor (or onInit for Cubit) and react to changes. Just remember to cancel the subscription in close().

Is it okay to inject a bloc into another bloc?

It's generally discouraged because it creates tight coupling and can lead to circular dependencies. If you must, consider using a shared service or repository instead.

How do I handle multi-bloc communication in widget tests?

Provide both blocs via MultiBlocProvider in your test widget tree. You can then verify that actions in one bloc cause the expected state changes in the other.

What's the best way to reset multiple blocs on logout?

Either use a shared AuthRepository and have each bloc listen to its stream, or use a BlocListener in the UI to dispatch reset events to all affected blocs.

Can I use `BlocObserver` to coordinate blocs?

BlocObserver is intended for debugging and logging, not for production business logic. It's global and can cause hard‑to‑track side effects. Use it only for analytics or error reporting.

Previous

bloc modular architecture

Next

bloc dependency injection

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.