How to update UI in coroutines in Kotlin 1.3

23,688

Solution 1

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.

Solution 2

private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

uiScope.launch {
            withContext(Dispatchers.IO) {
                //Do background tasks...
                withContext(Dispatchers.Main){
                    //Update UI
                }
            }
        }

Solution 3

If you're using coroutines-android you can use Dispatchers.Main
(gradle dependency is implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0")

network.getRoute(request,
        success = {
            withContext(Dispatchers.Main) {
                // update UI here
            }
        },
        fail = {
            // handle the exception
        }) 
Share:
23,688
Mohsen
Author by

Mohsen

Updated on March 25, 2020

Comments

  • Mohsen
    Mohsen about 4 years

    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

  • Mohsen
    Mohsen over 5 years
    Threre is no such thing as Dispatchers.Main in Kotlin 1.3
  • jguerinet
    jguerinet over 5 years
    @Mohsen There is indeed a Dispatchers.Main, see here. Make sure you include the Android dependency above in order to get its implementation.
  • Mohsen
    Mohsen over 5 years
    Thank you @jguerinet for clarification. Is there a difference between these two methods ? using withContext and runOnUiThread
  • EpicPandaForce
    EpicPandaForce over 5 years
    runOnUiThread runs the code on the next event loop and is not part of the coroutine anymore.
  • jguerinet
    jguerinet over 5 years
    To expand on @EpicPandaForce's answer, runOnUiThread is a method provided by Android, not by coroutines. It is therefore completely separate. If you intend on using coroutines, you should probably use them everywhere (and therefore use withContext). runOnUiThread() is less efficient in this case.
  • jguerinet
    jguerinet over 5 years
    Here's a Medium article explaining what runOnUiThread() actually does.
  • EpicPandaForce
    EpicPandaForce over 5 years
    the GlobalScope which is not meant for production use. I'm pretty sure there is nothing wrong with using the GlobalScope if you don't intend to cancel your job.
  • Marko Topolnik
    Marko Topolnik over 5 years
    This is just a patch on top of a wrong approach. The OP launches the coroutine in Dispatchers.Default and you suggest to immediately switch back to Dispatchers.Main. Why not recommend to launch in the correct context to begin with?
  • Mikhail Olshanski
    Mikhail Olshanski over 5 years
    @MarkoTopolnik you are right, I was careless and didn't pay enough attention into giving the best possible answer. I think OP should accept your answer instead of mine.
  • Marko Topolnik
    Marko Topolnik over 4 years
    While there are use cases where you want the coroutines to go on in the background, using GlobalScope opens you to coroutine leaks if anything in them gets stuck, as well as weird concurrency issues when one gets delayed and then another one doing the same thing launches. For example, if the operation is a DB write, the writes can get reordered and result in lost updates. I think this is not a fully solved problem yet.
  • EpicPandaForce
    EpicPandaForce over 4 years
    Sounds like I might want to run those writes on a dispatcher made from a single-threaded executor.
  • Marko Topolnik
    Marko Topolnik over 4 years
    But the writes are suspendable, they get interleaved.
  • EpicPandaForce
    EpicPandaForce over 4 years
    Well that's your own choice if you make it suspendable per item or not. I think the fact that Room made its insert DAO methods suspend fun-compatible was a mistake, as they should be strictly synchronous on a background thread.
  • Marko Topolnik
    Marko Topolnik over 4 years
    That sounds like a ham-handed solution to me. The proper way to get sequenced ops is an actor or similar, processing requests one at a time with no need for even a single background thread. Which again removes the need to write GlobalScope.launch anywhere, you just submit your task to the actor and move on.
  • EpicPandaForce
    EpicPandaForce over 4 years
    Not a single background thread? That's what dispatchers do: dispatch to other threads. I'd be surprised if actors magically made the UI not get blocked when you do a network request and it takes 5 seconds.
  • Marko Topolnik
    Marko Topolnik over 4 years
    Seems like we're talking past each other... you're talking about blocking IO and I'm talking about suspendable IO. For blocking IO you don't need coroutines in the first place. Just use a plain old ExecutorService.
  • Carlos Pérez Iruela
    Carlos Pérez Iruela over 3 years
    I was wondering how to load some background data and update the view just after the data loaded using coroutines. This really worked for me. Thanks!