The release of Kotlin 1.1 brings many welcome improvements to the language: val
properties with custom getters can take full advantage of type inference and inlining; they introduced the T.also { it -> }
extension method, which is to T.apply {}
what T.let { it -> }
is to T.run {}
; and data
classes can inherit from interfaces and base classes, just to name a few. But the two biggest changes are full support for JavaScript as a compilation target (possibly a huge deal, if you’re a full stack dev and reluctantly work with JavaScript), and coroutines(!!!).
For me, working primarily on Android lately, the obvious winner for my attentions is coroutines. Android is inherently asynchronous and event-driven, with strict requirements as to which thread certain things can happen on. Couple this with often-cumbersome Java callback interfaces that can thwart Kotlin’s best efforts to make your coding patterns sane, and your code can turn into spaghetti right quick.
If you’ve ever heard the term “Callback Hell” but weren’t sure what it meant, it’s the situation where you have to serially execute and process the results of asynchronous services by nesting callback often several layers deep. Node is infamous for this issue (I heard it’s gotten better?). But Android and many other environments can suffer from the same plague to a greater or lesser degree, depending on exactly what you’re trying to do in your project.
Coroutines to the Rescue
This is exactly the kind of problem Kotlin coroutines handle incredibly well. In fact, the Kotlin team designed the core coroutine mechanisms to be simple as possible, to serve as application agnostic building blocks for many different models of concurrent and parallel computation. They’ve opted to ship a variety of such facilities as library modules, rather than tying the language to any one concept of asynchronous computation. For instance, you can build a library to very cleanly support the Actor Model, as in e.g. Erlang, or channels and goroutines, as in Go.
Before the final Kotlin 1.1 release was shipped, I tried to read the informal coroutine design description, but found it a little opaque without some context. It did provide a comprehensive survey of features, though, which primed me to better appreciate the coroutines guide. I recommend you read both documents, though if you have to pick just one, the guide is shorter and more approachable, with (I think) clearer example code.
Interestingly, Kotlin provides a “coroutine context” for working with the Java Swing UI library’s requirement that widgets only be diddled on the main UI thread, but nothing for Android. So before we really dig into coroutines on the mobile OS, we’ve got a little work to do.
Coroutines for Android
If you haven’t read the documents I linked above yet (you should), here’s just a tiny bit of background. When launching a coroutine, you can provide a context object to tune or modify how it executes. The standard library provides many different classes and objects for you to use, and they provide features as varied as dispatching coroutines to a thread pool and intercepting / redirecting continuations.
What we want specifically is a CoroutineContext
class that will let us do work on any arbitrary worker thread, but the results will always be returned to the UI thread, where we can process them sequentially like any regular, synchronous code. We’ll do this with a ContinuationInterceptor
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /** * @file AndroidCouroutineUtil.kt * @author btoskin <brigham@ionoclast.com> * * Copyright © 2017 Ionoclast Laboratories, LLC. */ package com.ionoclast.kotlin.coroutine import android.os.Handler import android.os.Looper import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CoroutineScope import kotlinx.coroutines.experimental.run import kotlin.coroutines.experimental.AbstractCoroutineContextElement import kotlin.coroutines.experimental.Continuation import kotlin.coroutines.experimental.ContinuationInterceptor import kotlin.coroutines.experimental.CoroutineContext /** * *Schedules continuations on the UI thread.* */ object AndroidUI : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor { override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = AndroidUiContinuation(continuation) } /** @private */ private class AndroidUiContinuation<in T>(val cont: Continuation<T>) : Continuation<T> { companion object { private val sMainHandler = Handler(Looper.getMainLooper()) } override val context get() = cont.context override fun resume(value: T) { if(Looper.getMainLooper() === Looper.myLooper()) cont.resume(value) else sMainHandler.post { cont.resume(value) } } override fun resumeWithException(exception: Throwable) { if(Looper.getMainLooper() === Looper.myLooper()) cont.resumeWithException(exception) else sMainHandler.post { cont.resumeWithException(exception) } } } |
The AndroidUI
object only has one job: it intercepts our coroutine continuations and wraps them in an instance of AndroidUiContinuation
. If we look at the definition of that class, we see all it’s really doing is forcing the resume methods to execute on the main UI thread. With this written, we can do things like this:
1 2 3 4 5 6 7 8 | launch(AndroidUI) { // requests could execute on any thread, but results will // always be delivered on the UI thread and handled in order val result = doSomeBackgroundThing() val result2 = doSomeOtherThing(result) textView.text = "$result, $result2" textView.requestFocus() } |
Coroutines for Networking
If you find yourself implementing REST APIs for Android regularly, I strongly recommend looking at Retrofit, if you’re not already familiar. It wraps up a lot of the boilerplate and low level details of dealing with network I/O and HTTP requests, so you can just concentrate on defining your server endpoints and modeling the data they return.
On previous projects I’ve used Retrofit version 1.9—relatively clean and pleasant, but with very Java-ish interface—to implement my ApiClient
class. Once your service interfaces are set up and integrated into the client class, you can write very straightforward networking code with a callback-based mechanism:
1 2 3 4 5 6 7 8 9 10 11 | ApiClient.someInterface.someApiCall(object : Callback<Type> { override fun success(data: Type, response: Response) { // these callback methods get called on the UI thread, so we // can update the view with results from the background thread. ... } override fun failure(error: RetrofitError) { ... } }) |
But what happens when we need to chain network calls, for example fetching a security token before making a login call, or pulling down a basic user object so we can get the IDs we need to fetch their full account details? Welcome to Callback Hell.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ApiClient.someInterface.someApiCall(object : Callback<Type> { override fun success(data: Type, response: Response) { ApiClient.someOtherInterface.otherApiCall(data.field, object : Callback<OtherType> { override fun success(data2: OtherType, response: Response) { ... } override fun failure(error: RetrofitError) { ... } }) } override fun failure(error: RetrofitError) { ... } }) |
As you can see, we’re only two levels deep, and this is already kinda hard to read. First of all, we’ve got curly braces inside of parentheses, which as a good Kotlin programmer should make you cringe just a little. But it’s unavoidable, since the interface has two methods you have to implement. Then we have nesting layers of call-success-call-success, and then cascading failure blocks as we fall out the other end of this monstrosity.
What we’d really like to be able to write is code that looks the way we naturally think about it: make a call, grab the result, make the next call, and so on.
1 2 3 4 5 6 7 | networkTask { val (response1, err1) = ApiClient.someApiCall(mApiJob) if (response1 == null || err1 != null) return val (response2, err2) = ApiClient.otherApiCall(mApiJob, response.data.field) ... } |
Just look at that. It’s gorgeous. No nesting. No explicit juggling threads or callbacks. No curly braces inside of parentheses!
There’s actually a good bit going on here, though. Right off the bat, there’s the networkTask
function. This is a coroutine builder. Basically, a builder runs the block that follows under a coroutine context, allowing the suspending and dispatching and intercepting we need. We haven’t covered the code for this one yet, but basically it’s just a little wrapper that launches the coroutine in the AndroidUI
context, so we can update the UI with any network responses we receive, anywhere within the block.
Then, notice we’re passing in an object called mApiJob
to our API calls now. A Job
represents a running coroutine; if we pre-allocate and hold onto one of these, and pass it around, it gives us a convenient handle to cancel()
the executing network request, e.g. in response to use input, or Android lifecycle events.
The next thing to notice is actually a really big deal. We’re just directly returning the result of the network call as if it were a simple function call. We get either the response data, or an error object (or possibly both). We’re simply checking for failure and bailing before any subsequent calls, but we could have much more involved error handling if we wanted, perhaps prompting the user to re-enter their password, or retrying if the network is being spotty.
But note that the UI thread is never blocked while waiting on network latency; our code is suspended while the network call happens asynchronously on another thread. In the mean time, the UI thread has moved along, running its event loop as normal. It’s only via the AndroidUI
/ AndroidUiContinuation
mechanism that our continuation is resumed on that thread, and we pick up right where we left off.
Bending Coroutines to Our Will
To make all this happen, I had to move from Retrofit 1.9 to the 2.x branch. Retrofit 2 is a big improvement in a lot of ways. It gives you a lot more flexibility and control over exactly how your calls are made, gives you more granularity regarding request cancellation, and generally feels cleaner and tighter.
That said, it feels like it offers a little less structure, requiring more work to implement your API. It also has a different philosophy regarding error handling in its callback interface. Rather than differentiating between success and failure in a general way, it now differentiates between “got a response from the server” and “didn’t get a response from the server”. Under 1.9, if the server returned error code 500 and barfed a huge mess of error messages as HTML instead of the clean JSON data you were looking for, it would run your failure()
callback, and you could decide how to handle it there. Under 2.0, though, a 500 is still a valid server response, so instead of calling the new onFailure()
callback method, it will call onResponse().
So now what used to be your success block has to have additional error handling code in it.
Problematic.
What would be better would be to use the synchronous version of the call interface, and handle concurrency ourselves. We can avoid blocking the calling thread by dispatching requests to the CommonPool
coroutine context, which will maintain a pool of worker threads for us to use as we see fit. The output of such a call is either the generic Retrofit type Response<T>
(where T is your data model, or whatever the JSON response is expected to decode to). If there’s any kind of networking issue, it will preempt this by throwing an exception which we can catch and manipulate in any way that’s useful. Whichever result we end up with, we simply return it like any other normal function call, and it gets handed right back to the calling code when the coroutine is resumed on the UI thread.
Better!
So what does this all look like? The whole thing is about 30 lines of actual code to set up an abstract base class with the response wrappers and coroutine infrastructure. The final size of a client implementation based on this will depend on how many Retrofit service interfaces you create, and how many API endpoints you define in each one. But it should scale pretty linearly, which is decent for a quick first pass at rethinking how we write our network code.
Here’s the base class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | @file:Suppress("EXPERIMENTAL_FEATURE_WARNING") // for coroutines package com.ionoclast.kotlin.net import android.util.Log import com.google.gson.GsonBuilder import com.ionoclast.kotlin.coroutine.AndroidUI import com.ionoclast.kotlin.coroutine.task import com.ionoclast.kotlin.serialization.DateDeserializer import com.sdge.emergencymanagement.BuildConfig import kotlinx.coroutines.experimental.* import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Call import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.io.IOException import java.lang.Exception import java.util.* import java.util.concurrent.TimeUnit fun networkTask(block: suspend CoroutineScope.()->Unit) = launch(AndroidUI, block = block) /** * *Abstract base class for Android REST API clients in Kotlin 1.1+.* * * To implement an API client using this class, create your endpoint interfaces as usual, * and wrap them in suspending functions that call `makeRequest(Job, Call<T>)`. * * To use these APIs, create a coroutine block that will continue on the UI thread, such * as by using the supplied `networkTask()` function. If you need to make sequential * calls, such as pulling a list of IDs and querying one or more with a second call, or * prefetching a token to use with another call, you can do that within the coroutine * block; the will be executed in order, with results returned on the UI thread as if * it were all synchronously executed. If you want multiple concurrent calls, they * can be spawned from within multiple coroutine blocks. * * You should pre-allocate a Job instance for your Fragment or Activity, and pass that * to your API calls, so they can all be cancelled appropriately with the Android app * lifecycle, on user request, after a timeout, etc. * * @see Retrofit * @see makeRequest * @see networkTask * @see Job * * @author btoskin <brigham@ionoclast.com> */ abstract class AbstractRestClient { companion object { private val TAG = AbstractRestClient::class.java.simpleName } data class Response<out T>(val result: T? = null, val err: Throwable? = null) class ApiError(msg: String, cause: Throwable? = null) : IOException(msg, cause) abstract val BASE_URI: String abstract val TIMEOUT_SECS: Long? protected val restClient by lazy { buildClientAdapter() } /** * *Executes the given `Call` in a coroutine on a worker thread, cancellable via the given `Job`.* * * This method is the secret sauce to implementing a clean and small API client. Once you have * a retrofit API service interface instance, just pass it in here, along with a `Job`. If * `cancel()` is called on the job, the background coroutine will be canceled, which will * terminate the network request. * * @see Retrofit.create * @see Job */ protected suspend fun <T> makeRequest(job: Job, call: Call<T>) = try { val response = task(CommonPool + job) { call.execute() } response.takeIf { it.isSuccessful } ?.let { Response(it.body()) } ?: Response(err = ApiError("API Error (${response.code()}): ${response.errorBody()}")) } catch (cancelled: CancellationException) { Log.d(TAG, "API call cancelled: ${call.request().url()}") call.cancel() Response<T>(err = cancelled) } catch (ex: Exception) { Response<T>(err = ApiError("API call failed: ${call.request().url()}", ex)) } /** * *Builds a reasonable default API adapter.* * * Initializes the following networking components and features: * * Uses OkHttp for the underlying HTTP client implementation * * Registers a verbose logging interceptor in debug builds * * Sets connection and read timeouts as specified by the class implementation * * Registers a GSON decoder to inflate your models, with a `Date` helper class * * Binds the adapter to the base URI as specified by the class implementation * * You may override this method, if you need to add more hooks, or more fine-grained * control over how the components are configured. * * @see TIMEOUT_SECS * @see BASE_URI * @see DateDeserializer */ protected fun buildClientAdapter(): Retrofit { val logger = HttpLoggingInterceptor() logger.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE val client = OkHttpClient.Builder() .addInterceptor(logger) .connectTimeout(TIMEOUT_SECS ?: 0L, TimeUnit.SECONDS) .readTimeout(TIMEOUT_SECS ?: 0L, TimeUnit.SECONDS) .build() val gson = GsonBuilder().registerTypeAdapter(Date::class.java, DateDeserializer()).create() val retrofit = Retrofit.Builder() .baseUrl(BASE_URI) .client(client) .addConverterFactory(GsonConverterFactory.create(gson)) .build() return retrofit } } |
And your client implementation might look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | object ApiClient : AbstractRestClient() { private val TAG = ApiClient::class.java.simpleName override val BASE_URI = "https://www.example.com/api/v1/" override val TIMEOUT_SECS: Long? = 60L private val mUserApi by lazy { restClient.create(UserSummaryApi::class.java)!! } private val mEcommApi by lazy { restClient.create(EcommerceApi::class.java)!! } private val mMediaApi by lazy { restClient.create(MediaApi::class.java)!! } suspend fun getLoggedInUser(job: Job) = makeRequest(job, mUserApi.getLoggedInUser()) suspend fun getOrderHistory(job: Job, id: String) = makeRequest(job, mEcommApi.getOrderHistory(id)) suspend fun getMediaLibarary(job: Job) = makeRequest(job, mMediaApi.getMediaLibrary()) suspend fun getMediaDetails(job: Job, id: String) = makeRequest(job, mMediaApi.getMediaDetails(id)) } |
As you can see, the main things the ApiClient
implementation does is handle instantiating your various service interfaces, and wraps up pushing calls to those interfaces through the makeRequest()
method. And we use it just like you would hope:
1 2 3 4 5 6 7 8 9 10 | networkTask { val (user, err1) = ApiClient.getLoggedInUser(mApiJob) if (user == null || err1 != null) return err1.message val (purchases, err2) = ApiClient.getPurchaseHistory(mApiJob, user.id) ... val (mediaLib, err3) = ApiClient.getMediaLibrary(mApiJob) if (mediaLib == null || err3 != null) return err3.message val (details, err4) = ApiClient.getMediaDetails(mApiJob, mediaLib[0].id) ... } |
It looks and acts like sequential, synchronous code, but Kotlin takes care of suspending and resuming, and managing threads, with just a little help from us.
Code is available here.