When using flutter_bloc, you have two main ways to manage state: Cubit and Bloc. Both are based on the BLoC pattern, but they differ in complexity, boilerplate, and use cases. This guide will help you understand the differences, strengths, and when to choose one over the other.
What is Cubit?
Cubit is a simplified version of Bloc that uses methods to emit states. You define a state class and a cubit class that extends Cubit<State>. Inside the cubit, you write methods that emit new states by calling emit(). It’s perfect for simple state flows where you don’t need the power of event transforms.
What is Bloc?
Bloc uses events and an on<Event> handler. You define events (as classes) and states, and the bloc listens for incoming events, processes them, and emits new states. Bloc gives you more control: you can transform events, handle debouncing, use emit.forEach, and manage complex asynchronous flows.
Key Differences
| Feature | Cubit | Bloc |
|---|---|---|
| Event representation | Methods | Event classes |
| Boilerplate | Minimal | More (events, handlers) |
| Complexity | Low | Moderate to high |
| Event transformers | Not built-in | Built-in (`on<Event>(..., transformer: ...)`) |
| Debouncing / throttling | Manual (use `Future.delayed` or external) | Built-in with `debounceTime`, `throttleTime` |
| Error handling | Inside method try‑catch | Via `onError` or event handler try‑catch |
| Reactivity | Immediate | Can be immediate or transformed |
| Testing | Simple | Slightly more setup but still straightforward |
When to Use Cubit
- Simple UI state (e.g., toggles, counters, forms)
- No need for event transformation or advanced debouncing
- You prefer minimal boilerplate
- Small to medium apps where complexity is low
When to Use Bloc
- Complex state with many different actions
- Need event transformation (e.g., debounce search input, throttle button clicks)
- Want to use
emit.forEachfor reactive streams - You want to leverage event‑based debugging tools
- Large applications with many developers (events provide clear contract)
Advanced Example: Debouncing with Bloc
One of Bloc’s strengths is built‑in event transformation. Here’s how to debounce a search query using Bloc.
Migrating from Cubit to Bloc
If you start with Cubit and later need Bloc’s features, migration is straightforward:
- Create event classes for each public method of your Cubit.
- Replace the cubit class with a bloc that extends
Bloc<Event, State>. - Move each method’s logic into an
on<Event>handler. - Update UI to dispatch events instead of calling methods.
- Update tests to use
blocTest.
Best Practices
- Start with Cubit – For most features, Cubit is sufficient and simpler.
- Use Bloc when you need event transformation – If you find yourself implementing timers or manual debouncing, switch to Bloc.
- Keep states immutable – Always create new state instances, don’t mutate.
- Use Equatable for states and events – Reduces boilerplate and prevents unnecessary rebuilds.
- Separate business logic – Even in Cubit, keep logic in the cubit, not in UI.
Common Mistakes
- ❌ Using Bloc for everything – Over‑engineering adds unnecessary complexity. ✅ Use Cubit for simple cases.
- ❌ Mutating state directly – Causes subtle bugs. ✅ Always emit a new state.
- ❌ Calling
emitafter bloc is closed – Throws an error. ✅ CheckisClosedbefore emitting if needed. - ❌ Not handling errors – Async exceptions can crash the app. ✅ Catch and emit error states.
Conclusion
Cubit and Bloc are both powerful, but they serve different levels of complexity. Cubit is the recommended starting point for most apps, offering simplicity and clear code. When you need advanced features like debouncing or event transforms, Bloc is there to support you. Choose the tool that matches your needs and keep your state management clean and testable.