As an Android engineer I’m happy to see how this field of software engineering is changing over time. Each year our set of tools and approaches evolve and improve. However there is one particular problem we fight every day, and each time we do it differently. Today we going to talk about state management.
Any android application stores and manages some state, either implicitly or explicitly. The more data application holds, the harder it gets to understand what’s going on. Especially when it lives in multithreading asynchronous environment.
For instance remember how many solutions exist to preserve state across orientation change, and neither of them is ideal. So are we doomed, or is there a better way? I believe there is at least one!
Another field of software engineering with very similar problems is frontend development. Nowadays SPA are client side applications which interact with the user, talk to the server, manage some state and do all of these asynchronously as well as android applications. So maybe they have some better tools in the toolbox? And it seems like they do.
- Single source of truth
- State is read-only
- Changes are made with pure functions
Reductor is basically my reimplementation of Redux in Java.
I’ll not focus on how Redux or Reductor is implemented in this article (but I’ll do it in next one). However, for better understanding the problem, I’ll provide a simple example here and show how we can improve our code without any of additional tools.
Learn by example
To show the idea I will use a simple example of Todo list (which is a canonical example in Redux community). We will create a simple application with a naive approach (using a lot of mutable states) and then refactor it to make it easier to understand and maintain.
Our Todo app will have few features:
- Displaying a list of Todo items with text and checkbox
- User can add Todo items to list
- User can change status of each item by switching the checkbox
Note: I’ll omit most of the android related boilerplate code to focus only on state management
Here is our model definition:
And class which will store and manage Todo items. Let’s call it
As you can see these classes are simple POJO with getters and setters. Now imagine how client code would look like to work with this state:
Ok, we have built our first implementation. This code is simple but fragile! Here are few obvious disadvantages:
- Manual adapter notification. In any place we change the state, we need to manually notify adapter to reflect changes in UI
- Data can be mutated when we do not expect this
- Not thread safe
- Hard to test
Let’s try to refactor this solution a bit
Instead of notifying adapter manually we can create a listener to notify an adapter automatically each time we change the state
- Added method
addListenerto register listener for state updates
- Notify all listeners each time we change the state
Now we can remove manual calls to
But in fact this code will not work as expected, because we made one bug:
TodoStore we notify the listeners by calling
setTodoItems, however, in our
we mutate the list we get from
As you can see it’s easy to introduce a bug, when working with the mutable state.
Therefore we need to fix it.
As we saw in the previous example we can still accidentally mutate our data without notifying listeners.
We can fix it by encapsulating all the logic of managing the state inside the
So now we have no other way to change the state except defined methods in
TodoStore… Well, almost. We can still
TodoItem and modify List of items received by
getTodoItems(), let’s fix them.
Here is immutable
TodoItem. Now we can get rid of getters and setters as we have final fields;
Also, we need to change implementation of
TodoStore to create new object every time we change the list
Note: instead of copying lists by myself I used Pcollections as an implementation of persistent/immutable collections.
Good! Now each time we get List of TODO items, we know that object is immutable, so we can be sure nobody can change it
out of the contract (methods defined in
TodoStore). That’s already better than our first naive implementation.
But we can go even further with that.
Our code still has some disadvantages. The main one: this code is not testable enough.
For instance, to test function
changeState we need to have at least one item in the list,
so in tests, we need to ‘pre-initialize’ the state first before calling the function.
Then we need to get list of items via
getTodoItems() to check if it satisfy test conditions.
In our case, it’s not possible without additional constructors/methods for test purpose.
But let’s take a small step back. We know that our data is immutable now as we always create new state instead of mutating previous one. Each time we want to change the state we take current state, compute new state and re-assign the variable. What if we extract this computation step as a pure function that takes current state and returns next state.
So in result, we will get this structure of
As we can see we extracted logic of transition from old to a new state into pure functions, so they can be tested easily. In fact, our public functions become pretty similar. All they do now:
- Compute a new state from the old state and additional data passed externally as parameters
- Reassign a state variable
- Notify listeners
At this point, we can stop refactoring, as this solution is already better than our initial naïve implementation. However, we managed to improve state management only for one particular dataset in our application – list of Todo items. In real life, our application will manage much more data. So…
Should we apply this approach for other datasets? – Yes!
Should we reimplement Store for each dataset? – No! We can try to generify our solution to use with any data.
But that’s not as easy as to extract a type parameter. We will work on it in the next part of this series. But if you are curious what we will talk about next time take a look into the implementation of Reductor.
Stay tuned. To be continued…