Overview
A Bloc implements the app's business logic. It processes event data (called Action
) from the view / ui component and:
- updates
State
according to its business rules and emits the updatedState
to be consumed by the view / ui component. - optionally creates
SideEffect(s)
which can be consumed by the view / ui component (e.g. for navigation) or by other consumers (analytics events, logging etc.).
Public Interface
A Bloc
implements three public facing functions.
State Stream
A stream to observe State
called a StateStream
. It's similar to StateFlow but doesn't expose the replayCache
and doesn't conflate values.
public val value: State
public suspend fun collect(collector: FlowCollector<State>)
While value
is used to retrieve the current State
, the collect
function is used to collect the flow of States
emitted by the Bloc
.
A StateStream
emits:
- no duplicate values
- an initial value upon subscription (analogous
BehaviorSubject
)
SideEffect Stream
A stream to observe SideEffects
called a SideEffectStream
. It's identical to Flow.
public suspend fun collect(collector: FlowCollector<SideEffect>)
A SideEffectStream
emits:
- all values, even duplicates
- no initial value upon subscription (analogous
PublishSubject
)
Because StateStream
and SideEffectStream
have a function with the same signature -> collect(FlowCollector)
, the latter is accessible through the bloc's sideEffects
property while the former is implemented by the bloc directly:
bloc.collect { /* observe State here */ }
bloc.sideEffects.collect { /* observe side effects here */ }
There's no need to call above functions directly. There are extension functions / wrappers that make observing blocs very easy -> Extensions.
Sink
A sink to send data / Actions
to the Bloc
:
public fun send(action: Action)
While this is not a suspending function, it immediately returns after adding the action to a queue / channel.
Reducer
Processing an Action
usually means invoking a Reducer
:
A reducer is a function that receives the current state and an action object, decides how to update the state if necessary, and returns the new state: (state, action) => newState
(https://redux.js.org/tutorials/fundamentals/part-2-concepts-data-flow)
Above definition is the official Redux reducer definition and captures its essence. Reducers in the context of Kotlin Bloc
are very similar:
(State, Action) -> Proposal
Compared to a Redux reducer, this one returns a Proposal
instead of State
. More details can be found in Reducer.
Side Effect
Side effects are created by "special" reducers that return an Effect
object.
Effects
are simple data classes encapsulating the new proposed state and a list of side effects:
public data class Effect<Proposal : Any, SideEffect : Any>(
val proposal: Proposal?,
val sideEffects: List<SideEffect>
)
The difference between a "regular" reducer and one with side effects is simply the nature of its Proposal
:
// no side effects
(State, Action) -> Proposal
// with side effects
(State, Action) -> Effect<Proposal, SideEffect>
When the framework detects an Effect
it will emit the side effects to a dedicated Stream
that can be observed separately from the regular State
stream. There's a DSL that make it easy to "build" reducers with side effects (see Reducer).
Thunk
Reducers are synchronous in nature and their intended purpose is to update State
right away to make sure the user interface is responsive to user input and updates "without" perceptible delay. Longer running operations should be executed using a Thunk
:
The word "thunk" is a programming term that means "a piece of code that does some delayed work". Rather than execute some logic now, we can write a function body or code that can be used to perform the work later.
https://redux.js.org/usage/writing-logic-thunks
A Thunk
in the context of Kotlin Bloc
is exactly what above definition implies, although its implementation and especially its execution is completely different from a Redux thunk. While the latter is a function, dispatched as an action to a Redux store and processed by the redux-thunk middleware, "our" thunk is not dispatched as an action but triggered the same way a reducer is triggered, by reacting to an Action
that was sent to the Bloc
. On top of that it's also:
- a suspending function
- actions are dispatched to the "next" thunk or reducer in the execution chain
Details can be found in Thunk.
Initializer
Initializers are functions executed when the bloc is created. They are similar to thunks since they can execute asynchronous code and dispatch actions to be processed by other thunks and reducers. Unlike thunks, initializers are executed once and once only during the Lifecycle of a bloc.
Details can be found in Initializer.