在Android中正确使用Kotlin协程

53

我正在尝试使用异步更新适配器中的列表,但是我发现有太多样板代码。

使用Kotlin协程的方式是否正确?

还能进一步优化吗?

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

7
注意:在协程稳定版本的API更改后,大多数答案都是无效的。 - aksh1618
9个回答

46

在经过多日的挣扎之后,我认为使用Kotlin编写Android活动的最简单和清晰的异步等待模式是:

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-jre7kotlinx-coroutines-android

注意:使用job.await()代替job.join(),因为await()会重新抛出异常,但join()不会。如果使用join(),则需要在作业完成后检查job.isCompletedExceptionally

要启动并发的retrofit调用,您可以这样做:

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

14
请注意,这基本上与非静态的AsyncTask做相同的事情,并具有相同的潜在问题。您可以“触发”它,但不能“忘记”它,因为它在最后与您的Activity交互。我建议您在onStart()中启动协程,并在onStop()中取消它,以避免在Activity不可见时执行工作并防止在Activity被销毁后更新视图。另一个解决方案是将协程移到Loader或ViewModel(来自Architecture组件)。 - BladeCoder
这是关于潜在生命周期问题的非常好的观点。我同意所有协程(作业)都应该添加到某种类型的集合中,以便可以在onStop()中进行适当的清理。我还在响应用户操作(按钮点击)时使用这种“fire-and-forget”方法。感谢您的评论和建议。 - KTCO
是的,这对于Android应用程序来说不太好。请尝试https://proandroiddev.com/android-coroutine-recipes-33467a4302e9。 - user25

41

如何启动一个协程

kotlinx.coroutines 库中,您可以使用 launchasync 函数启动新的协程。

从概念上讲,asynclaunch 相似。它启动一个独立的协程,这是一个轻量级线程,与所有其他协程并发工作。

区别在于,launch 返回一个 Job,不带任何结果值,而 async 返回一个 Deferred - 一个轻量级非阻塞 future,表示稍后提供结果的承诺。 您可以在延迟值上使用 .await() 来获取其最终结果,但是 Deferred 也是一个 Job,因此如果需要,可以取消它。

协程上下文

在 Android 中,我们通常使用两个上下文:

  • uiContext 将执行调度到 Android 主线程 UI 线程 (对于父协程)
  • 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或自己实现缓存线程池。

launch + async (执行任务)

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
}

启动 + 异步 + 异步 (依次执行两个任务)

注:task1 和 task2 会依次执行。

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
}

启动 + 异步 + 异步(并行执行两个任务)

注意:task1 和 task2 会并行执行。

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 对象,可以被取消。当父协程被取消时,它的所有子协程也会被递归地取消。

如果在 dataProvider.loadData 仍在进行中时调用了 stopPresenting 函数,则函数 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”中找到。


9
我认为你可以使用Android应用程序的UI上下文,而不是CommonPool来摆脱runOnUiThread { ... }UI上下文由kotlinx-coroutines-android模块提供。

6
我们还有另一种选择。如果我们使用Anko库,那么它看起来像这样。
doAsync { 

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

在您的应用gradle中添加Anko依赖项,如下所示。
implementation "org.jetbrains.anko:anko:0.10.5"

我能以某种方式获取异步任务的进度吗? - user5307594
1
这个答案增加了一个额外的依赖项 - anko,目前版本为0.10.8。我相信kotlinx.coroutines已经足够实现OP所要求的功能,特别是在版本1.0.1中。 - mmBs
我们可以在ViewModel中使用Anko Async吗?还是它只能在Activity或Fragment中使用? - Sumit Kumar

3
如果您想从后台线程返回某些内容,请使用异步(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
}

我应该如何取消任务并像在 AsyncTask 上一样选择是否以良好的方式取消任务或使用线程中断?同时,我该如何检查它是否已被取消,以停止执行其操作? 参考链接:https://developer.android.com/reference/android/os/AsyncTask#cancel(boolean) - android developer

3

就像sdeff所说的,如果你使用UI上下文,协程中的代码将默认在UI线程上运行。而且,如果你需要在另一个线程上运行指令,你可以使用run(CommonPool){}

此外,如果你不需要从方法中返回任何东西,你可以使用函数launch(UI)代替async(UI)(前者将返回一个Job,后者将返回一个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的主要指南,此外还有协程与UI的指南


1
我找不到 withContext 方法,它是从哪里来的?我的 Kotlin 版本是 1.2.71。 - Felipe Belluco

2

以上所有答案都是正确的,但我曾经在找到来自kotlinx.coroutinesUI引入时遇到了困难,它与AnkoUI发生了冲突。

import kotlinx.coroutines.experimental.android.UI

这个已经被弃用了,你能帮忙找一个可用的吗? - clementiano
请看:https://github.com/Kotlin/kotlinx.coroutines实现 "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" 实现 "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" - Kenneth Argo

1
这是使用Kotlin协程的正确方式。Coroutine scope会暂停当前协程,直到所有子协程执行完毕。本例明确展示了子协程父协程内部的工作原理。
附带解释的示例:
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
}

希望这有所帮助。

1
请查看使用 Kotlin 协程和 Retrofit 库进行远程 API 调用的实现,附在此处。
import android.view.View
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.test.nyt_most_viewed.NYTApp
import com.test.nyt_most_viewed.data.local.PreferenceHelper
import com.test.nyt_most_viewed.data.model.NytAPI
import com.test.nyt_most_viewed.data.model.response.reviews.ResultsItem
import kotlinx.coroutines.*
import javax.inject.Inject

class MoviesReviewViewModel @Inject constructor(
private val nytAPI: NytAPI,
private val nytApp: NYTApp,
appPreference: PreferenceHelper
) : ViewModel() {

val moviesReviewsResponse: MutableLiveData<List<ResultsItem>> = MutableLiveData()

val message: MutableLiveData<String> = MutableLiveData()
val loaderProgressVisibility: MutableLiveData<Int> = MutableLiveData()

val coroutineJobs = mutableListOf<Job>()

override fun onCleared() {
    super.onCleared()
    coroutineJobs.forEach {
        it.cancel()
    }
}

// You will call this method from your activity/Fragment
fun getMoviesReviewWithCoroutine() {

    viewModelScope.launch(Dispatchers.Main + handler) {

        // Update your UI
        showLoadingUI()

        val deferredResult = async(Dispatchers.IO) {
            return@async nytAPI.getMoviesReviewWithCoroutine("full-time")
        }

        val moviesReviewsResponse = deferredResult.await()
        this@MoviesReviewViewModel.moviesReviewsResponse.value = moviesReviewsResponse.results

        // Update your UI
        resetLoadingUI()

    }
}

val handler = CoroutineExceptionHandler { _, exception ->
    onMoviesReviewFailure(exception)
}

/*Handle failure case*/
private fun onMoviesReviewFailure(throwable: Throwable) {
    resetLoadingUI()
    Log.d("MOVIES-REVIEWS-ERROR", throwable.toString())
}

private fun showLoadingUI() {
    setLoaderVisibility(View.VISIBLE)
    setMessage(STATES.INITIALIZED)
}

private fun resetLoadingUI() {
    setMessage(STATES.DONE)
    setLoaderVisibility(View.GONE)
}

private fun setMessage(states: STATES) {
    message.value = states.name
}

private fun setLoaderVisibility(visibility: Int) {
    loaderProgressVisibility.value = visibility
}

enum class STATES {

    INITIALIZED,
    DONE
}
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接