概述
点击上方蓝字关注我,知识会给你力量
这个系列我做了协程和Flow开发者的一系列文章的翻译,旨在了解当前协程、Flow、LiveData这样设计的原因,从设计者的角度,发现他们的问题,以及如何解决这些问题,pls enjoy it。
随着SharedFlow和StateFlow的引入,许多开发者正在从UI层的LiveData迁移,以利用Flow API的优点,并在所有层中获得更一致的API,但遗憾的是,正如Christophe Beyls在他的帖子中解释的那样,当视图的生命周期进入代码时,迁移就变得复杂了。lifecycle:lifecycle-runtime-ktx的2.4版本引入了API来帮助这方面的工作:repeatOnLifecycle和flowWithLifecycle(要了解更多关于这些的信息,请查看文章。从Android UIs收集Flow的更安全的方法),在这篇文章中,我们将尝试它们,我们将讨论它们在某些情况下带来的一个小问题,我们将看看我们是否能想出一个更灵活的解决方案。
The problem
为了解释这个问题,让我们想象一下,我们有一个Sample应用程序,当它处于活动状态时监听位置更新,每当有新的位置可用时,它就会调用API来检索一些附近的位置。因此,为了监听位置更新,我们将编写一个LocationObserver类,它提供了一个返回位置更新的Cold Flow。
class LocationObserver(private val context: Context) {
fun observeLocationUpdates(): Flow<Location> {
return callbackFlow {
Log.d(TAG, "observing location updates")
val client = LocationServices.getFusedLocationProviderClient(context)
val locationRequest = LocationRequest
.create()
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
.setInterval(0)
.setFastestInterval(0)
val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
if (locationResult != null) {
Log.d(TAG, "got location ${locationResult.lastLocation}")
trySend(locationResult.lastLocation)
}
}
}
client.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
awaitClose {
Log.d(TAG, "stop observing location updates")
client.removeLocationUpdates(locationCallback)
}
}
}
}
那么我们将在我们的ViewModel中使用这个类:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val locationObserver = LocationObserver(application)
private val hasLocationPermission = MutableStateFlow(false)
private val locationUpdates: Flow<Location> = hasLocationPermission
.filter { it }
.flatMapLatest { locationObserver.observeLocationUpdates() }
val viewState: Flow<ViewState> = locationUpdates
.mapLatest { location ->
val nearbyLocations = api.fetchNearbyLocations(location.latitude, location.longitude)
ViewState(
isLoading = false,
location = location,
nearbyLocations = nearbyLocations
)
}
fun onLocationPermissionGranted() {
hasLocationPermission.value = true
}
}
❝为了简单起见,我们使用一个AndroidViewModel来直接访问Context,我们不会处理关于位置权限和设置的不同边缘情况。
❞
现在,我们在Fragment中要做的就是听从对viewState更新的反应,并更新UI。
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.viewState
.onEach { viewState -> binding.render(viewState) }
.launchIn(this)
}
其中FragmentMainBinding#render是一个可以更新用户界面的扩展。
现在,如果我们尝试运行这个应用程序,当我们把它放到后台时,我们会看到LocationObserver仍然在监听位置更新,然后获取附近的地方,尽管用户界面忽略了它们。
我们解决这个问题的第一个尝试,是使用新的API flowWithLifecycle:
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.viewState
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.onEach { viewState -> binding.render(viewState) }
.launchIn(this)
}
如果我们现在运行该应用程序,我们会注意到它每次进入后台时都会向Logcat打印以下一行内容。
D/LocationObserver: stop observing location updates
所以新的API修复了这个问题,但是有一个问题,每当应用程序进入后台,然后我们回来,我们就会失去之前的数据,即使位置没有改变,我们也会再次点击API,出现这种情况是因为flowWithLifecycle会在每次使用的生命周期低于传递的状态(对我们来说是开始)时取消上游,并在状态恢复时再次重新启动。
Solution using the official APIs
在保持使用flowWithLifecycle的同时,官方的解决方案在Jose Alcérreca的文章中做了解释,它是使用stateIn,但有一个特殊的超时时间设置为5秒,以考虑到配置的变化,所以我们需要在viewState的Flow中加入以下语句,以达到这个目的。
stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
initialValue = ViewState(isLoading = true)
)
这样做很好,但是,每次应用程序进入后台时停止/重启Flow会产生另一个问题,比如说,我们不需要获取附近的地方,除非位置发生了最小距离的变化,所以让我们把代码改成以下内容。
val viewState: Flow<ViewState> = locationUpdates
.distinctUntilChanged { l1, l2 -> l1.distanceTo(l2) <= 300 }
.mapLatest { location ->
val nearbyLocations = api.fetchNearbyLocations(location.latitude, location.longitude)
ViewState(
isLoading = false,
location = location,
nearbyLocations = nearbyLocations
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
initialValue = ViewState(isLoading = true)
)
如果我们现在运行这个应用程序,然后把它放到后台超过5秒钟,再重新打开,我们会注意到我们重新获取附近的位置,即使位置根本没有变化,虽然这在大多数情况下不是一个大问题,但在某些情况下,它可能是昂贵的:网络慢,或慢的API,或沉重的计算。
An alternative solution: making the Flows lifecycle-aware
如果我们能使我们的locationUpdates流程具有生命周期意识,在没有来自Fragment的任何显式交互的情况下停止它呢?这样,我们就可以停止监听位置更新,而不必重新启动整个流程,如果位置没有变化,就重新运行所有的中间操作,我们甚至可以使用 launchWhenStarted 定期收集我们的 viewState Flow,因为我们将确定它不会运行,因为我们没有发射任何位置。
如果我们能在我们的ViewModel里面有一个内部热流,让我们观察到View的状态就好了。
private val lifeCycleState = MutableSharedFlow<Lifecycle.State>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
然后,我们将能够有一个扩展,根据生命周期,停止然后重新启动我们的上游流。
fun <T> Flow<T>.whenAtLeast(requiredState: Lifecycle.State): Flow<T> {
return lifeCycleState.map { state -> state.isAtLeast(requiredState) }
.distinctUntilChanged()
.flatMapLatest {
// flatMapLatest will take care of cancelling the upstream Flow
if (it) this else emptyFlow()
}
}
实际上,我们可以使用LifecycleEventObserver API实现这一点
private val lifecycleObserver = object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
lifeCycleState.tryEmit(event.targetState)
if (event.targetState == Lifecycle.State.DESTROYED) {
source.lifecycle.removeObserver(this)
}
}
}
我们可以用它来连接到Fragment的生命周期事件。
fun startObservingLifecycle(lifecycle: Lifecycle) {
lifecycle.addObserver(lifecycleObserver)
}
有了这个,我们现在可以将我们的locationUpdates流程更新为如下内容
private val locationUpdates: Flow<Location> = hasLocationPermission
.filter { it }
.flatMapLatest { locationObserver.observeLocationUpdates() }
.whenAtLeast(Lifecycle.State.STARTED)
而且我们可以在Fragment中定期观察我们的viewState Flow,而不必担心当应用程序进入后台时保持GPS开启。
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.viewState
.onEach { viewState -> binding.render(viewState) }
.launchIn(this)
}
扩展whenAtLeast是灵活的,因为它可以应用于链中的任何Flow,而不仅仅是在收集过程中,正如我们所看到的,将它应用于上游的触发Flow(在我们的例子中是位置更新),导致更少的计算。
除非有需要,否则包括附近地点的获取在内的中间运算符不会运行。
我们不会在从后台回来的时候重新向用户界面发送结果,因为我们不会取消收集。
如果你想在Github上查看完整的代码:https://github.com/hichamboushaba/FlowLifecycle,完整的代码包含了一个Sample,说明我们如何在这些变化下对ViewModels进行单元测试。
原文链接:https://proandroiddev.com/making-cold-flows-lifecycle-aware-92331440e4e5
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
往期推荐
flutter与compose的爱恨情仇
一篇掌握LiveData transformations
闲言碎语——第四期
谁能取代Android的LiveData- StateFlow or SharedFlow?
本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生
更文不易,点个“三连”支持一下????
最后
以上就是专注酒窝为你收集整理的让Flows感知生命周期的全部内容,希望文章能够帮你解决让Flows感知生命周期所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复