Bloc and Cubit Overview

Cubit vs Bloc

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()
class CounterCubit(
) :Cubit<Int>(0)  //0 is the initial state
{
    fun increment() {
        emit(state+1)
    }
    fun decrement() {
        emit(state-1)
    }
}

//application code
val cubit= CounterCubit()
cubit.increment()
println("${cubit.state}") //print 1 
cubit.close()the 
//a base interface defining the type of events CounterBloc can handle
interface CounterEvent
//the actual events that CounterBloc can handle
object IncrementEvent:CounterEvent
object DecrementEvent:CounterEvent

class CounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state 
{
    //the bloc event handlers are defined in the bloc constructor
    init {
        on<IncrementEvent> { event, emit ->
            emit(state+1)
        }
        on<DecrementEvent> { event, emit ->
            emit(state-1)
        }
    }
}

//application code
val 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:

class CounterCubit(
) :Cubit<Int>(0)  //0 is the initial state
{
    fun increment() {
        emit(state+1)
    }
    fun decrement() {
        emit(state-1)
    }
    //method called each time the state of this Cubit instance changes
    override fun onChange(change: Change<Int>) {
        super.onChange(change)
        //..do something here for example write the change to the log for debugging
    }    
    
}

where Change is defined as

class Change<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:

class CounterCubit(
) :Cubit<Int>(0)  //0 is the initial state
{
    fun increment() {
        emit(state+1)
    }
    fun decrement() {
        emit(state-1)
    }
    fun divideby(n:Int)
    {
        if(n==0) {
            addError(Exception("Cannot divide by zero")
            return
        }
        emit(state/n)
    }
    override fun onError(error: Throwable) {
        //...do something in response to the error, like logging
        super.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:

class CounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state 
{
    //the bloc event handlers are defined in the bloc constructor
    init {
        on<IncrementEvent> { event, emit ->
            emit(state+1)
        }
        on<DecrementEvent> { event, emit ->
            emit(state-1)
        }
    }
    override fun onTransition(transition: Transition<CounterEvent, Int>)
    {
        //..do something here, for example write the transition to a log
        super.onTransition(transition)
    }
}

where Transition is defined as:

public class Transition<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

class CounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state 
{
    init {
        //... the bloc event handlers here
    }
    
    override fun onEvent(event:CounterEvent)
    {
        //..do something here, for example write the event to a log
        super.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.

class SampleBlocObserver : BlocObserver<Int> {
    override fun onChange(bloc: BlocBase<Int>, change: Change<Int>) {
        super.onChange(bloc, change)
        println("onChange called with bloc:$bloc and change:$change")
    }
}


//application code
val 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:

interface BlocObserver<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
     */
    @MustCallSuper
    public fun onCreate(bloc:BlocBase<State>) {  }
    /**
     * Called whenever an [event] is `added` to any [bloc] with the given [bloc]
     * and [event].
     */
    //@protected
    @MustCallSuper
    public fun onEvent(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.
     */
    @MustCallSuper
    public fun onChange(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.
     */
    @MustCallSuper
    public fun onTransition(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()]
     */
    @MustCallSuper
    public fun onError(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.
     */
    @MustCallSuper
    public fun onClose(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]
 */
typealias EventTransformer<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.
 */
typealias EventMapper<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);
}


class CounterBloc: Bloc<CounterEvent, Int>(0) //0 is the initial state 
{
    //the bloc event handlers are defined in the bloc constructor
    init {
        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

com.beyondeye.kbloc.concurrency

Last updated