将 Kotlin 协程与生命周期感知型组件结合使用

Kotlin 协程提供了一个 API,使您能够编写异步代码。借助 Kotlin 协程,您可以定义一个 CoroutineScope,这有助于您管理协程的运行时间。每个异步操作都在特定范围内运行。

生命周期感知型组件为应用中的逻辑作用域提供对协程的一流支持,并提供与 LiveData 的互操作层。本主题介绍了如何有效地将协程与生命周期感知型组件结合使用。

添加 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 被清除后,工作会自动取消以避免消耗资源。

您可以通过 ViewModelviewModelScope 属性访问 ViewModelCoroutineScope,如以下示例所示

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
    }
}

LifecycleScope

为每个 Lifecycle 对象定义了一个 LifecycleScope。在此作用域中启动的任何协程都会在 Lifecycle 被销毁时取消。您可以通过 lifecycle.coroutineScopelifecycleOwner.lifecycleScope 属性访问 LifecycleCoroutineScope

以下示例演示了如何使用 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 状态时取消收集。这种方法仅在 UI 在屏幕上可见时处理流发射,从而节省资源并可能避免应用崩溃。

对于这些情况,LifecycleLifecycleOwner 提供了挂起 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.whenCreatedlifecycle.whenStartedlifecycle.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.

        }
    }
}

如果 Lifecycle 被销毁,而协程正在通过 when 方法之一活跃,则协程会自动取消。在以下示例中,finally 代码块在 Lifecycle 状态为 DESTROYED 后运行。

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 时,您可能需要异步计算值。例如,您可能希望检索用户的偏好设置并将其提供给您的 UI。在这些情况下,您可以使用 liveData 构建器函数来调用 suspend 函数,将结果作为 LiveData 对象提供。

在以下示例中,loadUser() 是在其他地方声明的 suspend 函数。使用 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))
    }
}

您也可以将 liveDataTransformations 结合使用,如以下示例所示

class MyViewModel: ViewModel() {
    private val userId: LiveData<String> = MutableLiveData()
    val user = userId.switchMap { id ->
        liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(database.loadUserById(id))
        }
    }
}

您可以通过在需要发射新值时调用 emitSource() 函数来从 LiveData 发射多个值。请注意,每次调用 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)
                }
            )
        }
    }
}

如需了解更多协程相关信息,请参阅以下链接:

其他资源

如需详细了解如何将协程与生命周期感知型组件结合使用,请参阅以下其他资源。

示例

博客