Basic Concepts
Reduks (similarly to Reduxjs) is basically a simplified Reactive Functional Programming approach for implementing UI.
A very good source of material for understanding redux/reduks are the official reduxjs docs, but I will describe here the main principles.
Reduks main components are:
- the State: it is basically the same as the Model in the standard MVC/MVVM software architectural pattern.
- State change subscribers: when the state changes you will react to changes (usually updating the UI) in state change subscribers.
- Actions and Reducers: Reducers are (pure)functions that specify how the State changes in response to a stream of events (Actions)
- Middlewares: additional pluggable layers (functions) for implementing logic for responding to the stream of events (Actions) or even modify them before they reach the reducers that implement the State change logic. Middlewares (together with event change subscribers) have also the main purpose to allow implementing 'side effects', that are prohibited in reducers, that must be pure functions.
- the Store : the "glue" used to connect all the other components. Its main responsibilities are
- Allow access to the current state
- Send update events to the state via
dispatch(action)
- Keep track of all applied middlewares
- Keep track of the current reducer.
- Registers and unregister state change listeners via
subscribe(listener)
This is Reduks in brief. let us now discuss it in more detail
The State
The state is the set of data that uniquely identify the current state of the application. In Reduks integration with Android, this corresponds to the state of the current Activity.
An important requirement for the data inside the state object is that it is required to be immutable, or in other words it is forbidden to update the state directly.
The only way to mutate the state is to send an action via the store dispatch method, to be processed by the registered state reducers(more on this later), that will generate a new updated state.
The old state must be never modified.
In Kotlin we will typically implement the state as a data class with all fields defined as val's (immutable)
Example:
data class LoginActivityState(val email:String, val password:String, val emailConfirmed:Boolean)
Why using a data class? Because it makes it easier to implement reducers, thanks to the autogenerated copy()
method.
But if you don't want to use data classes you can easily implement the copy()
method like this:
fun copy(email: String?=null, password:String?=null, emailConfirmed:Boolean?=null) =
LoginActivityState(email ?: this.email, password ?: this.password, emailConfirmed ?: this.emailConfirmed)
State Change Subscribers
Before we discuss how the state changes, let's see how we listen to those changes. Through the store method
fun subscribe(storeSubscriber: StoreSubscriber<S>): StoreSubscription
we register callbacks to be called each time the state is modified (i.e. some action is dispatched to the store).
val curLogInfo=LoginInfo("","")
val subscriber=StoreSubscriberFn<LoginActivityState> {
val newState=store.state
val loginfo=LoginInfo(newState.email,newState.password)
if(loginfo.email!= curLogInfo.email||loginfo.password!= curLogInfo.password) {
//log info changed...do something
curLogInfo= loginfo
}
}
You should have noticed that in the subscriber, in order to get the value of the newState we need to reference our store instance. You shoud always get a reference to the new state at
the beginning of the subscriber code and then avoid referencing store.state
directly, otherwise you could end up using different values for newState
Notice that we cannot subscribe for changes of some specific field of the activity state, but only of the whole state.
At first this seems strange. But now we will show how using some advanced features of Reduks, we can turn this into an advantage. The idea behind Reduks is that all that happens in the application is put into a single stream of events so that debugging and testing the application behavior is much easier.
Reduks allows all this but also working with state changes in a way very similar to traditional callbacks. This is enabled by Reduks selectors: instead of writing the subscriber as above we can write the following code:
val subscriberBuilder = StoreSubscriberBuilderFn<ActivityState> { store ->
val sel = SelectorBuilder<ActivityState>()
val sel4LoginInfo=sel.withField { email } .withField { password }.compute { e, p -> LoginInfo(e,p) }
val sel4email=sel.withSingleField { email }
StoreSubscriberFn {
val newState=store.state
sel4LoginInfo.onChangeIn(newState) { newLogInfo ->
//log info changed...send to server for verification
//...then we received notification that email was verified
store.dispatch(Action.EmailConfirmed())
}
sel4email.onChangeIn(newState) { newEmail ->
//email changed : do something
}
}
}
There are a few things to note in this new version of our sample subscriber:
- We are creating a
StoreSubcriberBuilderFn
that a takes aStore
argument and returns aStoreSubscriber
. This is actual the recommended way to build a subscriber. TheStoreSubscriberBuilderFn
takes as argument the store instance, so that inside the subscriber we can get the newState and dispatch new actions to the store. - We are creating selector objects: their purpose is to automatically detect change in one or more state fields and lazily compute a function of these fields, passing
its value to a lambda when the method
onChangeIn(newState)
is called.
As you can see the code now looks similar to code with Callbacks traditionally used for subscribing to asynchronous updates. Selectors can efficiently detect changes in the state, because we have embraced immutable data structures for representing the application state: because the state is immutable we can check for changes of its components, checking if their reference changed. In Reduks we never modify the state or any of its components in-place.
Actions and Reducers
As we mentioned above, whenever we want to change the state of the application we need to send(dispatch) an Action object, that will be processed by the Reducers, that are pure functions that take as input the action and the current state and outputs a new modified state. An action object can be literally any object. For example we can define the following actions
class LoginAction {
class EmailUpdated(val email:String)
class PasswordUpdated(val pw:String)
class EmailConfirmed
}
Reducers
a sample Reducer can be the following
val reducer = ReducerFn<LoginActivityState> { state, action ->
when(action) {
is LoginAction.PasswordUpdated -> state.copy(password = action.pw)
is LoginAction.EmailUpdated -> state.copy(email = action.email,emailConfirmed = false)
is LoginAction.EmailConfirmed -> state.copy(emailConfirmed = true)
else -> state
}
}
Reducers must be pure functions, without side-effects except for updating the state. In particular in a reducer you cannot dispatch actions.
When starting to use Reduks you will notice that many times, dispatching actions in the reducer, or in other words reacting to received actions with sending additional actions is one most natural things you will want to do. This is one of the main reasons that there are middlewares, and the reason why reduks has an integrated middleware called reduks saga