Create a Clean-Code App with Kotlin Coroutines and Android Architecture Components — Part 2

All About Actors

Marek Langiewicz
EL Passion Blog
9 min readOct 26, 2017

--

This is the second blog post about using Kotlin Coroutines and Android Architecture Components in one simple weather app.

Please read the first part if you haven’t already:

The most important part of our app is the MainModel. Here we have the actor that actually decides when to request some data from network, when to use cached data, when to push new state (which is observed by our activity).

To understand how it works, at first let’s dive into actors idea in general.

05 Actor

There is a whole theory about concurrent computations with actors as universal primitives. You can read more about it on Wikipedia.

Basically, actor is a kind of “worker” that can sequentially do some work based on messages he waits for. The messages are sent via “mailbox” which in Kotlin is implemented as a channel.

So let’s dive into channels first :-)

05.1 Channels Basics

The channel concept is similar to a blocking queue but it has suspending operations instead of blocking ones, and it can be closed. We can send messages to it and receive messages from it. Both of these operations can cause a suspension. The send operation can suspend when the channel is full (it will resume when someone calls receive on this channel). The receive operation can suspend when the channel is empty (it will resume when someone calls send on this channel).

In Kotlin the Channel<E> interface extends both:

  • SendChannel<E> base interface
    which defines the suspend fun send(element: E) method
  • ReceiveChannel<E> base interface
    which defines the suspend fun receive(): E method

These base interfaces also contain other not so essential (and not suspendable) methods:

  • SendChannel.offer(element: E): Boolean
    adds an element to channel if possible, or return false if the channel is full
  • SendChannel.close(cause: Throwable? = null)
    closes the channel
  • ReceiveChannel.poll(): E?
    returns an element if available, or null if channel is empty
  • And some other (less important) methods…

Channels’ Types

RendezvousChannel<E> — it does not contain any internal buffer

  • every send invocation is suspended until someone else invokes receive (unless there is some receive operation already suspended)
  • every receive invocation is suspended until someone else invokes send (unless there is some send operation already suspended)

ArrayChannel<E> — it does contain a fixed size buffer

  • send is suspended only if the buffer is full
  • receive is suspended only if the buffer is empty

LinkedListChannel<E> — it contains linked-list buffer with unlimited capacity

  • send is never suspended — but it may throw OutOfMemoryError
    (well, everything can throw it if we run out of memory)
  • receive is suspended when buffer is empty

ConflatedChannel<E> — it buffers at most one element and conflates all subsequent send invocations

  • send is never suspended, but new element overrides any old element waiting to be received
  • receive is suspended when buffer is empty

There are other types too, but we will skip them. Check the Coroutines Guide if you want to know more.

We will use the ConflatedChannel as our actor‘s mailbox.

05.2 Actors Basics

At many occasions it’s useful to have a coroutine with attached channel to send or receive some elements to/from other parts of the system. Usually, this coroutine only sends elements to the channel and those elements are received by the other part of the system (so our coroutine is a “producer”). Or the other way around: our coroutine only receives elements from a channel and someone else is sending elements to us through this channel (so our coroutine is a consumer, or rather an “actor).

To implement such scenarios we could create a suspendable function that takes such channel as a parameter and just uses it in its code. Then we would instantiate appropriate channel and use some coroutine builder to call our suspendable function with created channel. If it sounds like a bit too much to do, there are two special coroutine builders in kotlinx.coroutines library for two most common scenarios (when we use channels as “mailboxes”):

  • produce — Creates a coroutine with attached channel that coroutine itself uses to send elements into. The object returned from this coroutine implements the ReceiveChannel interface, so the user can consume elements produced by this coroutine.
  • actor — Creates a coroutine with attached channel that coroutine itself uses to receive elements from. The object returned from this coroutine implements the SendChannel interface, so the user can send elements to this coroutine.

The actor coroutine builder implementation in kotlinx.coroutines looks like this:

https://github.com/Kotlin/kotlinx.coroutines/...../Actor.kt#L82

Let’s analyze its parameters:

context — The context for created coroutine
(same as in all other coroutine builders).

capacity — This number defines what type of channel
will be created as a “mailbox” for this actor:

  • RendezvousChannel
    capacity = 0
  • LinkedListChannel
    capacity = Channel.UNLIMITED
  • ConflatedChannel
    capacity = Channel.CONFLATED
  • ArrayChannel
    capacity = required (fixed) buffer size

start — The coroutine start option
(same as in all other coroutine builders).

block — The coroutine code.
Here we provide the actual actor’s code.

The most important parameter is the last one. This is where we define what our actor will actually do. As you can see it’s a suspending function with receiver type: ActorScope. This scope (besides extending the usual CoroutineScope) extends the ReceiveChannel. Thanks to that we have convenient access to our “mailbox” from inside our coroutine code. We can even use an ordinary for loop to iterate over elements sent to us. This loop will call the receive operation, so it will suspend the coroutine if the receive operation suspends.

Don’t worry if it sounds complicated — it will all be clear once we look at the actual code of the actor in our app. However, before that, let’s look at one last thing — type returned from the coroutine builder: ActorJob. The ActorJob interface extends the Job interface — as all objects returned from coroutine builders, but it also extends the SendChannel interface. This way user can easily send elements to our actor using the send operation. User can also try to send elements to the channel using other non-suspendable operation: offer. It just returns false instead of suspending if it cannot send new element to this channel. This is what we will do in our app.

05.3 Our First Actor

Now, after dealing with some theory, we are ready to analyze the main actor in our app.

https://github.com/elpassion/crweather/…../MainModel.kt

There are three important things we can see in the actor coroutine builder invocation (line 27):

  • The type of our actor’s mailbox is the Action. It means that the user can send actions to our actor through its mailbox channel. Currently, there is only one possible action type: SelectCity, but this setup shows how it could be extended to other user actions.
  • We use the android specific UI coroutine dispatcher as a coroutine context — this ensures that all code of this actor will be executed on the android main thread. It allows us to easily change the app state by invoking the MutableLiveData.setValue function in actor’s code.
  • We could use the usual CommonPool coroutine dispatcher too, but then we would have to use MutableLiveData.postValue to change the app state.
  • We use the Channel.CONFLATED as a “mailbox” channel type, so new user actions replace any unprocessed action. In our case, this is exactly what we want because if we haven’t selected city requested by the user yet and the user now wants to select another city — we should forget the old action and process the new one as soon as we are ready.

Finally, let’s look at the actor’s code:

It is just a for loop iterating through “this”. As we remember “this” (receiver) of the actor’s code is the ActorScope which extends the ReceiveChannel. The ReceiveChannel type has defined iterator, so we can use it in for loops. Every iteration invokes the receive operation and possibly suspends if the mailbox channel is empty. So this is our first suspension point (IDE marks every suspension point with a green line crossing gray arrow). Iteration ends when the channel is closed. Inside the for loop we just check the type of the received action (currently it is always SelectCity) and do appropriate work.

Now, we have to implement the SelectCity user action. First, we change two parts of the app state immediately: we set the city LiveData value to the city name requested by user (so our view — MainActivity — displays new city as selected one), and the loading LiveData value to true (so our view displays some kind of progress indicator).

And finally, we try to gather the data for new selected city. I guess this line (nr 21/20) is the most important line in the app :-) First, we look for (fresh) charts for given city in the cache. The helper extension function on cache: getFreshCharts returns null if no fresh charts are found, and if that’s the case: we make a network call by invoking the getNewCharts suspension function. This is our second and last suspension point in the actor’s code. After it resumes we set the state: charts LiveData value to new charts (also in line 21/20) and we set the loading LiveData value to false (line 16). If the getNewCharts suspension point resumes with an exception, we catch it and display the error message via message LiveData.

There is a minor issue related to these messages — can you guess?
hint: think about device rotations :-)

The suspend fun getNewCharts — besides calling our carefully prepared suspend fun getCityCharts from Repository — also saves new charts for a particular city in the cache.

That’s virtually all business logic of our app implemented in clean and concise way thanks to actors (and coroutines in general).

Notice that we mutate external state from within the actor’s code. In general, the actor model of concurrent computation forbids any shared state in favour of implementing all communication via actor’s mailboxes.

If you’d like to know more about the actors, I’ve really enjoyed watching this video: The Actor Model, with Erik Meijer, Carl Hewitt, and Clemens Szyperski

The last part of the app is just a view. It is responsible for displaying current app state, and it forwards user actions to the main app model (which we’ve just discussed). You can read about view layer implementation in my next blog post:

Tap the 👏 button if you found this article useful!

About the Author
Marek is an Android Developer at EL Passion. You can find him on GitHub or at his website.

Find EL Passion on Facebook, Twitter and Instagram.

--

--