Kotlin 协程提供了一个可供您编写异步代码的 API。通过 Kotlin 协程,您可以定义
CoroutineScope
,以帮助您管理何时应运行协程。每个异步操作都在特定范围内运行。
本主题中介绍的内置协程范围包含在每个相应架构组件的 KTX 扩展程序中。请务必在使用这些范围时添加相应的依赖项。
ViewModelScope
,请使用 androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01
或更高版本。LifecycleScope
,请使用 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01
或更高版本。liveData
,请使用 androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01
或更高版本。为应用中的每个 ViewModel
定义了 ViewModelScope
。如果 ViewModel
已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel
处于活动状态时才需要完成的工作,此时协程非常有用。
例如,如果要为布局计算某些数据,则应将工作范围限定至
ViewModel
,以便在ViewModel
清除后,系统会自动取消工作以避免消耗资源。
您可以通过 ViewModel 的 viewModelScope
属性访问 ViewModel
的 CoroutineScope
,如以下示例所示:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
为每个 Lifecycle
对象定义了 LifecycleScope
。在此范围内启动的协程会在 Lifecycle
被销毁时取消。您可以通过 lifecycle.coroutineScope
或 lifecycleOwner.lifecycleScope
属性访问 Lifecycle
的 CoroutineScope
。
以下示例演示了如何使用 lifecycleOwner.lifecycleScope
异步创建预计算文本:
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
即使 CoroutineScope
提供了适当的方法来自动取消长时间运行的操作,在某些情况下,您可能需要暂停执行代码块(除非 Lifecycle
处于特定状态)。例如,要运行 FragmentTransaction
,您必须等到 Lifecycle
至少为 STARTED
。对于这些情况,Lifecycle
提供了其他方法:lifecycle.whenCreated
、lifecycle.whenStarted
和 lifecycle.whenResumed
。如果 Lifecycle
未至少处于所需的最低状态,则会暂停在这些块内运行的任何协程。
以下示例包含仅当关联的 Lifecycle
至少处于 STARTED
状态时才会运行的代码块:
class MyFragment: Fragment {
init { // Notice that we can safely launch in the constructor of the Fragment.
lifecycleScope.launch {
whenStarted {
// The block inside will run only when Lifecycle is at least STARTED.
// It will start executing when fragment is started and
// can call other suspend methods.
loadingView.visibility = View.VISIBLE
val canAccess = withContext(Dispatchers.IO) {
checkUserAccess()
}
// When checkUserAccess returns, the next line is automatically
// suspended if the Lifecycle is not *at least* STARTED.
// We could safely run fragment transactions because we know the
// code won't run unless the lifecycle is at least STARTED.
loadingView.visibility = View.GONE
if (canAccess == false) {
findNavController().popBackStack()
} else {
showContent()
}
}
// This line runs only after the whenStarted block above has completed.
}
}
}
如果在协程处于活动状态时通过某种 when
方法销毁了 Lifecycle
,协程会自动取消。在以下示例中,一旦 Lifecycle
状态变为 DESTROYED
,finally
块即会运行:
class MyFragment: Fragment {
init {
lifecycleScope.launchWhenStarted {
try {
// Call some suspend functions.
} finally {
// This line might execute after Lifecycle is DESTROYED.
if (lifecycle.state >= STARTED) {
// Here, since we've checked, it is safe to run any
// Fragment transactions.
}
}
}
}
}
注意:尽管这些方法为使用
Lifecycle
提供了便利,但只有当信息在Lifecycle
的范围(例如预计算文本)内有效时才应使用它们。请记住,协程不会随着 Activity 重启而重启。
使用 LiveData
时,您可能需要异步计算值。例如,您可能需要检索用户的偏好设置并将其传送给界面。在这些情况下,您可以使用 liveData
构建器函数调用 suspend
函数,并将结果作为 LiveData
对象传送。
在以下示例中,loadUser()
是在其他位置声明的暂停函数。使用 liveData
构建器函数异步调用 loadUser()
,然后使用 emit()
发出结果:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
liveData
构建块用作协程和 LiveData
之间的结构化并发基元。
当 LiveData
变为活动状态时,代码块开始执行;
当 LiveData
变为非活动状态时,代码块会在可配置的超时过后自动取消。
如果代码块在完成前取消,则会在 LiveData
再次变为活动状态后重启;
如果在上次运行中成功完成,则不会重启。
请注意,代码块只有在自动取消的情况下才会重启。如果代码块由于任何其他原因(例如,抛出 CancelationException
)而取消,则不会重启。
还可以从代码块中发出多个值。每次 emit()
调用都会暂停执行代码块,直到在主线程上设置 LiveData
值。
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
也可以将 liveData
与 Transformations
结合使用,如以下示例所示:
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
可以从 LiveData
中发出多个值,方法是在每次想要发出新值时调用 emitSource()
函数。请注意,每次调用 emit()
或 emitSource()
都会移除之前添加的来源。
class UserDao: Dao {
@Query("SELECT * FROM User WHERE id = :id")
fun getUser(id: String): LiveData<User>
}
class MyRepository {
fun getUser(id: String) = liveData<User> {
val disposable = emitSource(
userDao.getUser(id).map {
Result.loading(it)
}
)
try {
val user = webservice.fetchUser(id)
// Stop the previous emission to avoid dispatching the updated user
// as `loading`.
disposable.dispose()
// Update the database.
userDao.insert(user)
// Re-establish the emission with success type.
emitSource(
userDao.getUser(id).map {
Result.success(it)
}
)
} catch(exception: IOException) {
// Any call to `emit` disposes the previous one automatically so we don't
// need to dispose it here as we didn't get an updated value.
emitSource(
userDao.getUser(id).map {
Result.error(exception, it)
}
)
}
}
}
如需更多与协程相关的信息,请参阅以下链接:
Kotlin 协程与架构组件