Definition
A BlocState
is the actual keeper of State
, a source of asynchronous state data (StateStream
) and a Sink
for Proposals
to (potentially) alter its state.
Public Interface
A BlocState
implements two public facing functions.
State Stream
The StateStream
is a stream to observe State
. 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 BlocState
.
A StateStream
emits:
- no duplicate values
- an initial value upon subscription (analogous
BehaviorSubject
)
There are wrappers that make observing blocs very easy -> Extensions.
Sink
A sink to send data / Proposals
to the BlocState
:
public fun send(proposal: Proposal)
As mentioned in the Design Overview, reducers don't return State
but a Proposal
, a concept inspired by the SAM pattern. Proposals
increase the level of decoupling between Bloc
and BlocState
to support a number of use cases:
- a
BlocState]
can enforce domain specific rules like validation or enrichment - connect a
Bloc
to a Redux Store - use
Blocs
asBlocsState
(see Bloc isA BlocState)
Separation of Concerns
A Bloc
doesn't store the state itself but delegates to a BlocState
to separate the two concerns:
- business logic
- storing state
We can easily change a BlocState
to modify the behavior of the component. Take e.g. the ToDo sample app. The bloc is currently defined like this:
fun toDoBloc(context: BlocContext) = bloc<List<ToDo>, ToDoAction>(
context = context,
blocState = PersistingToDoState(CoroutineScope(SupervisorJob()))
) {
PersistingToDoState
is, as the name implies, storing to do data persistently. Changing one line of code can change that behavior:
fun toDoBloc(context: BlocContext) = bloc<List<ToDo>, ToDoAction>(
context = context,
blocState = blocState(emptyList())
) {
Apart from the clear separation of concerns, using BlocStates
has many advantages:
- we can share state between business logic components
- we can persist state (database, network)
- we can add domain rules to the actual state container (validation, enrichment)
- we can use a global state container like a Redux store instead of individual
BlocState
containers (compare Redux)
Bloc isA BlocState
The attentive reader will have noticed that Blocs
and BlocStates
have a very similar public interface:
The only difference between a Bloc
and a BlocState
is their intended purpose and the Bloc's
SideEffectStream
. As a matter of fact a Bloc
is also a BlocState
:
class Bloc<out State : Any, in Action : Any, SideEffect : Any> : BlocState<State, Action>() {
Given that, it's easy to use a Bloc
as BlocState
provided we treat the Proposal
output of one Bloc
as Action
for the next Bloc
like in this example:
fun <State: Any> auditTrailBloc(context: BlocContext, initialValue: State) = bloc<State, State>(
context,
initialValue
) {
thunk {
logger.d("auditTrailBloc: changing state from ${getState()} to $action")
// here we would write the changes to a db or send it to the backend
dispatch(action)
}
reduce { action }
}
Above example illustrates how to define a reusable Bloc
intercepting the Proposals
from one Bloc
, sending it to the actual BlocState
(which is created automatically by the BlocBuilder) and triggering an asynchronous operations. To use this Bloc
as BlocState
, we have an extension asBlocState()
function:
fun bloc(context: BlocContext) = bloc<State, Action>(
context,
auditTrailBloc(context, initialState).asBlocState()
) {
...
}