Both Bloc and Cubit are objects that encapsulate application state. More precisely a Flow of application states. The difference between them is that in order to change the application state, with Bloc the developer need to send Events to the Bloc, while with Cubit the developer just call methods of the specific Cubit class.
So basically with Bloc there is an additional level of abstraction (Events) separating the request of changing application state and the actual change, that many times can be useful. But many other times the simpler approach of a Cubit can be preferable. Let's see an example of implementing the same functionality both with a Cubit and a Bloc
// A Cubit with a state that is single integer// with two methods for changing the cubit state: increment() and decrement()classCounterCubit() :Cubit<Int>(0) //0 is the initial state{funincrement() {emit(state+1) }fundecrement() {emit(state-1) }}//application codeval cubit=CounterCubit()cubit.increment()println("${cubit.state}") //print 1 cubit.close()the
//a base interface defining the type of events CounterBloc can handleinterfaceCounterEvent//the actual events that CounterBloc can handleobjectIncrementEvent:CounterEventobjectDecrementEvent:CounterEventclassCounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state {//the bloc event handlers are defined in the bloc constructorinit {on<IncrementEvent> { event, emit ->emit(state+1) }on<DecrementEvent> { event, emit ->emit(state-1) } }}//application codeval bloc=CounterBloc()bloc.add(IncrementEvent)println("${bloc.state}") //print 1 bloc.close()
From the point of view of the UI code that is "consuming" the state of the Bloc or Cubit, there is no difference. Both Bloc and Cubit expose both a field state with the current state value, and a stream field that is a StateFlow<BlocState>, that can be converted to a state that can trigger UI recomposition in Compose with the method collectAsState . But you don't actually need to do it manually because compose_bloc provides higher level methods like BlocBuilder, that do it for you.
note that both a Bloc and a Cubit must be closed with a call to the close() method when your are done using them. This is handled automatically when using BlocBuilder
note that both Bloc and Cubit will ignore duplicate states. If we emit a state equal to the previous state then no new state will be emitted in the associated StateFlow.
By default state equality is checked by value. It is possible instead to check equality by reference by setting the useReferenceEqualityForStateChanges flag in Bloc or Cubit base constructor
the emit method should not be called directly from outside the Cubit class or the Bloc event handlers.
onChange and onError
In addition to subscribing to the Flow of states associated to a specific Cubit or Bloc instance, it is also possible to listen to state changes in other ways, both for a specific instance but also globally for all Bloc and Cubit instances. This can be useful for example for debugging. Let's see how this is done. For example for the CounterCubit class we defined above we can write:
classCounterCubit() :Cubit<Int>(0) //0 is the initial state{funincrement() {emit(state+1) }fundecrement() {emit(state-1) }//method called each time the state of this Cubit instance changesoverridefunonChange(change: Change<Int>) {super.onChange(change)//..do something here for example write the change to the log for debugging } }
where Change is defined as
classChange<State>(/** * The current [State] at the time of the [Change]. */val currentState:State,/** * The next [State] at the time of the [Change]. */val nextState:State )
It is also possible to observe "errors": every Cubit has an addError method to indicate that an error has occurred while handling a state change for a specific Cubit instance:
classCounterCubit() :Cubit<Int>(0) //0 is the initial state{funincrement() {emit(state+1) }fundecrement() {emit(state-1) }fundivideby(n:Int) {if(n==0) {addError(Exception("Cannot divide by zero")return }emit(state/n) }overridefunonError(error: Throwable) {//...do something in response to the error, like loggingsuper.onError(error) } }
overriding onChange and OnError is also available for Blocs.
Any unhandled exceptions that occur within an EventHandler are also reported to onError
OnTransition (for Blocs only)
For Blocs there is an additional method that can be overridden that is similar to onChange but that also provides the information about the specific event that caused the state changes:
classCounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state {//the bloc event handlers are defined in the bloc constructorinit {on<IncrementEvent> { event, emit ->emit(state+1) }on<DecrementEvent> { event, emit ->emit(state-1) } }overridefunonTransition(transition: Transition<CounterEvent, Int>) {//..do something here, for example write the transition to a logsuper.onTransition(transition) }}
where Transition is defined as:
publicclassTransition<Event,State>(val currentState:State,/** * The [Event] which triggered the current [Transition]. */val event:Event,val nextState:State)
onEvent (for Blocs only)
For Blocs there is an additional method that can be overridden that is called every time a new event is received. Note that not all events will cause emission of a new Bloc state, so onEvent is not equivalent to OnTransition
classCounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state {init {//... the bloc event handlers here }overridefunonEvent(event:CounterEvent) {//..do something here, for example write the event to a logsuper.onEvent(event) }}
Observing Cubits and Blocs globally
It is also possible to observe for state changes and for error in processing state changes globally for all Blocs and Cubits with a BlocObserver and BlocOverrides. This is very useful for debugging.
classSampleBlocObserver : BlocObserver<Int> {overridefunonChange(bloc: BlocBase<Int>, change: Change<Int>) {super.onChange(bloc, change)println("onChange called with bloc:$bloc and change:$change") }}//application codeval bloc=CounterBloc()val blocObserver=SampleBlocObserver()BlocOverrides.runWithOverrides(blocObserver=blocObserver) { bloc.add(IncrementEvent) //this event will trigger SampleBlocObserver.onChange()}//outside the scope of BlocOverrides the blocObserver is no more active
With a global BlocObserver we can observe not only state changes for Blocs and Cubits but also many other events. Here is the full definition of the BlocObserver interface:
interfaceBlocObserver<State:Any> {/** * Called whenever a [Bloc] is instantiated. * In many cases, a cubit may be lazily instantiated and * [onCreate] can be used to observe exactly when the cubit * instance is created. * NOTE: lazy bloc intantiation is not actually current supported in in compose_bloc * it is supported in the original flutter_bloc implementation */@MustCallSuperpublicfunonCreate(bloc:BlocBase<State>) { }/** * Called whenever an [event] is `added` to any [bloc] with the given [bloc] * and [event]. *///@protected@MustCallSuperpublicfunonEvent(bloc:Bloc<Any,State>,event:Any?) {}/** * Called whenever a [Change] occurs in any [bloc] * A [change] occurs when a new state is emitted. * [onChange] is called before a bloc's state has been updated. */@MustCallSuperpublicfunonChange(bloc:BlocBase<State>,change: Change<State>) {}/** * Called whenever a transition occurs in any [bloc] with the given [bloc] * and [transition]. * A [transition] occurs when a new `event` is added * and a new state is `emitted` from a corresponding [EventHandler]. * [onTransition] is called before a [bloc]'s state has been updated. */@MustCallSuperpublicfunonTransition(bloc:Bloc<Any,State>, transition: Transition<Any?,State>) {}/** * Called whenever an [error] is thrown in any [Bloc] or [Cubit]. * the stackTrace can be obtained (if present) from field [error.stackTraceToString()] */@MustCallSuperpublicfunonError(bloc:BlocBase<State>,error:Throwable) {}/** * Called whenever a [Bloc] is closed. * [onClose] is called just before the [Bloc] is closed * and indicates that the particular instance will no longer * emit new states. */@MustCallSuperpublicfunonClose(bloc:BlocBase<State>) {}}
Event Transformation (for Blocs only)
With Bloc we can provide a custom EventTransformer to change the way incoming events are processed by the Bloc. An EventTransformer is defined as
/** * Used to change how events are processed. * By default events are processed concurrently. * see also [EventTransformer_concurrent] and [EventTransformer_sequential] */typealiasEventTransformer<Event> = (events:Flow<Event>,mapper:EventMapper<Event>) -> Flow<Event>/** * Signature for a function which converts an incoming event * into an outbound stream of events. * Used when defining custom [EventTransformer]s. */typealiasEventMapper<Event> = (Event) -> Flow<Event>
For example we can define an EventTransformer for debouncing events:
fun <Event>EventTransformer_debounce(duration:Long):EventTransformer<Event> ={ events, mapper -> events.debounce(duration).flatMapConcat(mapper);}classCounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state {//the bloc event handlers are defined in the bloc constructorinit {on<IncrementEvent>(EventTransformer_debounce(100)) { event, emit ->emit(state+1) }on<DecrementEvent>(EventTransformer_debounce(100)) { event, emit ->emit(state-1) } }}
we can set EventTransformer at level of the single BlocEventHandler but also globally using BlocOverrides.runWithOverrides.
There are some standard eventtransformer ready to be used, in package