Developing Android apps with MVP/MVC or MVVM patterns helps you separate your business logic from interactions with the view layer and Android framework dependent classes. Creating unit tests for business logic and refactoring is also easier.
But when the codebase of app grows, presenters become more and more bloated, with lots of callbacks for async work and local variables for mutating state in different places. Flow of data and logic become tricky and especially hard to test.
In this post, I will explain how we can manage complex UI logic with The Elm Architecture.
The Elm Architecture –
So why ELM Architecture is more interesting?
TEA or ‘The ELM Architecture’ is an approach to building web applications, with several key aspects:
1. Unidirectional dataflow
2. Immutable state
3. Managed side effects
The core concept of TEA really boils down to only three core types or classes in OOP and three functions:
Model (State in Redux) —
This is the type for describing the state of your app or screen. I will refer from now on to it as State, because up to me this better expresses what it is used for, as the term Model has so many definitions and has became very bloated.
Msg (Message) Actions in Redux—
A base type for all events happening during interaction with UI (such as button click, text inputs, etc)
Cmd (Command) —
If you create Cmd, that means you want to execute a particular side effect (http request or other IO operation). When executed, the command will return new Msg with resulting data.
Function Update (reduce in Redux) —
This function Update takes Msg and State as input, and returns a pair of two values — new State and Cmd, or simply speaking, what side effect you want to execute for incoming Msg. The main aspect of this function is that it is a pure function. That means there must be no side effects inside this function.
Function View (render in Redux) —
It takes State as an input, and renders view (HTML in case of Elm) in declarative manner. I will name this function in Redux manner, as the term View is already heavily used in Android Framework.
Function Init —
Here we define our initial values for State, and, if necessary, return first Cmd, for initial HTTP request for example.
The best way to learn new things is to examine them by example. Let’s take a look into at a simple login screen, with input fields for login and password, and a button for sending http request. The following code snippets will be in Kotlin language, as this post is primarily addressed to Android Developers.
Now Why Kotlin!!!
Kotlin has several very powerful constructs, built in it’s type system.
Sealed class –
It is somewhat close implementation of Union Types from functional languages. They allow us to build class hierarchies in a very clear and concise way. Moreover, with sealed class you gain extra power of pattern matching with “when” expressions.
Data Classes –
Data classes allow us to create classes for expressing Msg and Cmd types with one liners, and, again, use the power of pattern matching.
TEA Concepts in Practice
The primary aspects are:
1. Immutable state
We keep our application state in one immutable class and we cannot mutate it.
data class LoginState(val login : String, val password : String, val auth : Boolean = false, val isLoading : Boolean = false) : State()
If we want to change some value in state, we create new state
loginState.copy(login = “name”)
All changes happen in function Update. This concept is often referred to as Single Source of Truth, meaning that we know, that if the state changes, this change happens only in one place.
2. Unidirectional Data Flow
For instance, a user types his username and password, and we need to express these interactions in terms of TEA.
data class LoginInput(val login : String) : Msg()
data class PasswordInput(val password : String) : Msg()
The messages come from the view, and are forwarded to the Update function
LoginInput(“R”) -> Update(state.copy(login=”R”)) -> Render(state)
LoginInput(“Rj”) -> Update(state.copy(login=”Rj”)) -> Render(state)
PasswordInput(“k”) -> Update(state.copy(password=”k”)) -> Render(state)
PasswordInput(“ku”) -> Update(state.copy(password=”ku”)) -> Render(state)
You can see that the data flow follows a cycle, going from the view (or from the outside world in the case of side effects) to the Update function, and then to the Render function.
3. Managed side effects
One of the greatest things in Elm(and hence in TEA) is the runtime’s management of side effects. If you need to do some asynchronous task, you just tell the Elm Runtime what to do, and what Msg to return with the result of this task. The Elm Runtime will do all the work and will return result to function Update.
How can we achieve this behavior in the Android multithreaded environment?
This is where RxJava comes in handy. More of that I will discuss in the next post. For now, I’ll just show how the cycle looks with a side effect:
data class AuthClick : Msg()
data class AuthResult(val token: String?, val error : Throwable?) : Msg()
data class Auth(val login: String, val password: String) : Cmd()
AuthClick() -> Update(state.copy(loading=true)) -> Render(state) -> (execute Auth) -> AuthResult(token) -> Update(state.copy(loading=false, token=authResult.token)
RxJava’s declarative style of controlling multithreading assists here.