A Responsive and Clean Android App with Kotlin Actors

le 23/10/2017 par Maxime Bonnet
Tags: Software Engineering

One crucial point that each front-end developer must face when developing an application is handling the switch from UI to background threads. Indeed, we can all agree on the fact that your application shouldn’t freeze during long running operations. The Android SDK even strictly prevents us from calling an API from the Main Thread. There are many ways to tackle this issue, for instance with RxJava, with a ThreadPool (check this out) … This article aims at proposing yet another asynchronism solution, using Kotlin actors. We’ll also investigate what actors bring to the table that other solutions do not.

Actors

Why ?

As said above, asynchronism is an issue we all have to face and multiple ways to approach it. As I don’t feel 100% satisfied by the solution I use daily, I wanted to investigate new possibilities. I first wanted to check Kotlin’s coroutines and decide if they were a viable solution. Simply put, coroutines can be seen as lightweight threads. To use them, you first need to add this to your build.gradle.

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.18"

During my experiments, I found that they offer a clean, simple and concise way to switch from the Main Thread to the background :

launch(CommonPool){
    /* Background computation */
}

And back to the Main Thread :

launch(UI){
    /* Update views */
}

The launch coroutine builder creates and launches a new coroutine in the given Context (CommonPool & UI). It’s only one of the many builders that you can use with coroutines. One other commonly used builder is async that you can use to implement an async / await kind of asynchronism. I then stumbled upon the actor builder that allows to build an actor model in Kotlin.

The actor model is quite popular in other languages like Erlang and Scala (with the Akka library). It’s an higher level model that allows us to write complex concurrent systems. Simply put, actors are lightweight processes that you can send message to.

Actors in Kotlin

In Kotlin, the actors are available through the experimental couroutines package. The “lightweight process” is implemented as a coroutine, and you can send messages asynchronously in a Channel (I picture it as a mailbox).

This is a basic actor code :

private val gradeActor = actor<Int>(CommonPool) {
        for (integer in channel) {
            when (integer) {
                in 0..9 -> computeLowGrade()
                in 10..20 -> computeHighGrade()
                else -> computeIllegalGrade()
            }
        }
    }

The basic thing to know about actor is that they have a mailbox, and you can send them messages. Kotlin guarantees that the actor only treats one message at a time, and that they will be treated in the order that they’ve been received. The expression actor<Int>(CommonPool){...} returns an actor that takes Integers as messages. The CommonPool denotes that all code of this actor will be executed in the CommonPool context, meaning in background, well away from the Main Thread. Indeed, an actor is really a kind of coroutine that exposes a SendChannel that you can send message to. That’s why we can use an actor as a way to achieve asynchronism. Once in the body of the actor, you have access to its channel. In the example above, we go through the channel and for each message (represented as an Integer), decide what to do with it depending on its value. Note how Kotlin’s when expression is useful here.

The Clean Architecture

Let’s have a look at the Clean Architecture, in which I want to incorporate actors.

Basics

Although probably not as popular as some mobile architectures like MVP or MVVM, Uncle Bob’s Clean Architecture is often used on Android and iOS application development. Resulting from the application of many clean code principles such as SOLID and separation of concerns, this architecture perfectly fits with large projects and development teams. In larger projects, the heavy boilerplate code that comes with this architecture gives us a template that makes it easier to read code, debug, and integrate new features. In this article we’ll focus on this implementation of the Clean Architecture :

Asynchronism

Whichever solution you decide to use to handle asynchronism, one thing is for sure : we switch to a background thread out of the Display, and back to the Main Thread out of the Presenter. This way, only the Display’s code is executed on the Main Thread. As it doesn’t contain any logic, the user experience is as good as possible. All heavy computation is done in the background.

Sample Application

I’ll use a simple application to present my experiments with actors. This application will consist of a counter to which you can increment or decrement. Note that the Clean Architecture will look way overkill for this example. I did not want to make things more complicated than what they needed to be to present actors.

Interfaces and basic implementation explanations

Controller

The Controller is called by the activity whenever the user interacts with the application. Here we handle the clicks on the two buttons. Implementation-wise, we do not handle any type of data, so there’s no type mapping needed in the data flow.

interface CountController {
    fun plusOne()
    fun minusOne()
}

Interactor

The Interactor is called by the Controller. It handles the business logic of the app. This is where we’ll store the current counter value. For simplicity sake, we’ll focus on the “display loop” and forget about the repository part of the architecture. In a real implementation we would store the in-memory counter in a repository.

interface CountInteractor {
    fun plusOne()
    fun minusOne()
}

Presenter

After changing the counter value, the Interactor wants to present it. We just need one method for this, and again, because it’s a very simple example, we do not need to map the data passed by the Interactor.

interface CountPresenter {
    fun presentCount(count: Int)
}

Display

This method will simply be called by the Presenter to update the TextView value.

interface CountDisplay {
    fun displayCount(count: Int)
}

Clean Actors

Now that we’ve seen the architecture that will be used for this example, let’s focus on fitting actors in the application.

A Controller

As stated earlier, we want to switch to a background thread straight out of the Display. We’ll try to fit actors in the Controller, and see how it goes.

We first need to adapt our interface. That’s where the sealed classes play an important part. We can set up our actor so that he receives a certain class of messages : the sealed class. Then, we declare all kind of messages that can be received as subclasses of this sealed class.

In this case, we’ve changed the CountController interface from :

interface CountController {
    fun plusOne()
    fun minusOne()
}

to

sealed class CountAction
class PlusAction : CountAction()
class MinusAction : CountAction()

interface CountController {
    fun handle(countAction: CountAction): Boolean
}

We just need to communicate with the Controller actor, which gives us the full Controller implementation:

class CountControllerImpl(private val interactor: CountInteractor) : CountController {

    private val actor = actor<CountAction>(CommonPool) {
        for (event in channel) {
            when (event) {
                is PlusAction -> interactor.plusOne()
                is MinusAction -> interactor.minusOne()
            }
        }
    }
    
    override fun handle(countAction: CountAction): Boolean {
        return actor.offer(countAction)
    }
}

Unit Testing

One of the Clean Architecture’s main focus is the testability of each component. That is why we do not want the Controller implementation to interfere with this. Let’s quickly see how you can test such a Controller :

@RunWith(MockitoJUnitRunner::class)
class CountControllerImplTest {

    @Mock private lateinit var interactor: CountInteractor
    private lateinit var controller: CountControllerImpl

    @Before
    fun setUp() {
        controller = CountControllerImpl(interactor, Unconfined)
    }

    @Test
    fun testPlusAction() {
        // When
        controller.handle(PlusAction())

        // Then
        Mockito.verify(interactor).plusOne()
    }

    @Test
    fun testMinusAction() {
        // When
        controller.handle(MinusAction())

        // Then
        Mockito.verify(interactor).minusOne()
    }
}

Those are basic tests using Mockito. Here I’m just verifying that the Interactor was called. They are kept simple, thanks to how the Controller is set up. Notice the second parameter Unconfined : I had to refactor the Controller constructor to define the actor coroutine’s context. The Unconfined value allows the actor to be executed in the same thread that the one in which he’s created. The actor’s code is now executed synchronously, which allows common testing. The Controller now looks like this:

class CountControllerImpl(private val interactor: CountInteractor, context: CoroutineContext = CommonPool) : CountController {

    private val actor = actor<CountAction>(context) {
        for (event in channel) {
            when (event) {
                is PlusAction -> interactor.plusOne()
                is MinusAction -> interactor.minusOne()
            }
        }
    }

    override fun handle(countAction: CountAction): Boolean {
        return actor.offer(countAction)
    }
}

I have defined the second parameter with a default value, if omitted, the Controller’s actor will execute in background, thanks to CommonPool.

The Activity

Now, the Interactor and the Presenter do not have anything particular, the actors do not have any impact on their implementation. But one interesting thing to focus on is the activity, and particularly two things : how the Controller is called, and how the UI is updated. First, since we’ve changed the interface earlier, our button listeners now looks like this :

override fun onCreate(savedInstanceState: Bundle?) {
        /* ... */
        plusOneButton.setOnClickListener { countController.handle(PlusAction()) }
        minusOneButton.setOnClickListener { countController.handle(MinusAction()) }
    }

Then, there’s one issue we’ve not yet tackled on the current implementation. We’ve switched from the Main Thread to the background in the Controller. But now, at the end of the loop, if we try to update a TextView content, an exception will be thrown because we’re trying to update a view from somewhere else than the UI Thread. We need a way to switch back to the UI Thread.

override fun displayCount(count: Int) {
        launch(UI) {
            numberValue.text = count.toString()
        }
    }

The launch(UI){...} block creates and launches a new coroutine in the UI context (remember the CommonPool context in which we launched the computation). This way, all code inside this block is executed on the Main Thread.

Capacities

One thing that actors offer that I haven’t yet discussed is a capacity. During its construction, we are able to describe how an actor handles its mailbox. Remember that the actor can only handle a message at a time. The basic predicate stating that the order in which the messages have been received will be the same as the one in which they’ll be executed will still be true. But we can change how we handle messages that arrive while the actor is busy computing the previous message. We have four options :

  • First, and default one is the RendezVousChannel :
private val actor = actor<CountAction>(CommonPool) { /*...*/ }

The actor will not accept any message while being occupied. In our case, if we quickly click on a button twice, only the first click will be handled.

  • Second, the ConflatedChannel :
private val actor = actor<CountAction>(CommonPool, capacity = Channel.CONFLATED) { /*...*/ }

While being busy, the actor will only remember the last message that it received.

  • Third, the LinkedListChannel :
private val actor = actor<CountAction>(CommonPool, capacity = Channel.UNLIMITED) { /*...*/ }

Here, we set up the actor with an unlimited buffer (the channel is a linked list). This way, we store every message that are received during computation, and the actor will handle them all.

  • Last, the ArrayChannel :
private val actor = actor<CountAction>(CommonPool, capacity = 5) { /*...*/ }

You may have guessed it already, in this last Channel implementation, the buffer has a limited size: only the 5 last messages will be handled.

Conclusion

Often, in this implementation of the Clean Architecture, the Controller does not play an important role. It often merely transforms data to a type comprehensible by the Interactor. That’s why I find it to be the best spot in the architecture to handle asynchronism. The actors do not add a lot of complexity to the class, and give an elegant way to tackle this issue. An extra benefit of this implementation is also the flexibility that is offered by the different capacities. You can really adapt the actor behaviour to your needs. Consider that since you may want to have different Controller behaviour on a same screen, you may need to decouple Displays and Controllers. This means multiple Controllers for a Display. You can check Uber’s Riblets Architecture for an example.

If you want to check the application code, it’s available on my GitHub here.