How to update UI in coroutines in Kotlin 1.3
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
})
Mohsen
Updated on March 25, 2020Comments
-
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
inContinuation<T>
class is "deprecated" since Kotlin 1.3 -
Mohsen over 5 yearsThrere is no such thing as
Dispatchers.Main
in Kotlin 1.3 -
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 over 5 yearsThank you @jguerinet for clarification. Is there a difference between these two methods ? using
withContext
andrunOnUiThread
-
EpicPandaForce over 5 yearsrunOnUiThread runs the code on the next event loop and is not part of the coroutine anymore.
-
jguerinet over 5 yearsTo 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 usewithContext
).runOnUiThread()
is less efficient in this case. -
jguerinet over 5 yearsHere's a Medium article explaining what
runOnUiThread()
actually does. -
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 over 5 yearsThis 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 toDispatchers.Main
. Why not recommend to launch in the correct context to begin with? -
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 over 4 yearsWhile 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 over 4 yearsSounds like I might want to run those writes on a dispatcher made from a single-threaded executor.
-
Marko Topolnik over 4 yearsBut the writes are suspendable, they get interleaved.
-
EpicPandaForce over 4 yearsWell 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 over 4 yearsThat 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 over 4 yearsNot 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 over 4 yearsSeems 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 over 3 yearsI 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!