Kotlin Coroutines właściwy sposób w Androidzie

Próbuję zaktualizować listę wewnątrz adaptera za pomocą asynchronizacji, widzę, że jest za dużo boilerplate.

Czy to dobry sposób na używanie Koroutinów Kotlin?

Czy można to bardziej zoptymalizować?

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()}
        }
    }
Author: KTCO, 2017-03-31

7 answers

Po kilku dniach zmagania się z tym pytaniem, myślę, że najprostszym i najbardziej przejrzystym wzorcem asynchronicznym dla działań z Androidem przy użyciu Kotlina jest:

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.
    }
}

Jedynymi zależnościami Gradle dla coroutines są: kotlin-stdlib-jre7, kotlinx-coroutines-android.

Uwaga: Użyj job.await() zamiast job.join(), ponieważ await() zmienia wyjątki, ale join() nie. Jeśli używasz join(), musisz sprawdzić job.isCompletedExceptionally po zakończeniu zadania.

Aby rozpocząć współbieżne , możesz wykonać to:

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

Lub:

val jobs = arrayListOf<Deferred<Unit>>();
jobs += async(CommonPool) { /* Blocking call A */ };
jobs += async(CommonPool) { /* Blocking call B */ };
jobs.forEach { it.await(); };
 29
Author: KTCO,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-19 02:18:16

Jak uruchomić koroutine

W bibliotece kotlinx.coroutines możesz uruchomić nowy coroutine używając funkcji launch lub async.

Koncepcyjnie, async jest jak launch. Rozpoczyna oddzielną koronę, która jest lekką nicią, która działa jednocześnie ze wszystkimi innymi koronami.

Różnica polega na tym, że launch zwraca Job i nie niesie żadnej wartości wynikowej, podczas gdy async zwraca Deferred - lekką, nieblokującą przyszłość, która reprezentuje obietnica dostarczenia rezultatu później. Możesz użyć .await() na wartości odroczonej, aby uzyskać jej ostateczny wynik, ale Deferred jest również Job, więc możesz ją anulować w razie potrzeby.

Coroutine context

W Androidzie zwykle używamy dwóch kontekstów:]}
  • uiContext Aby wysłać wykonanie do głównego wątku Androida UI (dla rodzica) .
  • bgContext Aby wysłać wykonanie w wątku tła (dla dziecka koroutines) .

Przykład

//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

W poniższym przykładzie użyjemy CommonPool dla bgContext, które ograniczają liczbę wątków działających równolegle do wartości Runtime.getRuntime.availableProcessors()-1. Więc jeśli zadanie coroutine jest zaplanowane, ale wszystkie rdzenie są zajęte, zostanie ono ustawione w kolejce.

Możesz rozważyć użycie newFixedThreadPoolContext lub własnej implementacji buforowanej puli wątków.

Uruchom + async (execute task)

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
}

Launch + async + async (wykonaj dwa zadania kolejno)

Uwaga: task1 i task2 są wykonywane kolejno.

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
}

Uruchom + async + async (wykonaj dwa zadania równolegle)

Uwaga: task1 i task2 są wykonywane równolegle.

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
}

Jak anulować koroutine

Funkcja loadData zwraca obiekt Job, który może zostać anulowany. Gdy koroutine rodzica jest anulowany, wszystkie jego dzieci są również rekurencyjnie anulowane.

Jeśli stopPresenting funkcja została wywołana gdy dataProvider.loadData była jeszcze w toku, funkcja view.showData nigdy nie zostanie wywołana.

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
}

Pełna odpowiedź jest dostępna w moim artykule Android Coroutine Recipes

 15
Author: Dmytro Danylyk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-20 11:11:18

Myślę, że możesz pozbyć się runOnUiThread { ... } używając kontekstu UI dla aplikacji na Androida zamiast CommonPool.

Kontekst UI jest dostarczany przez moduł kotlinx-coroutines-android.

 7
Author: Steffen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-31 08:54:15

Mamy też inną opcję. jeśli używamy biblioteki Anko , wygląda to tak

doAsync { 

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

Dodaj zależność dla Anko w swojej aplikacji gradle w ten sposób.

compile "org.jetbrains.anko:anko:0.10.3"
 5
Author: Suraj Nair,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-29 14:46:56

Jak powiedział sdeff, jeśli użyjesz kontekstu UI, kod wewnątrz którego coroutine będzie domyślnie uruchamiany w wątku UI. Jeśli chcesz uruchomić instrukcję w innym wątku, możesz użyć run(CommonPool) {}

Ponadto, jeśli nie musisz zwracać nic z metody, możesz użyć funkcji launch(UI) zamiast async(UI) (Pierwsza zwróci a Job, a druga a Deferred<Unit>).

Przykładem może być:

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()
    }
}

Jeśli potrzebujesz więcej pomocy, polecam przeczytać główny przewodnik kotlinx.coroutines oraz dodatkowo przewodnik po coroutines + UI

 3
Author: David Olmos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-28 21:14:44

Jeśli chcesz zwrócić coś z wątku tła użyj asynchronizacji

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

Jeśli wątek tła nie zwraca niczego

launch(UI) {
   launch(CommonPool) {
      //do long running operation   
   }.await()
   //do stuff on UI thread
}
 1
Author: Rocky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-14 09:38:08

Wszystkie powyższe odpowiedzi są słuszne, ale ciężko mi było znaleźć odpowiedni import dla UI z kotlinx.coroutines, był sprzeczny z UI z Anko. Its

import kotlinx.coroutines.experimental.android.UI
 1
Author: Max,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-04-02 09:47:50