【问题标题】:Kotlin Coroutines the right way in AndroidKotlin Coroutines 在 Android 中的正确方式
【发布时间】:2017-08-25 05:13:36
【问题描述】:

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

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

【问题讨论】:

  • 注意:大部分答案因协程稳定版API变化而无效。

标签: android async-await kotlin coroutine


【解决方案1】:

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

【讨论】:

  • 请注意,这基本上与非静态 AsyncTask 执行相同的操作,但存在相同的潜在问题。您可以“解雇”它,但不能“忘记”它,因为它最终会与您的 Activity 交互。我建议您在 onStart() 中启动协程并在 onStop() 中取消它,以避免在 Activity 不可见时执行工作,并防止在 Activity 销毁后更新视图。另一种解决方案是将协程移动到 Loader 或 ViewModel(来自架构组件)。
  • 关于潜在的生命周期问题,这是一个很好的观点。我同意所有协程(作业)都应该添加到某种类型的集合中,因此可以在 onStop() 中进行适当的清理。我还使用这种“即发即弃”的方法来响应用户操作(按钮单击)。感谢您的评论和建议。
  • 是的,它不适合 Android 应用。试试proandroiddev.com/android-coroutine-recipes-33467a4302e9
【解决方案2】:

如何启动协程

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

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

不同之处在于,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
}

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 返回一个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

【讨论】:

    【解决方案3】:

    我认为您可以通过在 Android 应用程序中使用 UI 上下文而不是 CommonPool 来摆脱 runOnUiThread { ... }

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

    【讨论】:

      【解决方案4】:

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

      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"
      

      【讨论】:

      • 我能以某种方式获取异步任务的进度吗?
      • 这个答案增加了额外的依赖——anko,目前在版本中。 0.10.8。我相信kotlinx.coroutines 足以实现这一目标,这是 OP 所要求的。特别是在版本1.0.1.
      • 我们可以在 ViewModel 中使用 Anko Async。或者只能在activity或fragment中使用。
      【解决方案5】:

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

      此外,如果您不需要从方法中返回任何内容,则可以使用函数launch(UI) 而不是async(UI)(前者将返回Job,后者将返回Deferred&lt;Unit&gt;)。

      一个例子可以是:

      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

      【讨论】:

      • 找不到withContext的方法,哪里来的?我的 kotlin 版本是 1.2.71
      【解决方案6】:

      如果你想从后台线程返回一些东西,使用异步

      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),以及如何检查它是否被取消, 停止做它做的事?
      【解决方案7】:

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

      import kotlinx.coroutines.experimental.android.UI
      

      【讨论】:

      • 这已经被弃用了,你能帮忙解决一下吗
      • 参见:github.com/Kotlin/kotlinx.coroutines implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9"
      【解决方案8】:

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

      希望这会有所帮助。

      【讨论】:

        【解决方案9】:

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

        【讨论】:

          猜你喜欢
          • 2021-10-13
          • 2020-09-13
          • 1970-01-01
          • 1970-01-01
          • 2020-12-14
          • 1970-01-01
          • 1970-01-01
          • 2019-09-02
          • 1970-01-01
          相关资源
          最近更新 更多