How to update UI in coroutines in Kotlin 1.3

Mohsen picture Mohsen · Oct 31, 2018 · Viewed 14.3k times · Source

I'm trying to call an API and when my variables are ready, update UI components respectively.

This is my Network singleton who is launching the coroutine:

object MapNetwork {
    fun getRoute(request: RoutesRequest,
                 success: ((response: RoutesResponse) -> Unit)?,
                 fail: ((throwable: Throwable) -> Unit)? = null) {
        val call = ApiClient.getInterface().getRoute(request.getURL())

        GlobalScope.launch(Dispatchers.Default, CoroutineStart.DEFAULT, null, {

            try {
                success?.invoke(call.await())
            } catch (t: Throwable) {
                fail?.invoke(t)
            }

        })
    }
}

And this is how I call it:

network.getRoute(request,
            success = {
                // Make Some UI updates
            },
            fail = {
                // handle the exception
            }) 

And I get the Exception that says can't update UI from any thread other than UI thread:

com.google.maps.api.android.lib6.common.apiexception.c: Not on the main thread

I already tried this solution but resume in Continuation<T> class is "deprecated" since Kotlin 1.3

Answer

Marko Topolnik picture Marko Topolnik · Nov 5, 2018

To answer your immediate question, you must simply launch the coroutine in the correct context:

val call = ApiClient.getInterface().getRoute(request.getURL())
GlobalScope.launch(Dispatchers.Main) {
    try {
        success?.invoke(call.await())
    } catch (t: Throwable) {
        fail?.invoke(t)
    }
}

However, this would be just the tip of the iceberg because your approach is the wrong way to use coroutines. Their key benefit is avoiding callbacks, but you're re-introducing them. You are also infringing on the structured concurrency best practice by using the GlobalScope which is not meant for production use.

Apparently you already have an async API that gives you a Deferred<RoutesResponse> that you can await on. The way to use it is as follows:

scope.launch {
    val resp = ApiClient.getInterface().getRoute(request.getURL()).await()
    updateGui(resp)
}

You may be distressed by the fact that I'm suggesting to have a launch block in every GUI callback where you must execute suspendable code, but that is actually the recommended way to use this feature. It is in a strict parallel to writing Thread { ... my code ... }.start() because the contents of your launch block will run concurrently to the code outside it.

The above syntax assumes you have a scope variable ready which implements CoroutineScope. For example, it can be your Activity:

class MyActivity : AppCompatActivity(), CoroutineScope by MainScope {

    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
}

The MainScope delegate sets the default coroutine dispatcher to Dispatchers.Main. This allows you to use the plain launch { ... } syntax.