New year new learnings, right? 😉
After spending my last couple of years with the Model View Presenter architecture I recently joined a new project which uses a reactive state container architecture with the help of a library called knot.
For me the whole concept of reactive state container was new and I didn’t know anything about it. Therefore I had to sit down for a while learning its basics, of course, but furthermore learning how to use the library which is basically used all over the project.
But I have a few hints before we dig into details:
- This blog is not about reactive state container in general. It is only about using knot. I may publish a general overview of this concept at a later point. But time will tell.
- The architecture is, similarly to the MVP, a presentation layer architecture.
We want to build the following:
- User opens a view
- The view contains a button with the label “Loading Movies”
- On button click the view shows a loading indicator
- After a while we display a list with movie titles
This is nothing fancy but gives us enough room to explain all concepts.
State
According to the current README of knot a state
is defined as:
State
represents an immutable partial state of an application. It can be a state of a screen or a state of an internal headless component, like a repository.
With this information and the requirements from above we can define it:
sealed class State {
object Init: State()
object Loading: State()
data class Ready(val movies: List<Movie>): State()
object Error: State()
}
After this declaration of our state
we can say that our view has:
- an initial
state
to display the button - a loading
state
to display the loading indicator - a ready
state
to display a list of movies - an error
state
to display an error
Change
The change
is something which results in a state
change. This change
can be triggered by either an action
(see below 👇) or an external source — which can be anything like a click on a button.
Anyways, now we can define our changes
:
sealed class Change {
object Load: Change() {
data class Success(val movies: List<Movie>): Change()
object Fail: Change()
}
}
We are defining three changes
so that we can say:
- the
Load
change
can transform thestate
toLoading
- the
Success
change
can to transform thestate
toReady
- the
Fail
change
can to transform thestate
toError
Action
An action
is an operation which can, if it’s finished, emit a change
.
For our case we should define only one and obvious action
called Load
:
sealed class Action {
object Load: Action()
}
The Load
action
is here to load the movies.
Reducer
A reducer
is a function which has only one responsibility. Turning the given current state
and change
into a new state
. Optionally it can start an action
operation.
Sounds easy, right? Sure, it is:
reduce { change ->
when(change) {
Change.Load -> when(this) { // this is the current state
State.Init -> State.Loading + Action.Load
else -> unexpected(change)
}
is Change.Load.Success -> when(this) {
State.Loading -> State.Ready(change.movies).only
else -> unexpected(change)
}
Change.Load.Fail -> when(this) {
State.Loading -> State.Error.only
else -> unexpected(change)
}
}
}
To be honest, it looks way more complicated than it is 🙂. If you look a little bit deeper this function will really just turn the current state
into another. But sure, this has to do this for all possible cases and this makes it look a little bloated.
But going over it with some words may simplify this code a little bit:
- if
Change.Load
andState.Init
then return
State.Loading
and “execute”Action.Load
- if
Change.Load.Success
andState.Loading
then return
State.Ready
with the loaded movies and without anyaction
- if
Change.Load.Fail
andState.Loading
then returnState.Error
without anyaction
- if there is a
change
incoming which can’t be handled with the currentstate
we throw an error (with theunexpected
function)
Knot
For now we have just declared some classes and a function which do not operate with each other. This is the time where the “heart” — knot — comes into the game. This is the glue code which puts everything together:
knot<State, Change, Action> {
state { initial = State.Init }
changes {
// The reduce function from above
}
actions {
perform<Action.Load> {
this // this is an Observable<Action.Load>
.map {
// Do a operation to load the movies
}
.map { movies -> Change.Load.Success(movies) as Change }
.onErrorReturn { Change.Load.Fail }
}
}
}
To keep it short I removed the reducer
from above. But what we have now is a fully working state machine. 🎉
There is only one piece missing to start the machine:
knot.change.accept(Change.Load)
After this call the following will happen:
- The
reducer
gets called withChange.Load
andState.Init
returnsState.Loading
andAction.Load
- The
perform<Action.Load>
gets called
loads the movies
maps it either toChange.Load.Success
orChange.Load.Fail
- The reducer gets called with
Change.Load.Success
orChange.Load.Fail
returnsState.Ready
resp.State.Error
Okay, cool. But how do we listen to these changes to reflect the state
changes in our view (still on board what we wanted to build 😀)?
This is also pretty easy and a one liner:
val stateChanges: Observable<State> = knot.state
This observable gets called each time when the state
changed. Now we can subscribe our view logic to the state changes.
That’s it. Nothing really fancy, right?
Sure, I also required some time to understand it and I also read a lot about Redux (and this Kotlin implementation example) where this state container architecture thing has its origin. But at the end — as everywhere in the computer world — there is no magic behind it but gives us a quite nice architecture.