web-dev-qa-db-fra.com

Kotlin Coroutines dans la bonne direction Android

J'essaie de mettre à jour une liste à l'intérieur de l'adaptateur en utilisant async, je peux voir qu'il y a trop de passe-partout.

Est-ce la bonne façon d'utiliser Kotlin Coroutines?

cela peut-il être optimisé davantage?

fun loadListOfMediaInAsync() = async(CommonPool) {
        try {
            //Long running task 
            adapter.listOfMediaItems.addAll(resources.getAllTracks())
            runOnUiThread {
                adapter.notifyDataSetChanged()
                progress.dismiss()
            }
        } catch (e: Exception) {
            e.printStackTrace()
            runOnUiThread {progress.dismiss()}
        } catch (o: OutOfMemoryError) {
            o.printStackTrace()
            runOnUiThread {progress.dismiss()}
        }
    }
37
Sai Kiran

Après avoir lutté pendant plusieurs jours avec cette question, je pense que le modèle le plus simple et le plus clair pour l'attente asynchrone des activités Android utilisant Kotlin est:

override fun onCreate(savedInstanceState: Bundle?) {
    //...
    loadDataAsync(); //"Fire-and-forget"
}

fun loadDataAsync() = async(UI) {
    try {
        //Turn on busy indicator.
        val job = async(CommonPool) {
           //We're on a background thread here.
           //Execute blocking calls, such as retrofit call.execute().body() + caching.
        }
        job.await();
        //We're back on the main thread here.
        //Update UI controls such as RecyclerView adapter data.
    } 
    catch (e: Exception) {
    }
    finally {
        //Turn off busy indicator.
    }
}

Les seules dépendances de Gradle pour les coroutines sont: kotlin-stdlib-jre7, kotlinx-coroutines-Android.

Remarque: Utilisez job.await() à la place de job.join() car await() renvoie les exceptions, mais pas join(). Si vous utilisez join(), vous devrez vérifier job.isCompletedExceptionally À la fin du travail.

Pour lancer des appels simultanés de conversion, vous pouvez procéder comme suit:

val jobA = async(CommonPool) { /* Blocking call A */ };
val jobB = async(CommonPool) { /* Blocking call B */ };
jobA.await();
jobB.await();

Ou:

val jobs = arrayListOf<Deferred<Unit>>();
jobs += async(CommonPool) { /* Blocking call A */ };
jobs += async(CommonPool) { /* Blocking call B */ };
jobs.forEach { it.await(); };
37
KTCO

Comment lancer une coroutine

Dans la bibliothèque kotlinx.coroutines, Vous pouvez créer une nouvelle coroutine en utilisant la fonction launch ou async.

Conceptuellement, async est semblable à launch. Il commence une coroutine séparée qui est un fil léger qui fonctionne en même temps que toutes les autres coroutines.

La différence est que le lancement retourne un Job et ne porte aucune valeur résultante, alors que async renvoie un Deferred - un avenir léger non bloquant qui représente une promesse de fournir un résultat plus tard. Vous pouvez utiliser .await() sur une valeur différée pour obtenir son résultat final, mais Deferred est également un Job, vous pouvez donc l'annuler si nécessaire.

contexte Coroutine

Dans Android, nous utilisons généralement deux contextes:

  • uiContext pour répartir l'exécution sur le thread Android principal UI = [(pour la coroutine parent)).
  • bgContext pour répartir l'exécution dans le thread d'arrière-plan (pour les coroutines enfants).

Exemple

//dispatches execution onto the Android main UI thread
private val uiContext: CoroutineContext = UI

//represents a common pool of shared threads as the coroutine dispatcher
private val bgContext: CoroutineContext = CommonPool

Dans l'exemple suivant, nous allons utiliser CommonPool pour bgContext, ce qui limite le nombre de threads exécutés en parallèle à la valeur de Runtime.getRuntime.availableProcessors()-1. Donc, si la tâche de coroutine est planifiée, mais que tous les cœurs sont occupés, elle sera mise en file d'attente.

Vous pouvez envisager d'utiliser newFixedThreadPoolContext ou votre propre implémentation de pool de threads mis en cache.

lance + async (exécute la tâche)

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread

    val task = async(bgContext) { dataProvider.loadData("Task") }
    val result = task.await() // non ui thread, suspend until finished

    view.showData(result) // ui thread
}

lance + async + async (exécute deux tâches de manière séquentielle)

Remarque: task1 et task2 sont exécutés de manière séquentielle.

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread

    // non ui thread, suspend until task is finished
    val result1 = async(bgContext) { dataProvider.loadData("Task 1") }.await()

    // non ui thread, suspend until task is finished
    val result2 = async(bgContext) { dataProvider.loadData("Task 2") }.await()

    val result = "$result1 $result2" // ui thread

    view.showData(result) // ui thread
}

lance + async + async (exécute deux tâches en parallèle)

Remarque: task1 et task2 sont exécutés en parallèle.

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread

    val task1 = async(bgContext) { dataProvider.loadData("Task 1") }
    val task2 = async(bgContext) { dataProvider.loadData("Task 2") }

    val result = "${task1.await()} ${task2.await()}" // non ui thread, suspend until finished

    view.showData(result) // ui thread
}

Comment annuler une coroutine

La fonction loadData renvoie un objet Job qui peut être annulé. Lorsque la coroutine parent est annulée, tous ses enfants sont annulés de manière récursive.

Si la fonction stopPresenting a été appelée alors que dataProvider.loadData Était encore en cours, la fonction view.showData Ne sera jamais appelée.

var job: Job? = null

fun startPresenting() {
    job = loadData()
}

fun stopPresenting() {
    job?.cancel()
}

private fun loadData() = launch(uiContext) {
    view.showLoading() // ui thread

    val task = async(bgContext) { dataProvider.loadData("Task") }
    val result = task.await() // non ui thread, suspend until finished

    view.showData(result) // ui thread
}

La réponse complète est disponible dans mon article Android Coroutine Recipes

30
Dmytro Danylyk

Je pense que vous pouvez vous débarrasser de runOnUiThread { ... } en utilisant UI contexte pour Android au lieu de CommonPool.

Le contexte UI est fourni par le module kotlinx-coroutines-Android .

8
Steffen

Nous avons aussi une autre option. si nous utilisons la bibliothèque Anko , alors cela ressemble à ceci

doAsync { 

    // Call all operation  related to network or other ui blocking operations here.
    uiThread { 
        // perform all ui related operation here    
    }
}

Ajouter une dépendance pour Anko dans votre application gradé comme ceci.

compile "org.jetbrains.anko:anko:0.10.3"
5
Suraj Nair

Comme dit sdeff, si vous utilisez le contexte d'interface utilisateur, le code à l'intérieur de cette coroutine sera exécuté par défaut sur le thread d'interface utilisateur. Et, si vous avez besoin d'exécuter une instruction sur un autre thread, vous pouvez utiliser run(CommonPool) {}

De plus, si vous n'avez rien à renvoyer de la méthode, vous pouvez utiliser la fonction launch(UI) au lieu de async(UI) (la première retournera un Job et le ce dernier a Deferred<Unit>).

Un exemple pourrait être:

fun loadListOfMediaInAsync() = launch(UI) {
    try {
        withContext(CommonPool) { //The coroutine is suspended until run() ends
            adapter.listOfMediaItems.addAll(resources.getAllTracks()) 
        }
        adapter.notifyDataSetChanged()
    } catch(e: Exception) {
        e.printStackTrace()
    } catch(o: OutOfMemoryError) {
        o.printStackTrace()
    } finally {
        progress.dismiss()
    }
}

Si vous avez besoin d’aide supplémentaire, je vous recommande de lire le guide principal de kotlinx.coroutines et, en outre, le guide de coroutines + UI

3
David Olmos

Toutes les réponses ci-dessus sont exactes, mais j'avais du mal à trouver la bonne importation pour le UI de kotlinx.coroutines, il était en conflit avec UI de Anko. Ses

import kotlinx.coroutines.experimental.Android.UI
1
Max

Voici la bonne façon d'utiliser Kotlin Coroutines. Coroutine scope suspend simplement la coroutine actuelle jusqu'à ce que toutes les coroutines enfants aient terminé leur exécution. Cet exemple nous montre explicitement comment child coroutine travaille dans parent coroutine.

Un exemple avec des explications:

fun main() = blockingMethod {                    // coroutine scope         

    launch { 
        delay(2000L)                             // suspends the current coroutine for 2 seconds
        println("Tasks from some blockingMethod")
    }

    coroutineScope {                             // creates a new coroutine scope 

        launch {
            delay(3000L)                         // suspends this coroutine for 3 seconds
            println("Task from nested launch")
        }

        delay(1000L)
        println("Task from coroutine scope")     // this line will be printed before nested launch
    } 

    println("Coroutine scope is over")           // but this line isn't printed until nested launch completes
}

J'espère que cela t'aides.

0
ARGeo

Si vous voulez renvoyer quelque chose d'un fil de fond, utilisez async

launch(UI) {
   val result = async(CommonPool) {
      //do long running operation   
   }.await()
   //do stuff on UI thread
   view.setText(result)
}

Si le fil de fond ne renvoie rien

launch(UI) {
   launch(CommonPool) {
      //do long running operation   
   }.await()
   //do stuff on UI thread
}
0
Rocky