使用更为安全的方式收集 Android UI 数据流
Kotlin 数据流
https://developer.android.google.cn/kotlin/flow
本文将会带您学习如何使用 LifecycleOwner.addRepeatingJob、Lifecycle.repeatOnLifecycle 以及 Flow.flowWithLifecycle API 来避免资源的浪费;同时也会介绍为什么这些 API 适合作为在 UI 层收集数据流时的默认选择。
资源浪费
注意: 冷流是一种数据流类型,这种数据流会在新的订阅者收集数据时,按需执行生产者的代码块。
推荐
https://developer.android.google.cn/kotlin/coroutines/coroutines-best-practices#coroutines-data-layerCoroutineScope.launch
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.htmlFlow<T>.launchIn
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/launch-in.htmlLifecycleCoroutineScope.launchWhenX
https://developer.android.google.cn/reference/kotlin/androidx/lifecycle/LifecycleCoroutineScopechannel
https://kotlinlang.org/docs/channels.htmlbuffer
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/buffer.htmlconflate
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/conflate.htmlflowOn
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.htmlshareIn
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/share-in.html
// 基于 Channel 实现的冷流,可以发送位置的更新
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
try { offer(result.lastLocation) } catch(e: Exception) {}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e) // 在出现异常时关闭 Flow
}
// 在 Flow 收集结束时进行清理操作
awaitClose {
removeLocationUpdates(callback)
}
}
注意: callbackFlow 内部使用 channel 实现,其概念与阻塞队列十分类似,并且默认容量为 64。
callbackFlow
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/callback-flow.htmlchannel
https://kotlinlang.org/docs/reference/coroutines/channels.html队列
https://en.wikipedia.org/wiki/Queue_(abstract_data_type)
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)
}
}
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()
}
}
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 {
// 新的位置!更新地图
}
}
}
}
lifecycle-runtime-ktx
https://developer.android.google.cn/jetpack/androidx/releases/lifecycle
Lifecycle.State
https://developer.android.google.cn/reference/android/arch/lifecycle/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
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
当您只需要收集一个数据流时,也可以使用 Flow.flowWithLifecycle 操作符。这一 API 的内部也使用 suspend Lifecycle.repeatOnLifecycle 函数实现,并会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。
class LocationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
locationProvider.locationFlow()
.flowWithLifecycle(this, Lifecycle.State.STARTED)
.onEach {
// 新的位置!更新地图
}
.launchIn(lifecycleScope)
}
}
注意: Flow.flowWithLifecycle API 的命名以 Flow.flowOn(CoroutineContext) 为先例,因为它会在不影响下游数据流的同时修改收集上游数据流的 CoroutineContext。与 flowOn 相似的另一点是,Flow.flowWithLifecycle 也加入了缓冲区,以防止消费者无法跟上生产者。这一特点源于其实现中使用的 callbackFlow。
配置内部生产者
您可以使用 MutableStateFlow 与 MutableSharedFlow 两个 API 中暴露的 subscriptionCount 字段来控制它们,当该字段值为 0 时,内部的生产者就会停止。默认情况下,只要持有数据流实例的对象还在内存中,它们就会保持生产者的活跃状态。针对这些 API 也有一些合适的用例,比如使用 StateFlow 将 UiState 从 ViewModel 中暴露给 UI。这么做很合适,因为它意味着 ViewModel 总是需要向 View 提供最新的 UI 状态。
相似的,也可以为此类操作使用共享开始策略配置 Flow.stateIn 与 Flow.shareIn 操作符。WhileSubscribed() 将会在没有活跃的订阅者时停止内部的生产者!相应的,无论数据流是 Eagerly (积极) 还是 Lazily (惰性) 的,只要它们使用的 CoroutineScope 还处于活跃状态,其内部的生产者就会保持活跃。
共享开始策略
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/index.html
Flow.stateIn
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html
Flow.shareIn
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/share-in.html
WhileSubscribed()
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-companion/-while-subscribed.html
Eagerly
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-companion/-eagerly.html
Lazily
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-companion/-lazily.html
注意: 本文中所描述的 API 可以很好的作为默认从 UI 收集数据流的方式,并且无论数据流的实现方式如何,都应该使用它们。这些 API 做了它们要做的事: 在 UI 于屏幕中不可见时,停止收集其数据流。至于数据流是否应该始终处于活动状态,则取决于它的实现。
在 Jetpack Compose 中安全地收集数据流
Flow.collectAsState
https://developer.android.google.cn/reference/kotlin/androidx/compose/runtime/package-summary#collectasstate
State<T>
https://developer.android.google.cn/reference/kotlin/androidx/compose/runtime/State
@Composable
fun LocationScreen(locationFlow: Flow<Flow>) {
val lifecycleOwner = LocalLifecycleOwner.current
val locationFlowLifecycleAware = remember(locationFlow, lifecycleOwner) {
locationFlow.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED)
}
val location by locationFlowLifecycleAware.collectAsState()
// 当前位置,可以拿它做一些操作
}
注意,您需要记得生命周期感知型数据流使用 locationFlow 与 lifecycleOwner 作为键,以便始终使用同一个数据流,除非其中一个键发生改变。
需要记得
https://developer.android.google.cn/jetpack/compose/state
Compose 的副作用 (Side-effect) 便是必须处在受控环境中,因此,使用 LifecycleOwner.addRepeatingJob 不安全。作为替代,可以使用 LaunchedEffect 来创建跟随 composable 生命周期的协程。在它的代码块中,如果您需要在宿主生命周期处于某个 State 时重新执行一个代码块,可以调用挂起函数 Lifecycle.repeatOnLifecycle。
受控环境中
https://developer.android.google.cn/jetpack/compose/lifecycle#state-effect-use-cases
LaunchedEffect
https://developer.android.google.cn/reference/kotlin/androidx/compose/runtime/package-summary#launchedeffect_1
对比 LiveData
LiveData https://developer.android.google.cn/topic/libraries/architecture/livedata
操作符 https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
数据绑定对 StateFlow 的支持
另一方面,您会想要使用 LiveData 的原因之一,可能是它受到数据绑定的支持。不过 StateFlow 也一样!更多有关数据绑定对 StateFlow 的支持信息,请参阅官方文档。
StateFlow https://developer.android.google.cn/kotlin/flow/stateflow-and-sharedflow 官方文档 https://developer.android.google.cn/topic/libraries/data-binding/observability#stateflow
在 Android 开发中,请使用 LifecycleOwner.addRepeatingJob、suspend Lifecycle.repeatOnLifecycle 或 Flow.flowWithLifecycle 从 UI 层安全地收集数据流。
👆点击获取 "开发者的日常" 表情包
推荐阅读