【译】Making cold Flows lifecycle-aware

757 阅读6分钟

这个系列我做了协程和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上查看完整的代码:github.com/hichamboush…

原文链接:proandroiddev.com/making-cold…

向大家推荐下我的网站 xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问