Kotlin Coroutines in Android — Part 7

A small DSL for Android apps development

Andrea Bresolin
Kin + Carta Created
8 min readApr 5, 2019

--

In Android apps development we’re likely to use a subset of all the features available in the coroutines world. We tend to apply the same patterns to equivalent situations. We’ve also seen in the previous part that we need to handle some tricky situations with the exceptions propagation. It would be good if we could just focus on efficient and fast apps development without caring too much about details and make sure, at the same time, that our code is reliable. This is a good case to define a small DSL (Domain-Specific Language) specifically for Android apps development.

The key features of our coroutines DSL

Let’s define the key features that our coroutines DSL must have:

  • easy to use
  • clear terminology
  • cover almost all the common coroutines usage scenarios
  • let the developer use plain coroutine methods when a specific scenario is not covered by the DSL
  • concise code
  • testable

Terminology

It’s important to introduce some clear terminology to avoid confusion while using our DSL. The plain coroutines methods can be confusing and people often ask what’s the difference between them because their names don’t necessarily express it well enough for everybody. We can see them as low level methods that provide all the coroutines features, but in our specific case, Android apps development, we can define some terminology that adapts well to our particular domain.

The Presenter and ViewModel are our main entry points to the coroutines world because they are the main classes that implement CoroutineScope. We’ll have some methods inside them that need to start some coroutines. Each coroutine can be decomposed into multiple other tasks and we want all of them to complete before completing the coroutines.

We can define that:

  • the methods in the Presenter and ViewModel can start jobs
  • each job is made of multiple tasks
  • each task can have sub-tasks

Let’s visualise it:

Here we have 2 jobs each one having multiple tasks. Some tasks have sub-tasks as well. We can now start defining some methods to achieve the desired DSL.

DSL helpers

We need some helper methods that represent the jobs and tasks in our DSL. We can have both tasks that run sequentially or in parallel, so we need to make a distinction between them. We also need to be able to specify the dispatcher for running our methods because they can run on the main UI thread or on background threads depending on our requirements. The helper methods must treat cancellation and exceptions propagation correctly according to what we’ve discussed in the previous parts dedicated to these two topics.

Given the above conditions, here are our helper methods:

startJob() starts a job that can be made of multiple tasks.

startTask() starts a sequential task that suspends the parent job/task until it’s done and returns a result.

startTaskAsync() starts a parallel task. We can have multiple parallel tasks running at the same time and wait for their result through their Deferreds. The name ending with the Async suffix is a convention that we have with coroutines. Each method that returns a Deferred (so each method that can run in parallel with others) should have a name that ends with the Async suffix.

All the above methods allow passing a CoroutineContext. This is useful to specify the dispatcher that must be used to run each one.

startJob() and startTaskAsync() also allow passing a CoroutineScope as the parent scope. This is very important to make the structured concurrency work. Each coroutine method must be executed in the appropriate scope to handle the cancellation and the exceptions propagation correctly. We must preserve the parent-child relationship of the coroutines.

DSL methods

Now that we have our helpers, we can define our main DSL methods. Given that we have only three coroutine dispatchers provided out of the box by the coroutines library (Main, Default and IO), we can write our DSL such that we can avoid explicitly passing the dispatchers as parameters. We could also define our own custom dispatchers of course, but given that the already available ones cover basically every use case in a typical app, we don’t need any other. In case we need others, we can add more DSL methods to cover them.

Here are our main DSL methods:

They make use of the helpers and explicitly specify the dispatchers internally. The reason to explicitly state which dispatcher we want for any job/task is that, in case of refactoring, we want to make sure that our coroutine code is going to always run in the expected dispatcher without having to worry about the coroutine contexts inheritance policies. If you think that specifying the dispatchers multiple times might be inefficient as opposed to just inheriting the parent dispatcher when it’s the same, you don’t have to worry. If two coroutines share the same dispatcher, the coroutines machinery can decide to make the child coroutine run in the same thread as the parent one without context switch if that thread can still be available for the child. This happens also in case of different dispatchers that use background threads like Default and IO. They represent different thread pools, but a single thread can be reused across them for efficiency. This is an efficiency improvement that we automatically have with coroutines, but wouldn’t be automatic with asynchronous programming making use of plain threads.

The jobs methods and the async tasks methods are defined as extensions of CoroutineScope because they internally use launch() and async() which are extensions of CoroutineScope as well. The sequential tasks methods are defined as simple suspend functions because they internally use withContext() which is a simple suspend function as well. By keeping consistency with the coroutines library methods, we make sure that our own methods will be composable and working in the same way as the standard ones.

DSL in action

We have all the pieces to finally start using our DSL. What does it look like when used inside an Android app?

Let’s take a ViewModel as an example (a Presenter would look exactly the same):

aMethod() and anotherMethod() are part of the ViewModel interface and they can be called by the view (the same would be true with a Presenter). Both of them start a new job that can have some tasks inside.

As you can see, our DSL methods are composable and let us avoid writing all the code that is actually needed to handle the coroutines properly because we’ve hidden it in our DSL methods and helpers. This allows the developer to just focus on the requirements of a development task instead of dealing multiple times with the details related to the coroutines. We also have more concise code and it’s less likely that the developer is going to make mistakes while dealing with the common coroutines scenarios. Note that in case a scenario is not covered by our DSL methods, we can still use the plain coroutines methods inside our DSL because we always have a CoroutineScope in each code block and the DSL is defined consistently with the behaviour of the underlying coroutines methods.

Testing

We’ve said that one of the requirement of our DSL is that it has to be testable. How can we make it testable if each dispatcher is defined inside the DSL methods and not passed as an argument? We’ll talk about testing extensively later in this series, but for this specific case, we can just have a preview of what we’re going to do later.

We can define a singleton that specifies the dispatchers for our app like this:

Then we can replace the dispatchers in our DSL methods like this (here are a couple of methods, all the others will look similar):

For testing, our coroutines configuration will have all the dispatchers replaced with Unconfined.

In this way, we can keep our DSL concise and avoid passing the dispatchers everywhere as arguments. The default values of AppCoroutinesConfiguration are those needed to run the app and any replacement is useful just for testing. The above example implements a BeforeClass method of JUnit as a set up of the coroutines configuration and can be used in a base test class for example.

What’s next

We have the DSL and we’ve taken a quick look at how it could be used in a Presenter or ViewModel. In the next part, we’ll see how it could be used in an app with MVP and MVVM architectures that also makes use of Clean Architecture.

Get the source code

The source code for this series can be found on GitHub. It’s an example Android project that covers multiple cases. Download it and play with it.

Missed the other parts of this series?

If you’ve missed the other parts of the Kotlin Coroutines in Android series, take a look at the introduction and check the full list of topics.

--

--