让Flows感知生命周期

Posted eclipse_xu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了让Flows感知生命周期相关的知识,希望对你有一定的参考价值。

点击上方蓝字关注我,知识会给你力量

这个系列我做了协程和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 欢迎大家访问

往期推荐

本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

< END >

作者:徐宜生

更文不易,点个“三连”支持一下👇

以上是关于让Flows感知生命周期的主要内容,如果未能解决你的问题,请参考以下文章

JetPack架构---Lifecycle生命周期相关与原理

androidx.lifecycle 生命周期感知型组件实现原理

鸿蒙OS 生命周期

如何在 Android 的生命周期感知协程范围内返回函数值?

Page Ability生命周期内容介绍!

鸿蒙HarMonyOS的Page Ability生命周期