使用更为安全的方式收集 Android UI 数据流,深入浅出 Android
close(e) // 在出现异常时关闭 Flow
}
// 在 Flow 收集结束时进行清理操作
awaitClose {
removeLocationUpdates(callback)
}
}
注意:callbackFlow 内部使用 channel 实现,其概念与阻塞队列十分类似,并且默认容量为 64。
使用任意前述 API 从 UI 层收集此数据流都会导致其持续发送位置信息,即使视图不再展示数据也不会停止!示例如下:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 最早在 View 处于 STARTED 状态时从数据流收集数据,并在
// 生命周期进入 STOPPED 状态时 SUSPENDS(挂起)收集操作。
// 在 View 转为 DESTROYED 状态时取消数据流的收集操作。
lifecycleScope.launchWhenStarted {
locationProvider.locationFlow().collect {
// 新的位置!更新地图
}
}
// 同样的问题也存在于:
// - lifecycleScope.launch { /* 在这里从 locationFlow() 收集数据 */ }
// - locationProvider.locationFlow().onEach { /* ... */ }.launchIn(lifecycleScope)
}
}
lifecycleScope.launchWhenStarted 挂起了协程的执行。虽然新的位置信息没有被处理,但 callbackFlow 生产者仍然会持续发送位置信息。使用 lifecycleScope.launch 或 launchIn API 会更加危险,因为视图会持续消费位置信息,即使处于后台也不会停止!这种情况可能会导致您的应用崩溃。
为了解决这些 API 所带来的问题,您需要在视图转入后台时手动取消收集操作,以取消 callbackFlow 并避免位置提供者持续发送项目并浪费资源。举例来说,您可以像下面的例子这样操作:
class LocationActivity : AppCompatActivity() {
// 位置的协程监听器
private var locationUpdatesJob: Job? = null
override fun onStart() {
super.onStart()
locationUpdatesJob = lifecycleScope.launch {
locationProvider.locationFlow().collect {
// 新的位置!更新地图。
}
}
}
override fun onStop() {
// 在视图进入后台时停止收集数据
locationUpdatesJob?.cancel()
super.onStop()
}
}
这是一个不错的解决方案,美中不足的是有些冗长。如果这个世界有一个有关 Android 开发者的普遍事实,那一定是我们都不喜欢编写模版代码。不必编写模版代码的一个最大好处就是——写的代码越少,出错的概率越小!
LifecycleOwner.addRepeatingJob
==========================================================================================
现在我们境遇相同,并且也知道问题出在哪里,是时候找出一个解决方案了。我们的解决方案需要:
简单;
友好或者说便于记忆与理解;
更重要的是安全!无论数据流的实现细节如何,它都应能够应对所有用例。
事不宜迟——您应该使用的 API 是 lifecycle-runtime-ktx 库中所提供的
LifecycleOwner.addRepeatingJob。请参考下面的代码:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 最早在 View 处于 STARTED 状态时从数据流收集数据,并在
// 生命周期进入 STOPPED 状态时 STOPPED(停止)收集操作。
// 它会在生命周期再次进入 STARTED 状态时自动开始进行数据收集操作。
lifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地图
}
}
}
}
addRepeatingJob 接收 Lifecycle.State 作为参数,并用它与传入的代码块一起,在生命周期到达该状态时,自动创建并启动新的协程;同时也会在生命周期低于该状态时取消正在运行的协程。
由于 addRepeatingJob 会在协程不再被需要时自动将其取消,因而可以避免产生取消操作相关的模版代码。您也许已经猜到,为了避免意外行为,这一 API 需要在 Activity 的 onCreate 或 Fragment 的 onViewCreated 方法中调用。下面是配合 Fragment 使用的示例:
class LocationFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// ...
viewLifecycleOwner.addRepeatingJob(Lifecycle.State.STARTED) {
locationProvider.locationFlow().collect {
// 新的位置!更新地图
}
}
}
}
注意:这些 API 在
lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 库或其更新的版本中可用。
使用 repeatOnLifecycle
出于提供更为灵活的 API 以及保存调用中的 CoroutineContext 的目的,我们也提供了 挂起函数
Lifecycle.repeatOnLifecycle 供您使用。repeatOnLifecycle 会挂起调用它的协程,并会在进出目标状态时重新执行代码块,最后在 Lifecycle 进入销毁状态时恢复调用它的协程。
如果您需要在重复工作前执行一次配置任务,同时希望任务可以在重复工作开始前保持挂起,该 API 可以帮您实现这样的操作。示例如下:
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
// 单次配置任务
val expensiveObject = createExpensiveObject()
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 在生命周期进入 STARTED 状态时开始重复任务,在 STOPED 状态时停止
// 对 expensiveObject 进行操作
}
// 当协程恢复时,lifecycle
处于 DESTROY 状态。repeatOnLifecycle 会在
// 进入 DESTROYED 状态前挂起协程的执行
}
}
}
==================================================================================
当您只需要收集一个数据流时,也可以使用 Flow.flowWithLifecycle 操作符。这一 API 的内部也使用 suspend Lifecycle.repeatOnLifecycle 函数实现,并会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationProvider.locationFlow()
评论