Kotlin 协程在 Android 中的正确方式

Kotlin Coroutines the right way in Android

我正在尝试使用异步更新适配器内的列表,我可以看到样板太多。

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

我认为您可以通过对 Android 应用程序使用 UI 上下文而不是 CommonPool.

来摆脱 runOnUiThread { ... }

UI 上下文由 kotlinx-coroutines-android 模块提供。

在为这个问题苦苦挣扎了几天之后,我认为使用 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

要开始并发 改造调用,您可以这样做:

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

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

此外,如果您不需要 return 方法中的任何内容,您可以使用函数 launch(UI) 而不是 async(UI)(前者将 return 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()
    }
}

如果您需要更多帮助,我建议您阅读 main guide of kotlinx.coroutines and, in addition, the guide of coroutines + UI

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

doAsync { 

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

像这样在您的应用程序中添加 Anko 的依赖项 gradle。

implementation "org.jetbrains.anko:anko:0.10.5"

如何启动协程

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

从概念上讲,async 就像 launch。它启动一个单独的协程,这是一个与所有其他协程同时工作的轻量级线程。

区别在于launch returns a Job不携带任何结果值,而async returns a Deferred - a light- weight 代表承诺稍后提供结果的非阻塞未来。您可以对延迟值使用 .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

在下面的示例中,我们将对 bgContext 使用 CommonPool,这将并行线程数 运行 限制为 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
}

launch + async + async(顺序执行两个任务)

注意: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
}

launch + async + async(并行执行两个任务)

注意: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 returns 一个可以被取消的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

如果你想return后台线程中的某些东西使用异步

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

如果后台线程没有return任何东西

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

以上所有答案都是正确的,但我很难找到 kotlinx.coroutinesUI 的正确导入,它与 [=14] 的 UI 冲突=]. 它的

import kotlinx.coroutines.experimental.android.UI

这是使用 Kotlin 协程的正确方法。协程作用域只是暂停当前协程,直到所有子协程都完成执行。这个例子明确地向我们展示了 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
}

希望对您有所帮助。

请在附件中找到使用 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
}
}