Bringing Redux to Kotlin
Our today’s story isn’t typical. We’ll talk about a bit unusual architecture for the mobile world. Yet, we hope that you will find our insights useful and implement described here practices during next app development in Kotlin.
Short historical reference: in 2014 Facebook developed its Flux architecture. A year later, a new architecture, Redux, appeared on its basis and won great popularity among developers. It was designed to simplify the work with complex and branched systems of possible application states and create apps that are easy to test and have no problem with running in different environments.
Speaking of now, Redux has proved its suitability and that’s why we want to share with you our insights on this topic.
📚 Redux: Main Concepts of the Architecture
The Redux architecture is not the most complicated one. Actually, there is just a few basic concepts and features that you should be aware of before putting it into practice. It’s also important to figure out how all the elements are interconnected to fold a complete and deep understanding of the Redux architecture. And that’s what we’re going to do right now.
State is an object that holds a minimum required amount of information to construct a screen. In other words, current screen characteristics are recorded in a state and can be easily displayed to users.
For the better comprehension, let’s find some real-life situations that match this description. If we draw an analogy between a screen and a pie, then the possible pie “states” are: the pie is baked; the pie is sprinkled with powdered sugar; the pie is half-eaten; the pie is stale, and others. These are some static states that provide us with information which is true for anybody (if you and I look at the half-eaten pie, we both will see only the half of it but not the whole).
Yet, if we come back to coding and imagine some hypothetical ToDo app, its state may look like this:
As you could understand from our example, the state is a static object, yet, it can change. And to introduce these changes, we’ll have to dispatch an action.
Actions are the objects that describe changes in the state. These payloads of data are the only source of information for the store (we’ll come to this a bit later) and a key element that allows producing a new state from the previous one.
Back to our pies. In this case, actions are the changes that transformed the previous state into the new one. For example, you had a baked pie (previous state), then you sprinkled it with powdered sugar (action) and got a pie with powdered sugar on the top (new state). Later, you took this pie (previous state), ate half of it (action) and then there is only half of your pie left (new state). That’s the simplified model of how actions and states are interrelated.
Having every change described as an action is very convenient for developers since it provides with a clear understanding of what’s going on in the app.
Using Kotlin, actions in a ToDo app can be written like this:
However, states and actions are just objects that require some function to interact with each other. And that’s when a reducer comes into play.
Reducer is a function that ties two previous objects (a state and actions) together and returns a new updated state that includes these changes.
The point is that actions include information about some changes but not about their consequences. That’s, actually, the task for reducers.
So, if you have a whole cake (previous state) and then eat half of it (action), it’s the reducer who will tell you “well, now you have only half of the cake left” (new state).
How does it work in practice? Let’s take a look at the reducer in our hypothetical ToDo app written in Kotlin:
But ToDo apps like the one above are usually simple and don’t have too many objects (actions, for example). But what is to be done when we’re considering complicated apps where a lot of different actions take place? For this purpose, we advise splitting reducers so each of them manages only some specific slice of the state. That’s what we call reducer composition and consider as the fundamental pattern of creating mobile apps using Redux.
So, the reducer from the code above may be split into 2:
Finally, there is only one core concept left to review.
This object fulfills several important tasks:
- stores the state;
- provides access to the states as well as allows to update them;
- notifies listeners about changes of the state.
Note that you can have only a single store. For splitting data handling logic, you should use reducer composition that we mentioned in the previous paragraph.
Now let’s come back to our ToDo app prototype and find out how we managed to write the store for it. Here is the implementation example in Kotlin:
So, these were our short review of the 4 main concepts related to the Redux architecture. They perfectly describe the main idea behind Redux and offer us a deeper understanding of app’s lifecycle. If in a simplified form lifecycle of a mobile app looks like this:
After we apply what we’ve just learned about Redux core concepts, our outline can be more detailed and specific:
However, in order to successfully apply Redux in practice, you should also get to know basic principles of its work. So let’s find out what Redux is all about.
🏆 3 Main Principles of Redux
Compliance with these principles is the key to successful use of the Redux architecture in mobile applications.
Principle # 1: Single source of truth
The main idea: the state of your app or screen is stored in an object tree within a single store.
A single state tree is a great advantage as it allows inspecting and debugging an app with no extra effort. Moreover, with the single source of truth the state from your server can be easily serialized and hydrated into the client one. This also opens up opportunities for implementing specific functionality - for example, Undo/Redo.
Principle # 2: State is read-only
The main idea: the state cannot be changed in any other way than by emitting the action.
The one and only way to change the system is through actions. Network callbacks, system events or views can’t directly influence the state - they can only initiate actions. These actions are centralized and processed one by one in a strict order, which makes it easier to log, store, serialize and replay them for different purposes.
Principle # 3: Changes are made with pure functions
The main idea: for specification of state tree transformation pure reducers are used.
The workflow of pure reducers is simple as that: they combine the previous state with an action and return a new updated state back to the user. Because reducers are just functions, you can easily manage them: pass additional data, control the order of their usage and so on.
🤔 Why Use Redux in Your App?
The Redux architecture allows solving many different development issues.
For example, an application state is usually spread out through the whole codebase. And that can be a great problem since over time we no longer understand what happens and why, how and when states are changed. Therefore, it’s difficult to add new features or reproduce bugs. However, Redux brings the state into a more streamlined form and makes it behavior more predictable.
Another issue is cascade updates that complicate the structure of apps and slow them down. Yet, do you remember the “single source of truth” principle? So this single store manages data flow to all other views. As a result, the hierarchy inside the app becomes more horizontal (we have one center that sends data to all other objects).
Predictable state mutations are the additional advantage of using Redux. Since all the updates are done one by one in a strict order, you always can foretell what state you’re going to receive in the end.
To sum up, the Redux architecture isn’t typical for the mobile world but it has enough advantages to make your app development easier and smoother. With its help you’ll be able to work with branched and complex systems in a simplified way. Let’s briefly remember main advantages and disadvantages of Redux:
- Improved Safety (no mutations together with asynchronicity).
- Good Testability.
- Consistent development flow (State - Actions - Reducer - View).
- State isn’t spread out.
- Common architecture across platforms.
- It takes some time to learn.
- You’ll have to write more code than usual.