Reactive state container architecture with knot

Stefan M.
4 min readJan 31, 2020

--

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 the state to Loading
  • the Success change can to transform the state to Ready
  • the Fail change can to transform the state to Error

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 and State.Init then return
    State.Loading and “execute” Action.Load
  • if Change.Load.Success and State.Loading then return
    State.Ready with the loaded movies and without any action
  • if Change.Load.Fail and State.Loading then return
    State.Error without any action
  • if there is a change incoming which can’t be handled with the current state we throw an error (with the unexpected 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 with
    Change.Load and State.Init returns
    State.Loading and Action.Load
  • The perform<Action.Load> gets called
    loads the movies
    maps it either to Change.Load.Success or Change.Load.Fail
  • The reducer gets called with
    Change.Load.Success or Change.Load.Fail returns
    State.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.

--

--