How to update UI in coroutines in Kotlin 1.3
Asked Answered
C

3

43

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

Claudication answered 31/10, 2018 at 8:34 Comment(1)
resume is not deprecated, it just became an extension fun.Raama
R
37

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.

Raama answered 5/11, 2018 at 13:15 Comment(8)
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.Aircraftman
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.Raama
Sounds like I might want to run those writes on a dispatcher made from a single-threaded executor.Aircraftman
But the writes are suspendable, they get interleaved.Raama
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.Aircraftman
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.Raama
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.Aircraftman
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.Raama
M
35
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

uiScope.launch {
            withContext(Dispatchers.IO) {
                //Do background tasks...
                withContext(Dispatchers.Main){
                    //Update UI
                }
            }
        }
Mcmillian answered 24/3, 2020 at 15:41 Comment(3)
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!Newcomer
I think this is a good solution and worked for me, Thank youNickolai
This a good answer: concise and to the point!Basset
H
11

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
        }) 
Hausfrau answered 31/10, 2018 at 8:47 Comment(8)
Threre is no such thing as Dispatchers.Main in Kotlin 1.3Claudication
@Claudication There is indeed a Dispatchers.Main, see here. Make sure you include the Android dependency above in order to get its implementation.Simple
Thank you @Simple for clarification. Is there a difference between these two methods ? using withContext and runOnUiThreadClaudication
runOnUiThread runs the code on the next event loop and is not part of the coroutine anymore.Aircraftman
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.Simple
Here's a Medium article explaining what runOnUiThread() actually does.Simple
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?Raama
@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.Hausfrau

© 2022 - 2024 — McMap. All rights reserved.