Over and done with unexpected multithreading errors. Our take on Redux

Illustration by Magdalena Tomczyk

At Indoorway we develop mobile apps to smarten up brick-and-mortar buildings. We create many of them and deadlines are usually tight. Thus, as an Android developer, I was looking for a reliable architecture that fits mobile devices well. That’s how I found Redux, and I’ll absolutely stick with it.

Redux has been created by Dan Abramov, primarily to design single page web applications. The aim of the framework is to make managing app states easy and predictable. Asynchronous tasks (needed so much in Android world) and mutability can lead to disaster: race conditions, unpredictable state changes, you name it. Redux introduces 3 principles that help us with taming these problems.

Why do we use Redux on Android?

Android apps rely heavily on background jobs and multitasking, which makes us, developers struggle with handling configuration changes, app restarting, going to background, etc. every single day. It often leads to unpredictable app behaviour or nasty bugs.

Redux helps us with managing these concerns by defining a clear state, in which the app is at the moment. We can easily come back to the previous state or even transfer a state between platforms.

Redux has 3 principles that allow us achieve this:

  • Single source of truth — all state data is kept in a single store. It is useful as you can easily debug and pass data between screens. It’s also easy to serialize the whole app state to recreate it later.
  • Read-only state — the only way to change the app state is to emit an event. Thanks to this and immutabile state you always know in what state the app is and concurrent actions won’t interfere with one another.
  • Changes are made with pure functions — reducers should be pure functions that only take the previous state and action to return the next state. Reducer should be as simple as possible and just return a new state without meddling with previous one.

How do we do it?

OK, let’s set up the basis for our architecture. At Indoorway we don’t use any architecture-specific library. We implemented almost everything ourselves, using a few external libraries of our choice.

We use RxJava2, RxRelay and RxLifecycle to avoid managing Observables lifetime explicitly.

I will describe the most crucial parts of the app. If you want to see the full sample project you can check it out at: https://github.com/indoorway/ReduxAndroidSampleApp

Three basic concepts

In Redux there are 3 basic concepts:

action — it is an event that triggers state change. We declare it as a common interface, which will be then implemented by feature-specific events.

interface AppEvent

In our sample app we have the main feature, which has one action triggered on Activity creation. Actions are objects or data classes, when we need to pass any data.

sealed class MainEvent : AppEvent {
object OnCreate : MainEvent()
}

state — describes state of the app. It should be immutable to allow seamless multi thread state change.

Data class, which is the base for other, more specific states, looks like that:

data class AppState(val main: MainState)

The state usually reflects the one of our screen. We include there everything screen-specific, like actual data that should be displayed. Then we can easily manage these states in our model.

data class MainState(
val githubData: GithubResponse?,
val showLoader: Boolean,
val showError: Boolean)

reducer — it is a simple function, which — for a given event and previous state — returns a new state without meddling with previous one. In our case base function looks like this:

typealias GeneralSubReducer<UiState> = (eventsRelay: Relay<AppEvent>, events: Observable<AppEvent>, states: Observable<UiState>) -> Observable<UiState>

Core classes

In our Redux implementation we maintain Global state, so we can use it in every part of the app. We store it in GlobalModel class.

First, we have base interface GeneralModel, which stores all events and feature-specific states:

abstract class GeneralModel<UiEvent, UiState>(defaultState: UiState){
val events = PublishRelay.create<UiEvent>()
val states = BehaviorRelay.createDefault<UiState>(defaultState)
}

Second, basing on this we implement GlobalModel:

class GlobalModel(initialState: AppState,
private val mainReducer: GeneralSubReducer<MainState>
) : GeneralModel<AppEvent, AppState>(initialState) {

val mainState: Observable<MainState> = states.mapNotNull { it.main }

init {
Observable.merge(listOf(
main()
)).subscribe(states)
}

private fun main() =
mainReducer(events, events, mainState)
.onLatestFrom(states) { copy(main = it) }

companion object {

fun initialAppState(): AppState {
return AppState(main = MainModel.initialState())
}
}
}

Here we have our main state, which is a part of global state:


val mainState: Observable<MainState> = states.mapNotNull { it.main }

Then we implement our main reducer,

private fun main() =
mainReducer(events, events, mainState)
.onLatestFrom(states) { copy(main = it) }

which we add to reducers list.

init {
Observable.merge(listOf(
main()
)).subscribe(states)
}

In companion object we have the initial state of our AppState. We’ll add similar initializer in feature states.

Before we implement feature-specific models, let’s declare its super class. It will contain process method, which will allow us pass events to feature specific methods.

abstract class GeneralSubModel<UiState>(val states: Observable<UiState>) {
abstract fun process(events: Observable<AppEvent>): Observable<UiState>
}

Here we have our generic GeneralSubModel, which has process method in which it is merging it all.

Finally, we can get to our MainModel. Here we override the process method mentioned before,

override fun process(events: Observable<AppEvent>): Observable<MainState> {
return Observable.merge(listOf(callApiOnCreate(events)))
}

which passes events to our business logic methods.

Network jobs

private fun callApiOnCreate(events: Observable<AppEvent>): Observable<MainState> {
return events.ofType(MainEvent.OnCreate::class.java)
.withLatestFrom(states, { event, state -> event to state })
.filter { (_, state) -> state.githubData == null }
.flatMap { (event, state) ->
api.getUserData("octokit")
.subscribeOn(ioScheduler)
.toObservable()
.onLatestFrom(states) {
copy(githubData = it.first(), showLoader = false, showError = false)
}
.observeOn(uiScheduler)
.startWith(state.copy(showLoader = true, showError = false))
.doOnError { Log.e(javaClass.simpleName, it.message, it) }
.onErrorReturn { states.blockingFirst().copy(showError = true, showLoader = false) }
}
}

OK, there’s a lot to process.

Here we have network request to get github data.

First we filter our events looking for MainEvent.OnCreate

return events.ofType(MainEvent.OnCreate::class.java)

Then we pair event with latest state

.withLatestFrom(states, { event, state -> event to state })

and we filter out cases when github data isn’t null so we won’t download it twice.

.filter { (_, state) -> state.githubData == null }

In the next lines we send retrofit data request using ioScheduler.

api.getUserData("octokit")
.subscribeOn(ioScheduler)
.toObservable()

Then we take latest state and then we copy it with new data. The copy is important part because to ensure multithread safety we set states to be immutable.

.onLatestFrom(states) {
copy(githubData = it.first(), showLoader = false, showError = false)
}

After a successful request we should have the state with latest data and our subscription in MainActivity should update UI accordingly.

Going further, we are defining in which state our app should be just after request start. In our example we set to show loader and don’t show error.

.startWith(state.copy(showLoader = true, showError = false))

And finally, we handle error case in turn logging error and then updating our state accordingly.

.doOnError { Log.e(javaClass.simpleName, it.message, it) }
.onErrorReturn { states.blockingFirst().copy(showError = true, showLoader = false) }

All we have to do now is implementing our Activity, which should override RxAppCompatActivity, to help us manage lifecycle of Observables.

Initializing the model

private fun initModel() {
bind(uiEvents(), { GlobalModelModuleImpl.globalModel.mainState }, this::handleStates)}

Firstly, we’re attaching ui events bus. It’s our addition to Redux architecture we were lacking. This way we can send custom events from ui to model. We can handle e.g. custom backpress his way.

Secondly, we point at the part of the app state that we want to use. In our example it is the main state.

Lastly, we observe main events bus, where we observe state changes:

private fun handleStates(states: Observable<MainState>) {
states.publish().apply {
showGithubData()
showNetworkError()
hideNetworkError()
}.connect()
}

Subscription to state change looks like this:

private fun Observable<MainState>.showGithubData() {
mapNotNull { it.githubData }
.distinctUntilChanged()
.subscribe { updateGithubData(it) }
}

Here we subscribe to githubData changes. If it is not null, we check if data changed since the last fetch. If it did, then we are updating our ui.

Finally, we send event MainEvent.OnCreate at the start of our Activity, which then is retrieved in our MainModel.

private fun uiEvents(): Observable<AppEvent> =
Observable.merge(listOf(
additionalEvents,
Observable.just(MainEvent.OnCreate)
))

We have finally gone through all the code allowing us to make basic Redux application. The solution is easily modifiable. For example, in our project we have custom flavours and di of our own, which we will describe in one of our future articles.
Although our blueprint isn’t pure Redux, we are quite pleased with the results and positive that we will implement this architecture in our future apps.

Learning by doing

The best way to comprehend a new topic is to try it yourself. So go on and create your sample app allowing seamless.

--

--