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-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(); };
如 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
库中,您可以使用 launch
或 async
函数启动新协程。
从概念上讲,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.coroutines
的 UI
的正确导入,它与 [=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
}
}
我正在尝试使用异步更新适配器内的列表,我可以看到样板太多。
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-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(); };
如 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
库中,您可以使用 launch
或 async
函数启动新协程。
从概念上讲,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.coroutines
的 UI
的正确导入,它与 [=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
}
}