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
}
Надеюсь это поможет.