41

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

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
Mohsen
  • 2,121
  • 3
  • 18
  • 25

3 Answers3

36

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.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436
  • 5
    `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. – EpicPandaForce Nov 05 '18 at 13:17
  • 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. – Marko Topolnik Dec 16 '19 at 07:51
  • Sounds like I might want to run those writes on a dispatcher made from a single-threaded executor. – EpicPandaForce Dec 16 '19 at 08:22
  • But the writes are suspendable, they get interleaved. – Marko Topolnik Dec 16 '19 at 11:11
  • 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. – EpicPandaForce Dec 16 '19 at 12:42
  • 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. – Marko Topolnik Dec 16 '19 at 13:35
  • 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. – EpicPandaForce Dec 16 '19 at 13:38
  • 1
    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`. – Marko Topolnik Dec 16 '19 at 13:52
33
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

uiScope.launch {
            withContext(Dispatchers.IO) {
                //Do background tasks...
                withContext(Dispatchers.Main){
                    //Update UI
                }
            }
        }
X-Black...
  • 1,376
  • 2
  • 20
  • 28
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
        }) 
jguerinet
  • 1,429
  • 1
  • 14
  • 21
  • 1
    Threre is no such thing as `Dispatchers.Main` in Kotlin 1.3 – Mohsen Oct 31 '18 at 08:50
  • 1
    @Mohsen There is indeed a `Dispatchers.Main`, see [here](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html). Make sure you include the Android dependency above in order to get its implementation. – jguerinet Oct 31 '18 at 10:22
  • Thank you @jguerinet for clarification. Is there a difference between these two methods ? using `withContext` and `runOnUiThread` – Mohsen Oct 31 '18 at 11:02
  • 1
    runOnUiThread runs the code on the next event loop and is not part of the coroutine anymore. – EpicPandaForce Oct 31 '18 at 13:17
  • 3
    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 Oct 31 '18 at 13:50
  • Here's a [Medium article](https://medium.com/@yossisegev/understanding-activity-runonuithread-e102d388fe93) explaining what `runOnUiThread()` actually does. – jguerinet Oct 31 '18 at 13:52
  • 1
    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? – Marko Topolnik Nov 05 '18 at 13:24
  • @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. – Mikhail Olshanski Nov 05 '18 at 13:35