Reductor - Redux for Android. Part 1: Introduction
The article is part of series about Reductor library – Redux implementation for Android.
In Part 0 we made simple TODO application using immutable state and self-written state container. We found that mutable data structures can lead to uncontrolled data changes that could be the root of the unpredictable behaviour. After some code changes, we showed that mutations can be avoided by keeping only one immutable object with mutable reference.
In this article, we will cover how Redux can help with this problem and reimplement our application using Reductor.
Here is our TodoStore (class that manage the state) we end up with last time:
Redux and Reductor
This “Store” pattern looks nice for a particular case. It can help to isolate mutations and make state change testable. But how can we reuse it and decrease the amount of boilerplate code needed every time we want to add and manage any new dataset. And that’s where Redux and Reductor will help us.
Reductor is a minimalistic library that reimplements Redux (Javascript library for state management) in Java. It was created with Android in mind but can be used in any Java application. The main idea is to embrace the power of immutable data structures and functional composition to provide a sophisticated approach to developing rich client-side applications.
Here is how Redux works (from here):
The whole idea is really simple:
Store
object holds and maintains the state- To change the state application can dispatch actions to
Store
- For each action
Store
compute new state using pure function f(s, a) (calledreducer
) Store
replace current state with new state
And that’s it! Though it looks elementary this approach is really powerful. To illustrate this will reimplement our TODO application using this approach with Reductor. But don’t worry, there will be not so many changes from our original example.
So, let’s start!
Installation
First of all, we need to add Reductor as a dependency. Follow Installation for more details.
When added, Store
, Reducer
and Action
classes should be available in the classpath.
Defining the shape state.
Before writing any code, we need to choose how our state should look like.
As we only need to store a list of TodoItem
, for our simple example that will be List<TodoItem>
.
You may wonder: What if we need to store not a single object but a few of them?
Can I store 2, 3 or 42 objects with different types in Store
? – Yes and No.
Store
is designed to store one particular state object of the arbitrary type.
However what we can do is to compose several “substates” into one object.
We will talk about how to do this in the next article.
Defining actions
As said before, we mutate change our state by dispatching actions.
Actions are dispatched by actioin type – string identifier that denotes the action which will be applied to the state.
We can define them just as constants.
However, to handle an action we need a bit more information than just a name.
That’s why in Reductor Action
is predefined type with two fields:
type
is String action type we just defined abovevalue
is payload you can attach to an action if necessary
Additionally, we can define action creators – helper functions to create actions. This step is not required but is strongly suggested to keep code clean.
Basically, that’s our contract. We defined all possible operations we can perform with a state.
Reducer
As we defined actions, we need to define the way we will react to them.
To do this we need to implement Reducer
interface.
So the idea is really simple behind this signature.
You define a pure function (method) reduce
that will apply an action to a state to compute a new state.
Let’s check what we have here:
- Two pure functions that perform particular actions (copied from our original implementation)
- Method
reduce
that takes our state and action. This method does a few things:- Dispatch the action by switching on
action.type
- Extract payload per each action if necessary
- Compute new state and return it
- Return the previous state if action is not dispatched (
action.type
is unknown for this Reducer)
- Dispatch the action by switching on
We defined our state reducer. As you can see we still keep the principle of using only pure functions to compute a new state. That means that this class is still well testable.
Note: our implementation of
reduce
method contain some unsafe operations like type casting. Do not worry about this boilerplate code now, as we will get rid of it using annotations later.
Using a Store
After we created our Reducer, we can create our state container – Store
.
Creating Store
We do it by calling static method Store.create
Store<List<TodoItem>> todoStore =
Store.create(new TodoReducer(), Collections.emptyList());
Store.create
takes at least two arguments:
- State reducer
- Initial state
Store API
Once created, Store
API is really simple:
- To access a state at any moment method
getState()
is used - State can be changed by calling
dispatch(action)
- If necessary, app can register
StateChangeListener
to be notified when state updates by callingsubscribe
method
Having this in mind we can rewrite our MainActivity:
As you can see changes are minimal. The structure is the same. The main difference is action dispatching.
Reducing boilerplate
As you can see, to create state container we only need to define two things: Reducer and actions.
However, as I mentioned before, there is a bit of ‘untyped’ code in reducer and in action creators.
This is because we want to pack different data types into the Action.value
. Here is our Reducer again:
But actually, if we decompose the logic of reduce
method into sub-methods per each action (as we did before), the logic of reduce
becomes really straightforward.
So why do we need to write it every time?
Let computer do it for us!
And that is why Reductor contains Magic Annotation processor.
First, we need to ensure that reductor-processor
dependency is added.
Follow Installation for more information.
Then we need to simplify TodoReducer
as followed:
What did we do here?:
- Made class abstract and removed ‘reduce’ method.
Reductor processor will generate an implementation of
reduce
method in childTodoReducerImpl
class. - Annotated class with
@AutoReducer
annotation. - Annotated each action method with
@AutoReducer.Action
and provided Action type as annotation value - Added static factory method
create
to instantiate generated class
Nice! Our code is clean and fancy. All the boilerplate is written by a machine. But that’s only the half of the story. What about action creators? Reductor generates them too!
This full generated class out of our Reducer:
Now our actions and Reducer are type-safe, even though all of them are dispatched as Action
objects.
Conclusion
In this article, we used Reductor library to implement our TODO example app.
The main interaction point is Store
object which use user-defined Reducer
to change the state.
Reductor library was designed to bring Redux experience into the Java and Android world without sacrificing type safety.
That’s why there are such features as auto-generated @AutoReducer
.
In next article, we will cover how to compose a state and reducers with Reductor.
Stay tuned. To be continued…