A Redux store implemtation entirely base on RxJava (inspired by redux-observable) that helps to isolate side effects.
Dependencies are hosted on Maven Central:
implementation 'com.freeletics.rxredux:rxredux:1.0.0'
Keep in mind that this is library is written in kotlin which means you also add kotlin-stdlib to your project by using RxRedux.
Latest snapshot (directly published from master branch from Travis CI):
allprojects {
repositories {
...
// Add url to snapshot repository
maven {
url "https://oss.sonatype.org/content/repositories/snapshots/"
}
}
}
implementation 'com.freeletics.rxredux:rxredux:1.0.1-SNAPSHOT'
How is this different from other Redux implementations like Mobius
In contrast to any other Redux inspired library out there, this library is really backed on top of RxJava (Mobius just offers some extensions to use RxJava for async works).
This library offers a custom RxJava operator .reduxStore( initialState, sideEffects, reducer ) and treats upstream events as Actions.
A Store is basically an observable container for state.
This library provides a kotlin extension function to create . .reduxStore<State, Action>(initialState, sideEffects, reducer) to create such a state container.
It takes an initialState and a list of SideEffect<State, Action> and a Reducer<State, Action>
An Action is a command to "do something" in the store.
An Action can be triggered by the user of your app (i.e. UI interaction like clicking a button) but also a SideEffect can trigger actions.
Every Action goes through the reducer.
If an Action is not changing the state at all by the Reducer (because it's handled as a side effect), just return the previous state.
Furthermore, SideEffects can be registered for a certain type of Action.
A Reducer is basically a function (State, Action) -> State that takes the current State and an Action to compute a new State.
Every Action goes through the state reducer.
If an Action is not changing the state at all by the Reducer (because it's handled as a side effect), just return the previous state.
A Side Effect is a function of type (Observable<Action>, StateAccessor<State>) -> Observable<Action>.
So basically it's Actions in and Actions out.
You can think of a SideEffect as a use case in clean architecture: It should do just one job.
Every SideEffect can trigger multiple Actions (remember it returns Observable<Action>) which go through the Reducer but can also trigger other SideEffects registered for the corresponding Action.
Also a Action can have a payload. For example if you load some data from backend you emit the loaded data as a Action like data class DataLoadedAction (val data : FooData).
The mantra an Action is a command to do something is still true: in that case it means data is loaded, do with it "something".
Whenever a SideEffect needs to know the current State it can use StateAccessor to grab the latest state from Redux Store. StateAccessor is basically just a function () -> State to grab the latest State anytime you need it.
Let's create a simple Redux Store for Pagination: Goal is to display a list of Persons on screen. Plus one can
data class State {
val currentPage : Int,
val persons : List<Person>, // The list of persons
val loadingNextPage : Boolean,
val errorLoadingNextPage : Throwable?
}
val initialState = State(
currentPage = 0,
persons = emptyList(),
loadingNextPage = false,
errorLoadingNextPage = null
)sealed class Action {
object LoadNextPageAction : Action() // Action to load the first page. Triggered by the user.
data class PageLoadedAction(val personsLoaded : List<Person>, val page : Int) : Action() // Persons has been loaded
object LoadPageAction : Action() // Started loading the list of persons
data class ErrorLoadingNextPageAction(val error : Throwable) : Action() // An error occurred while loading
}// SideEffect is just a type alias for such a function:
fun loadNextPageSideEffect (actions : Observable<Action>, StateAccessor<State>) : Observable<Action> =
actions
.ofType(LoadNextPageAction::class.java) // This side effect only runs for actions of type LoadNextPageAction
.switchMap {
// do network request
val currentState : State = state()
val nextPage = state.currentPage + 1
backend.getPersons(nextPage)
.map { persons : List<Person> ->
PageLoadedAction(
personsLoaded = persons,
page = nextPage
)
}
.onErrorReturn { error -> ErrorLoadingNextPageAction(error) }
.startWith(LoadPageAction)
}// Reducer is just a typealias for a function
fun reducer(state : Statine, action : Action) : State =
when(action) {
is LoadPageAction -> state.copy (loadingNextPage = true)
is ErrorLoadingNextPageAction -> state.copy( loadingNextPage = false, errorLoadingNextPage = action.error)
is PageLoadedAction -> state.copy(
loadingNextPage = false,
errorLoadingNextPage = null
persons = state.persons + action.persons,
page = action.page
)
else -> state // Reducer is actually not handling this action (a SideEffect does it)
}val actions : Observable<Action> = ...
val sideEffects : List<SideEffect<State, Action> = listof(::loadNextPageSideEffect, ... )
actions
.reduxStore( initialState, sideEffects, ::reducer )
.subscribe( state -> view.render(state) )The following video (click on it) illustrate the workflow:
-
Let's take a look at the following illustration: The blue box is the
View(think UI).PresenterorViewModelhas not been drawn for the sake of readability but you can think of having such additional layers between View and Redux State Machine. The yellow box represents aStore. The grey box is thereducer. The pink box is aSideEffectAdditionally, a green circle representStateand a red circle represent anAction(see next step). On the right you see a UI mock of a mobile app to illustrate UI changes. -
NextPageActionget triggered from the UI (by scrolling at the end of the list). EveryActiongoes through thereducerand allSideEffectsregistered for this type of Action. -
Reduceris not interresting onNextPageAction. So whileNextPageActiongoes through the reducer, it doesn't change the state. -
loadNextPageSideEffect(pink box), however, cares aboutNextPageAction. This is the trigger to run the side-effect -
So
loadNextPageSideEffecttakesNextPageActionand starts doing the job and makes the http request to load the next page from backend. Before doing that, this side effect starts with emittingLoadPageAction. -
ReducertakesLoadPageActionemitted from the side effect and reacts on it by "reducing the state". This meansReducerknows how to react onLoadPageActionto compute the new state (showing progress indicator at the bottom of the list). Please note that the state has changed (highlighted in green) which results in also changing the UI (progress indicator at the end of the list). -
Once
loadNextPageSideEffectgot the result back from backend the side effect emits a newPageLoadedAction. This Action contains a "payload" - the loaded data.
data class PageLoadedAction(val personsLoaded : List<Person>, val page : Int)- As any other Action
PageLoadedActiongoes through theReducer. The Reducer processes this Action and computes a new state out of it by appending the loaded data to the already existing data (progress bar also is hidden).
Final remark:
This system allows you to create a plugin in system of SideEffects that are highly reusable and specific to do a single use case.
Also SideEffects can be invoked by Actions from other SideEffects.
This is a common pitfall and is most of the time caused by the fact that an SideEffect emits an Action as output that it also consumes from upstream leading to a infinite loop.
val sideEffect: SideEffect<Int, State> = { actions, state ->
actions.map { it * 2 }
}
val inputActions = Observable.just(1)
inputActions
.reduxStore(
initialState = "InitialState",
sideEffects = listOf(sideEffect)
) { state, action ->
...
}The problem is that from upstream we get Int 1.
But since SideEffect reacts on that action Int 1 too, it computes 1 * 2 and emits 2, which then again gets handled by the same SideEffect 2 * 2 = 4 and emits 4, which then again gets handled by the same SideEffect 4 * 2 = 8 and emits 8, which then getst handled by the same SideEffect and so on (endless loop) ...
Since every Action runs through both Reducer and registered SideEffects this is a valid question.
Technically speaking Reducer gets every Action from upstream before the registered SideEffects.
The idea behind this is that a Reducer may change already the state before a SideEffect starts processing the action.
For example lets assume upstream only emits exactly one action (because then it's simpler to illustrate the squence of workflow):
// 1. upstream emits events
val upstreamActions = Observable.just( SomeAction() )
val sideEffect1: SideEffect<Action, Action> = { actions, state ->
// 3. Runs because of SomeAction
actions.filter { it is SomeAction }.map { OtherAction() }
}
val sideEffect2: SideEffect<Action, Action> ={ actions, state ->
// 5. This runs second because of OtherAction
actions.filter { it is OtherAction }.map { YetAnotherAction() }
}
upstreamActions
.reduxStore(
initialState = ... ,
sideEffects = listOf(sideEffect)
) { state, action ->
// 2. This runs first because of SomeAction
...
// 4. This runs again because of OtherAction (emitted by SideEffect1)
...
// 5. This runs again because of YetAnotherAction emitted from SideEffect2)
}.subscribe( ... )So the workflow is as follows:
- Upstream emits
SomeAction reducerprocessesSomeActionSideEffect1reacts onSomeActionand emitsOtherActionas outputreducerprocessesOtherActionSideEffect2reacts onOtherActionand emitsYetAnotherActionreducerprocessesYetAnotherAction
Absolutely. SideEffect is just a type alias for a function (actions: Observable<Action>, state: StateAccessor<State>) -> Observable<out Action>.
In kotlin you can use a lambda for that like this:
val sideEffect1: SideEffect<Action, Action> = { actions, state ->
actions.filter { it is SomeAction }.map { OtherAction() }
}of write a function (instead of a lambda):
fun sideEffect2(actions : Observable<Action>, state : StateAccessor<State>) : Observable<Action> {
return actions
.filter { it is SomeAction }.map { OtherAction() }
}Both are totally equal and can be used like that:
upstreamActions
.reduxStore(
initialState = ... ,
sideEffects = listOf(sideEffect1, ::sideEffect2)
) { state, action ->
...
}
.subscribe( ... )The same thing is valid for Reducer. Reducer is just a type alias for a function (State, Action) -> State
You can define your reducer as lambda or function:
val reducer = { state, action -> ... }
// or
fun reducer(state : State, action : Action) : State {
...
}Yes it is because .reduxStore(...) is not taking care of only emitting state that has been changed
compared to previous state.
Therefore, .distinctUntilChanged() is considered as best practice.
actions
.reduxStore( ... )
.distinctUntilChanged()
.subscribe { state -> view.render(state) }

