Reactive Programming in Mobile Applications — a Voyage
Reactdroid — Reactive MVI Architecture for Android— Part 2
A comprehensive mobile development library, designed with a pure Kotlin core and extended with an Android layer. For rapid, structured and solid app development — part 2 — Redux
Introduction
In the previous article, we learned How to implement a React-like architecture in Kotlin. This part(s) will continue from where we stopped to show How to implement a Redux-like architecture, using RxKotlin. These two form the Reactdroid library, which is available on GitHub.
This article assumes you have some familiarity with React, Redux and rx.
How (is the Redux-like architecture built) ?
To remind you — on a high level, this is how Reactdroid’s Kotlin core is structured:
Today, we will talk about Reactdroid’s Redux core and its Store.
What we will cover in this article
- The Store class
- The Store’s API — subscribe() & dispatch()
- The Store’s GlobalState and its StoreKeys
- The Store’s Reducers
- Connecting our Component to the Store
Next article(s) will go much deeper into how exactly the Store works.
The Store
The Store is the GlobalState manager. One may subscribe to it — to receive GlobalState change-updates; and dispatch to it — to update the GlobalState.
This 1-directional flow goes like this (this article will cover everything) :
Let’s create our Store. Same as with the Component, we will use an abstract class for platform-specific base-classes to extend (e.g. in Android — AndroidStore
). Extending classes will use Kotlin’s object
(a singleton).
This constructor consists of 2 arguments:
mainReducer
— will hold all the Reducers of the Store (as aList
)preloadedState
— the initial state for when the app starts
And the only member is the (global) state
object, initialized either with the preloadedState
(e.g. some state from the API server), if given, or with the Reducers-defined initial state of the app — provided by the mainReducer
(e.g. some state according to the local DB).
The Store’s API
The Store’s core API consists of 2 methods — subscribe()
and dispatch()
.
One to subscribe for GlobalState updates and the other to update it.
Here is a (very) simplified declaration of these 2 API’s :
In practice, this declaration is way more complex, with generics and HOCs and whatnot. But we really don’t have time for that now, following articles will go deeper 😉.
As a reminder, Redux (and the Store) have nothing to do with React — unless we bind them together (with a Component-dedicated subscribe method).
The Store therefore, is an independent GlobalState manager that anyone can subscribe to by using a pure rx Observer instead of a Component.
The GlobalState
The GlobalState holds the whole state of the whole app.
That means it should be able to hold any type of Object or else we’ll be very restricted. It’s no wonder then, that it’s just a HashMap
:)
The map
maps a String
key to Any?
Object (nullable) value.
Also, as you can see, the underlying map
is internal
— we will use ‘getters’ to easily retrieve values out of it — as this map
is going to be huge with lots of hierarchies.
Although this map
’s keys are raw String
, updating and retrieving from the GlobalState requires StoreKeys.
The GlobalState’s StoreKeys
The StoreKey is basically just a functional interface that returns a String
.
That String
refers to the GlobalState’s map
keys.
The keys in practice will be enums (or maybe Kotlin’s sealed class) which will implement this interface:
Why not just use Strings? Well, it will become more clear as we go deeper, but for example, this way we can bind a StoreKey to a specific Reducer and add ‘getters’ to it, to easily retrieve its respective value from the Store.
Note: this is where we differ from the JavaScript Redux which requires raw Strings as keys.
The Reducer(s)
This part is little less intuitive. Bare with me 🤓.
The Store holds both the GlobalState and the Reducer(s), and simply put, the Reducer(s) helps updating the GlobalState. That’s it.
Now that we calmed down, let’s also say that each Reducer is in charge of updating its own part of the cake; sorry, the GlobalState.
For example, each Reducer might be in charge of a different feature in our app. Makes sense?
The state machine goes like this:
On each dispatch, each Reducer receives 3 arguments:
previous-state, (Store)Key and value. Using them, each Reducer produces a new ‘small’ GlobalState — to replace its relative part of the cake; sorry, the ‘big’ GlobalState.
Finally, the Store merges all ‘small’ parts into the ‘big’ GlobalState and notifies its subscribers.
Enough talk, let’s create the Reducer:
As you can see, the constructor takes a List
of ‘child’ Reducers — this is how we create the mainReducer
which the Store receives to its constructor.
The onNewAction
callback is called by the Store on each dispatch, requesting a new ‘small’ GlobalState part.
Yes, you got that right — both a Reducer’s ‘small’ part of the global state and the Store’s ‘big’, merged global state, have the same type — GlobalState
.
As a GlobalState is basically just a HashMap
, we therefore have a big HashMap which holds multiple, smaller HashMaps — one per Reducer. Like this:
Last, the getDefaultState()
method serves mainly for app starts, when the Store will create its initial GlobalState out of all the Reducers’ default states.
For example, one Reducer might be a DataReducer, which returns all the DB data, which is how we load DB to the Store on app starts :)
To sum this up: the Reducer’s onNewAction
is how it controls a specific, smaller part of the whole GlobalState.
Like the Store, it will be extended as a Kotlin object
(a singleton).
Summarizing the flow
Before we conclude this article with a connected Component example, let’s have a second look at the flow’s diagram and summarize what’s going on, now that we understand all the parts:
- A Component (for example) dispatches an Action(StoreKey, value).
- The Store executes
onNewAction
on all of its Reducers and receives a list of ‘small’ GlobalStates. - The Store merges all ‘small’ parts into 1 ‘big’ GlobalState and updates its
var state: GlobalState
- The Store notifies all subscribers of the new state, e.g. Components.
We’re done 💪. Let’s review a simplified version of everything we did, for perspective:
This, right there 👆 , is Redux, in Kotlin 🤘 🤓 🤘.
Our first connected Component
Remember our ButtonComponent from the previous article? Its text was controlled by its (Component) parent. Let’s instead connect it to a Store:
Our Button’s text is now connected to the Store instead of controlled by its parent. In an actual app, this means you can connect many Components to the same value(s) in a Store, so when it is updated, all of them are re-rendered, automatically, at the same time. No hassle.
Also, the parent in this example, as opposed to the previous one, will not re-render on button clicks, because it updates the Store and not its ownState — also a nice plus.
We will talk about how the connect method works in the next article…
Note: Reactdroid’s mapStateToProps is a merge of 2 of the ‘real’ Redux Connect methods: mapStateToProps + mergeProps.