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
SettingsBlocchanges theme, affecting all screens. - UI consistency – e.g., a cart counter updated from
CartBlocis reflected in theHomeBloc. - 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
BlocListenerin the UI to dispatch events to other blocs. - Or have the bloc itself subscribe to the global bloc's stream.
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.
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.
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.
Comparison of Approaches
| Approach | Pros | Cons | Best For |
|---|---|---|---|
| UI with BlocListener | Simple, keeps blocs decoupled | Logic lives in UI, harder to test | One‑time reactions (logout, navigation) |
| Bloc subscribes to other bloc | Logic stays in bloc | Tight coupling, need to manage subscriptions | When one bloc is clearly a dependency of another |
| Shared repository/service | Clean separation, testable | More boilerplate | Complex cross‑bloc logic |
| BlocObserver | Global side effects | Not suitable for business logic | Logging, 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
BlocListenerat 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
BlocObserverfor 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:
AuthBlocemitsLoggedOut.CartBlocshould clear the cart.OrderBlocshould 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.
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.