Reductor - Redux for Android. Part 2: Composing reducers
The article is part of series about Reductor library – Redux implementation for Android.
Recap
In Part 1 we covered what is Reductor and how to use it to model state transitions as pure functions. We also used a feature of Reductor called “AutoReducer” to keep reducers type-safe and clean.
We ended up with reducer that looks like this:
Today we are going to use our approach to storing multiple datasets in Reductor Store
.
Problem
In the previous article, we developed our Android app using Redux pattern.
For our application, the instance of Store
was a Single source of truth.
That Store
was holding and managing a single piece of data as a state – List<TodoItem>
.
But we live in real world where requirements change every day. And as experienced android developers, we want to be able to add features to our application easily.
So what if we need to add one feature to our TODO application: allow the user to filter todo items by status.
As filter is just one of three options, we can model it as storing one of defined enum values:
But how can we store current filter?
Store
class is designed to store only one state object.
Naïve approach
The simplest way that might come to mind is to create separate Store
for TodoFilter
.
To do that we would need to create one more Reducer
.
We will call it FilterReducer
.
Using @AutoReducer it should be pretty straightforward.
Then we create corresponding Store
:
As it might look OK-ish initially, this idea is not as good as it seems. By having two different stores we’ve lost the important property of our application – Single source of truth. It means that each time we want to dispatch the action, we need to know which store will handle it. That’s not good.
Any other way? Yes!
Composition
One of 3 principles of Redux
says that whole state should be represented as one tree with a single store.
That means in our case we need to do is to create one more class to hold both List<TodoItem>
and TodoFilter
.
Ok, nothing complex. One holder class to store both our sub-states. The class is also immutable to prevent unexpected mutations (see Redux principle 2, Part 0).
Fixing reducer
But as we changed type our state from List<TodoItem>
to AppState
, reducer needs to be changed as well.
One can easily be created by merging FilterReducer
and TodoReducer
actions into one class.
Each action handler should also be updated to take and return AppState
.
That looks good.
Now we can handle both actions related to List<TodoItem>
and TodoFilter
.
Even more, the structure with container class AppState
allow us to add any arbitrary sub-states and action handlers for them.
However, this approach has some downsides:
- Particular sub-states need to be unpacked from
AppState
before processing, and then packed back to newAppState
; - When application will grow, reducer can become “God reducer” with all the actions we can perform on the whole state.
Let’s see how we can sort it out.
Refactoring
Our reducer now handles all the actions we have in our application.
But we still want logically separate actions that only relate to List<TodoItem>
from actions that handle TodoFilter
.
To do this we need to remind ourselves that Reducer
is still a function with signature (state, action) => state
.
And what we, as programmers are taught to do? – Right! Compose functions.
If we will decompose our AppStateReducer
to smaller functions to compute each sub-state it will look like this:
This way we can manage how we can reduce each particular sub-state. But hey, look closer at this function:
List<TodoItem> items = reduceItems(state.todoItems, action);
Looks familiar, isn’t it? Is it a… Reducer
? Exactly!
Why not reuse reducer from the beginning of the article. This way we can have our reducers small and concise.
That looks fancy.
Our main reducer contains only the logic of dispatching actions to “sub-reducers” and combining the result. That’s the all it needs to know.
On the other hand, our “sub-reducers” only handle the logic related to particular “sub-state”. And they know nothing about the outer state.
That’s what I call composition.
Reducing boilerplate
What we have done is already good enough. But can we do it even better? Sure!
Take a look at our final AppStateReducer
class.
Looks boring, doesn’t it?
For the particular structure of state class, the structure of reducer will be pretty obvious. But do you know any tool to help to write boilerplate code for us? – Right! Annotation processor.
That’s why Reductor has @CombinedState annotation included.
To take advantage of annotation processor, we need to define our combined state as interface annotated with @CombinedState
.
If our AppState
will look like this,
reductor will generate the corresponding reducer:
The generated class will looks very similar to what we wrote by hand, except:
- Reducer includes
Builder
to make it more convenient to provide sub-reducers; - Reducer does null-check on
state
to prevent null pointer exception (Why it matters I’ll cover in one of the next articles); - Before returning new state back, reducer do identity check for all the fields. And if all values are equals to original – we can safely return current state.
Therefore instantiation of reducer will be straithforward:
Customizable @CombinedState class.
As you can see from the example, AppState
is defined as an interface.
That means that Reductor will actually generate two things:
- Implementation of
AppState
(which will be calledAppStateImpl
); - Implementation of
Reducer<AppState>
we saw before.
The implementation of our interface is just simple POJO, even without toString()
and equals()
defined.
But what if we need to make it Serializable
or Parcelable
?
What if we need to use this with Gson or just make toString()
produce something human readable?
That’s a big bunch of features to support.
However, there is already a project which specialises on generating value classes. I’m talking about AutoValue. If you not familiar with it, check out Github page and documentation for more information.
Since version 1.2
AutoValue supports extensions, so value classes can have suppot for
Parcelable,
Gson,
Moshi and much more.
And the good news is that Reductor support AutoValue
classes as @CombinedState
out of the box!
With AutoValue, our AppState
will be defined as
Current @CombinedState support
At the moment (Oct 23, 2016) Reductor support @CombinedState
to be implemented either by
- Interface
- AutoValue class
Support for Kotlin data classes is planned.
Conclusion
The idea of reducer as a pure function is powerful because of the ways it can be composed. Keeping your reducers small and focused makes it’s easy to manage and reason about them.
Taking this into account carrying all your application state tree in one object is just matter of combining smaller reducers. And with help of Java annotation processor, we can make this even easier.