Kotlin Coroutines - правильный путь в Android

Я пытаюсь обновить список внутри адаптера, используя async, я вижу, что слишком много шаблонов.

Правильно ли вы используете Koutlin Coroutines?

можно ли это оптимизировать больше?

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

Ответы

Ответ 1

После того, как я боролся с этим вопросом в течение нескольких дней, я думаю, что самый простой и понятный асинхронный шаблон для действий Android с использованием Kotlin:

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

Единственными зависимостями Gradle для сопрограмм: kotlin-stdlib-jre7, kotlinx-coroutines-android.

Примечание. Используйте job.await() вместо job.join(), потому что await() отменяет исключения, но join() нет. Если вы используете join(), вам нужно будет проверить job.isCompletedExceptionally после завершения задания.

Чтобы начать одновременную перестройку, вы можете сделать это:

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

Или:

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

Ответ 2

Как запустить сопрограмму

В библиотеке kotlinx.coroutines вы можете запустить новую сопрограмму, используя либо функцию launch либо async функцию.

Концептуально, async - это как launch. Он запускает отдельную сопрограмму, которая представляет собой легкую нить, которая работает одновременно со всеми остальными сопрограммами.

Разница в том, что запуск возвращает Job и не несет никакого результирующего значения, в то время как async возвращает Deferred - легкое неблокирующее будущее, которое представляет обещание предоставить результат позже. Вы можете использовать .await() для отложенного значения, чтобы получить возможный результат, но Deferred также является Job, поэтому вы можете отменить его, если это необходимо.

Сопрограмма контекста

В Android мы обычно используем два контекста:

  • uiContext для отправки выполнения в основной поток UI Android (для родительской сопрограммы).
  • bgContext для отправки выполнения в фоновом потоке (для дочерних сопрограмм).

пример

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

В следующем примере мы собираемся использовать CommonPool для bgContext который ограничивает число параллельно работающих потоков значением Runtime.getRuntime.availableProcessors()-1. Поэтому, если задание сопрограммы запланировано, но все ядра заняты, оно будет поставлено в очередь.

Вы можете рассмотреть возможность использования newFixedThreadPoolContext или вашей собственной реализации кэшированного пула потоков.

запуск + асинхронный (выполнить задачу)

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
}

запуск + асинхронный + асинхронный (выполнить две задачи последовательно)

Примечание: задача 1 и задача 2 выполняются последовательно.

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
}

launch + async + async (выполнить две задачи параллельно)

Примечание: задача 1 и задача 2 выполняются параллельно.

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
}

Как отменить сопрограмму

Функция loadData возвращает объект Job который может быть отменен. Когда родительская сопрограмма отменяется, все ее дочерние элементы также рекурсивно удаляются.

Если функция stopPresenting была вызвана, когда dataProvider.loadData все еще находился в процессе, функция view.showData никогда не будет вызываться.

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
}

Полный ответ доступен в моей статье Android Coroutine Recipes

Ответ 3

Я думаю, вы можете избавиться от runOnUiThread { ... }, используя контекст UI для приложений Android вместо CommonPool.

Контекст UI предоставляется модулем kotlinx-coroutines-android.

Ответ 4

У нас также есть другой вариант. если мы используем Anko, то это выглядит как

doAsync { 

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

Добавьте зависимость для Anko в приложении gradle следующим образом.

compile "org.jetbrains.anko:anko:0.10.3"

Ответ 5

Как и sdeff, если вы используете контекст пользовательского интерфейса, код внутри этой сопрограммы будет выполняться по потоку пользовательского интерфейса по умолчанию. И, если вам нужно запустить инструкцию в другом потоке, вы можете использовать run(CommonPool) {}

Кроме того, если вам не нужно ничего возвращать из метода, вы можете использовать функцию launch(UI) вместо async(UI) (первая вернет a Job, а вторая a Deferred<Unit>).

Примером может быть:

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

Если вам нужна дополнительная помощь, я рекомендую вам прочитать главное руководство kotlinx.coroutines и, кроме того, руководство по сопрограммам + пользовательский интерфейс

Ответ 6

Все приведенные выше ответы kotlinx.coroutines, но мне было трудно найти правильный импорт для UI из kotlinx.coroutines, он конфликтовал с UI от Anko. это

import kotlinx.coroutines.experimental.android.UI

Ответ 7

Если вы хотите вернуть что-то из фонового потока, используйте async

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

Если фоновый поток ничего не возвращает

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

Ответ 8

Здесь правильный способ использования Kotlin Coroutines. Область действия сопрограмм просто приостанавливает текущую сопрограмму, пока все дочерние сопрограммы не завершат свое выполнение. Этот пример явно показывает нам, как child coroutine работает в parent coroutine.

Пример с объяснениями:

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
}

Надеюсь это поможет.