web-dev-qa-db-fra.com

Comment mettre à jour l'interface utilisateur dans les coroutines dans Kotlin 1.3

J'essaie d'appeler une API et lorsque mes variables sont prêtes, mettez à jour les composants de l'interface utilisateur respectivement.

C'est mon réseau singleton qui lance la 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)
            }

        })
    }
}

Et voici comment je l'appelle:

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

Et j'obtiens l'exception qui dit ne peut pas mettre à jour l'interface utilisateur à partir d'un thread autre que le thread d'interface utilisateur:

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

J'ai déjà essayé la solution this mais resume in Continuation<T> la classe est "obsolète" depuis Kotlin 1.3

10
Mohsen

Pour répondre à votre question immédiate, vous devez simplement lancer la coroutine dans le bon contexte:

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

Cependant, ce ne serait que la pointe de l'iceberg, car votre approche est la mauvaise façon d'utiliser les coroutines. Leur principal avantage est d'éviter les rappels, mais vous les réintroduisez. Vous enfreignez également la concurrence concurrentielle structurée meilleure pratique en utilisant le GlobalScope qui n'est pas destiné à la production utilisation.

Apparemment, vous disposez déjà d'une API asynchrone qui vous donne un Deferred<RoutesResponse> Sur lequel vous pouvez await. La façon de l'utiliser est la suivante:

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

Vous pouvez être troublé par le fait que je suggère d'avoir un bloc launch dans chaque rappel GUI où vous devez exécuter du code suspendable, mais c'est en fait la façon recommandée d'utiliser cette fonctionnalité. Il est strictement parallèle à l'écriture de Thread { ... my code ... }.start() car le contenu de votre bloc launch s'exécutera simultanément avec le code extérieur.

La syntaxe ci-dessus suppose que vous avez une variable scope prête qui implémente CoroutineScope. Par exemple, il peut s'agir de votre Activity:

class MyActivity : AppCompatActivity(), CoroutineScope {
    lateinit var masterJob: Job
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + masterJob

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        masterJob = Job()
    }

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

Notez comment coroutineContext définit le répartiteur de coroutine par défaut sur Dispatchers.Main. Cela vous permet d'utiliser la syntaxe simple launch { ... }.

8
Marko Topolnik

Si vous utilisez coroutines-Android, vous pouvez utiliser Dispatchers.Main
(la dépendance gradle est implementation "org.jetbrains.kotlinx:kotlinx-coroutines-Android:1.0.0")

network.getRoute(request,
        success = {
            withContext(Dispatchers.Main) {
                // update UI here
            }
        },
        fail = {
            // handle the exception
        }) 
6
Mikhail Olshanski