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.
Here is how Redux works (from here):
The whole idea is really simple:
Storeobject holds and maintains the state
- To change the state application can dispatch actions to
- For each action
Storecompute new state using pure function f(s, a) (called
Storereplace 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!
First of all, we need to add Reductor as a dependency. Follow Installation for more details.
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
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.
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:
typeis String action type we just defined above
valueis 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.
As we defined actions, we need to define the way we will react to them.
To do this we need to implement
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)
reducethat takes our state and action. This method does a few things:
- Dispatch the action by switching on
- Extract payload per each action if necessary
- Compute new state and return it
- Return the previous state if action is not dispatched (
action.typeis 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
reducemethod 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 –
We do it by calling static method
Store<List<TodoItem>> todoStore = Store.create(new TodoReducer(), Collections.emptyList());
Store.create takes at least two arguments:
- State reducer
- Initial state
Store API is really simple:
- To access a state at any moment method
- State can be changed by calling
- If necessary, app can register
StateChangeListenerto be notified when state updates by calling
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.
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
reducemethod in child
- Annotated class with
- Annotated each action method with
@AutoReducer.Actionand provided Action type as annotation value
- Added static factory method
createto 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
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
In next article, we will cover how to compose a state and reducers with Reductor.
Stay tuned. To be continued…