Kotlin 协程提供了一个可供您编写异步代码的 API。通过 Kotlin 协程,您可以定义 CoroutineScope
,以帮助您管理何时应运行协程。每个异步操作都在特定范围内运行。
生命周期感知型组件针对应用中的逻辑范围以及与 LiveData
的互操作层为协程提供了头等 (first-class) 支持。本主题介绍了如何有效地结合使用协程与生命周期感知型组件。
添加 KTX 依赖项
本主题中介绍的内置协程范围包含在每个相应组件的 KTX 扩展中。请务必在使用这些范围时添加相应的依赖项。
- 对于
ViewModelScope
,请使用androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0
或更高版本。 - 对于
LifecycleScope
,请使用androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
或更高版本。 - 对于
liveData
,请使用androidx.lifecycle:lifecycle-livedata-ktx:2.4.0
或更高版本。
生命周期感知型协程范围
生命周期感知型组件定义了以下内置范围供您在应用中使用。
ViewModelScope
应用中的每个 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. } } }
LifecycleScope
每个 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) } } }
可重启生命周期感知型协程
即使 lifecycleScope
提供了适当的方法以在 Lifecycle
处于 DESTROYED
状态时自动取消长时间运行的操作,但在某些情况下,您可能需要在 Lifecycle
处于某个特定状态时开始执行代码块,并在其处于其他状态时取消。例如,您可能希望在 Lifecycle
处于 STARTED
状态时收集数据流,并在其处于 STOPPED
状态时取消收集。通过此方法,应用仅在界面显示在屏幕上时才处理数据流发出操作,从而节省资源并可能避免发生应用崩溃问题。
对于这些情况,Lifecycle
和 LifecycleOwner
提供了挂起 repeatOnLifecycle
API 来确切实现相应操作。以下示例中的代码块会在关联的 Lifecycle
至少处于 STARTED
状态时运行,并且会在 Lifecycle
处于 STOPPED
状态时取消运行:
class MyFragment : Fragment() { val viewModel: MyViewModel by viewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Create a new coroutine in the lifecycleScope viewLifecycleOwner.lifecycleScope.launch { // repeatOnLifecycle launches the block in a new coroutine every time the // lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED. viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // Trigger the flow and start listening for values. // This happens when lifecycle is STARTED and stops // collecting when the lifecycle is STOPPED viewModel.someDataFlow.collect { // Process item } } } } }
生命周期感知型数据流收集
如果您只需要对单个数据流执行生命周期感知型收集,可以使用 Flow.flowWithLifecycle()
方法简化代码:
viewLifecycleOwner.lifecycleScope.launch { exampleProvider.exampleFlow() .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED) .collect { // Process the value. } }
但是,如果您需要并行对多个数据流执行生命周期感知型收集,则必须在不同的协程中收集每个数据流。在这种情况下,直接使用 repeatOnLifecycle()
会更加高效:
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // Because collect is a suspend function, if you want to // collect multiple flows in parallel, you need to do so in // different coroutines. launch { flow1.collect { /* Process the value. */ } } launch { flow2.collect { /* Process the value. */ } } } }
挂起生命周期感知型协程
即使 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. } } } } }
将协程与 LiveData 一起使用
使用 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
再次变为活动状态后重启;如果在上次运行中成功完成,则不会重启。请注意,代码块只有在自动取消的情况下才会重启。如果代码块由于任何其他原因(例如,抛出 CancellationException
)而取消,则不会重启。
您还可以从代码块中发出多个值。每次 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) } ) } } }
如需更多与协程相关的信息,请参阅以下链接:
其他资源
如需详细了解如何将协程与生命周期感知型组件一起使用,请参阅下面列出的其他资源。
示例
博客
为您推荐
- 注意:当 JavaScript 处于关闭状态时,系统会显示链接文字
- LiveData 概览
- 使用生命周期感知型组件处理生命周期
- 加载并显示分页数据