I’ve been navigating the Flutter landscape for over five years, and in that time I’ve worked with a lot of state management libraries. I’ve experimented with Provider, RiverPod, the infamous GetX, and of course adopted BLoC. The latter one has become an industry standard and I’ll try to use it in every project I work with.
It has been developed by Felix Angelov for over 6 years. It’s constantly evolving to keep up with the ever-changing requirements of the Flutter world. With BLoC, you get two approaches: a full-fledged BLoC or a streamlined Cubit.
So, why is state management such a big deal? In fact, state management is the framework that keeps application data under control. In the Flutter world, this state stores the data your app is based on.
All the state management libraries provide a standardized way to handle changes in your app’s state and how those changes are transmitted to the UI. The user interface observes state changes, and is adapting accordingly. The idea is to separate the data logic from the user interface, ensuring that everything runs like a well-oiled machine.
Cubit is the simplified sibling of BLoC. Initially launched as a separate plugin, Cubit has been part of the BLoC package since version 6, with the BLoC class itself now extending from Cubit.
The key difference between BLoC and Cubit lies in the way events are handled. BLoC uses an event queue, adding an additional layer of processing, while Cubit gets to the point with direct method calls.
BLoC requires a bit more boilerplate code — each event needs its own class.
abstract class CounterEvent {}class Increment extends CounterEvent {}class Decrement extends CounterEvent {}// Define a BLoC class to manage state and eventsclass CounterBloc extends Bloc<CounterEvent, int> {CounterBloc() : super(0) {// Register event handlerson<Increment>((event, emit) => emit(state + 1));on<Decrement>((event, emit) => emit(state - 1));}}
Cubit simplifies things by allowing state changes to be handled directly via methods in Cubit itself.
// Define a Cubit class to manage stateclass CounterCubit extends Cubit<int> {CounterCubit() : super(0); // Initial state is 0void increment() => emit(state + 1); // Update state by emitting a new valuevoid decrement() => emit(state - 1);}
Every developer eventually faces the decision of choosing the right state management approach for their project. The choice between BLoC and Cubit depends largely on the specific demands and complexity of the application.
Internet community is oftentimes repeating the slogan claiming that you should consider BLoC for large-scale, enterprise projects. BLoC is ideal when your application demands a clear separation between different layers of logic, especially in complex scenarios where you need flexibility in managing and processing events. For instance, if you’re building an app with intricate user flows, such as a multi-step form with validation, BLoC’s event-driven model enables you to separate the event emission from its processing. This decoupling allows for greater modularity, making your code easier to maintain and extend.
Another advantage of BLoC is its ability to handle concurrent events—since version 7.20, BLoC supports processing two events simultaneously, a feature that’s particularly useful in real-time applications where multiple user interactions might overlap.
BLoC also offers a significant edge when it comes to adding or modifying event handlers. You can introduce new event handlers without altering the existing codebase, which enhances scalability and reduces the risk of introducing bugs during updates. Additionally, BLoC makes it easy to incorporate cross-cutting concerns like logging and analytics by simply subscribing to all events, providing a centralized point for monitoring and handling these concerns.
Moreover, for more advanced state management scenarios, such as implementing debounce, switchMap, or throttle operations, BLoC’s event-driven nature makes these patterns easier to implement and manage compared to Cubit, which lacks native support for such event-centric patterns.
You can override onTransition method in both Cubit and BLoC. We usually do it for debugging, to examine state changes precisly. However, when you use a bloc you have a Transition that contains the event:
Transition {currentState: AuthenticationState.authenticated,event: LogoutRequested,nextState: AuthenticationState.unauthenticated}
When you use cubit it contains a simplified version of it:
Transition {currentState: AuthenticationState.authenticated,nextState: AuthenticationState.unauthenticated}
Cubit is the go-to choice when you need a more straightforward and synchronous way to manage state without the overhead of extensive boilerplate code. If your use case involves waiting until event processing is complete before moving forward, Cubit provides a cleaner approach. For example in one of the project I worked with it was crucial to execute data changes in particular order, because interacted with an external hardware. Refactoring BLoC into a Cubit simplified the code and helped in debugging.
Since Cubit uses simple method calls instead of separate Event classes, it’s much easier to implement and maintain for simpler state management needs.
One of the key advantages of Cubit is that it allows you to set breakpoints and flow through the code more intuitively during debugging, something that BLoC’s event-driven architecture can complicate. Additionally, Cubit’s lack of boilerplate makes it ideal for scenarios where you don’t need to transform events or handle them asynchronously.
In most cases, Cubit will suffice for managing state, especially when your state transitions are straightforward. However, if your requirements change and you find yourself needing more complex event transformations, you can easily refactor a Cubit into a BLoC since both extend the BlocBase class. This flexibility makes Cubit a versatile starting point, with the option to scale up to BLoC if the need arises.
They work only with BLoCs.
You can think of transformers as event stream discarders. They give you a full control over what goes into your BLoC or UI.
They are a handy toolbox for fine-tuning events or processing state, and making them invaluable for tasks like throttling, or filtering.
There’s a popular package called bloc_concurrency that includes a few well-rounded class types:
Imagine a complicated dashboard with multiple views, charts and buttons. Pressing the button should initiate some defined action unrelated to other buttons. We can clearly see that processing of those events does not interfere in-between. Each of these updates is an event that needs to be processed and displayed as soon as it arrives. Using a concurrent transformer allows your app to handle multiple streams of data simultaneously, ensuring that no update is delayed because another is being processed.
Consider an app that is executing financial operations. Obviously here you cannot change the order of events if they are assigned to money transactions. A sequential transformer ensures that each upload event is processed only after the previous one has completed. This also helps preserving backend load if events are processed by remote endpoints.
Consider a scenario where a user submits a form and your application processes that submission. If the user accidentally clicks the submit button multiple times, you don’t want your application to process all those clicks. A transformer with deletable capabilities will ignore any additional click events while the initial one is still being processed. This will save us from duplicate submissions and reduce the load on the backend.
In a search feature where results update as the user types, you don’t want to process every single keystroke if the user is typing quickly and processing the search takes more time. With a restartable transformer, the app cancels the previous search query and starts over with the recent one. This guarantees that the user sees the most relevant results without unnecessary delays.
We can also write easily our own one:
Consider a Transformer which automatically retries processing of the events. This is useful in scenarios where operations might fail intermittently, like network requests, and you want to automatically retry before giving up.
EventTransformer<T> retryTransformer<T>(int maxRetries) {return (Stream<T> events, EventMapper<T> mapper) {return events.asyncExpand((event) async* {int attempts = 0;while (attempts < maxRetries) {try {yield await mapper(event);break;} catch (_) {attempts++;if (attempts >= maxRetries) rethrow;}}});};}
And this is how you’d apply the this transformer in a BLoC code:
class NewsBloc extends Bloc<NewsEvent, NewsState> {NewsBloc() : super(NewsState()) {on<FetchNewsEvent>(_onFetchNews,transformer: retryTransformer(3),);}void _onFetchNews(FetchNewsEvent event, Emitter<NewsState> emit) {// Handle the event}}
You can also create a Transformer which delays particular events or event buffers event data into a batch and processes them as a single event.
So now we know that both BLoC and Cubit extend from the same foundation. Each of them has its strength that fits specific needs. BLoC excels in handling of complex and asynchronous event-driven situations. It supports situations where separation of concerns and more than simple event manipulation is a must. On the other hand, Cubit is much simpler, synchronous and does not provide easy tracking and does not support transformers.
From the project management - you need to understand the differences. So no matter what your environment is, and no matter the project requirements are - you should know which of the two fits your code the best.
Quick Links
Legal Stuff