Kotlin Coroutines在Android中正确的方式

时间:2017-03-31 03:46:59

标签: android async-await kotlin coroutine

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

使用Kotlin 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()}
        }
    }

9 个答案:

答案 0 :(得分:34)

经过几天的挣扎,我认为使用Kotlin的Android活动最简单明了的async-await模式是:

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

答案 1 :(得分:24)

如何启动协程

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

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

不同之处在于,发布会返回Job并且不会带来任何结果值,而async会返回Deferred - 一个轻量级的非阻塞未来,代表一个承诺稍后提供结果。您可以对延迟值使用.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或您自己的缓存线程池实现。

启动+异步(执行任务)

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
}

启动+ 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
}

启动+ 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返回可能被取消的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

中提供了完整的答案

答案 2 :(得分:8)

我认为您可以使用runOnUiThread { ... }上下文替代UI来删除CommonPool

UI模块提供- local_action: command rsync -av /root/owned/dir/ {{ item }}:/root/owned/dir/ with_items: "{{ groups['group'] }}" when: inventory_hostname != "{{item}}" become: no 上下文。

答案 3 :(得分:5)

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

doAsync { 

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

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

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

答案 4 :(得分: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()
    }
}

如果您需要更多帮助,我建议您阅读main guide of kotlinx.coroutines,另外还要阅读guide of coroutines + UI

答案 5 :(得分:1)

以上所有答案都是正确的,但我很难从UI找到kotlinx.coroutines的正确导入,但UIAnko有冲突。 其

import kotlinx.coroutines.experimental.android.UI

答案 6 :(得分:0)

如果你想从后台线程返回一些东西,请使用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
}

答案 7 :(得分:0)

这是使用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
}

希望这会有所帮助。

答案 8 :(得分:0)

请附带Kotlin Coroutines和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
}
}
相关问题