将 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 提供了一种在 LifecycleDESTROYED 时自动取消长时间运行操作的正确方法,您可能还有其他情况需要在 Lifecycle 处于特定状态时启动代码块的执行,并在其处于另一种状态时取消。例如,您可能希望在 LifecycleSTARTED 时收集流,并在其为 STOPPED 时取消收集。此方法仅在 UI 在屏幕上可见时处理流发射,从而节省资源并可能避免应用崩溃。

对于这些情况,LifecycleLifecycleOwner 提供了挂起 repeatOnLifecycle API,它可以准确地做到这一点。以下示例包含一个代码块,该代码块在关联的 Lifecycle 至少处于 STARTED 状态时每次运行,并在 LifecycleSTOPPED 时取消

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.

        }
    }
}

如果协程通过其中一个 when 方法处于活动状态时,Lifecycle 被销毁,则该协程将自动取消。在下面的示例中,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() 是在其他地方声明的挂起函数。使用 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)
                }
            )
        }
    }
}

有关更多与协程相关的信息,请参阅以下链接。

其他资源

要了解有关将协程与生命周期感知型组件一起使用的更多信息,请参阅以下其他资源。

示例

博客