In this series, I translated a series of articles of coroutines and Flow developers, aiming to understand the reasons for the current design of coroutines, flows and LiveData, find out their problems from the perspective of designers, and how to solve these problems. PLS Enjoy it.

With the introduction of SharedFlow and StateFlow, many developers are migrating LiveData from the UI layer to take advantage of the Flow API and get a more consistent API across all layers, but unfortunately, as Christophe Beyls explains in his post, Migration becomes complicated when the view life cycle enters the code. Lifecycle :lifecycle- run-time – the 2.4 release of KTX introduces apis to help with this: repeatOnLifecycle and flowWithLifecycle (for more information on these, check out the article. Safer ways to collect flows from Android UIs), in this article we’ll try them out, we’ll discuss a small problem they bring up in some cases, and we’ll see if we can come up with a more flexible solution.

The problem

To explain this, let’s imagine that we have a Sample application that listens for location updates while it is active and calls the API to retrieve some nearby locations whenever a new location is available. So, to listen for location updates, we’ll write a LocationObserver class that provides a Cold Flow that returns location updates.

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) } } } }Copy the code

So we’ll use this class in our 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
    }
}
Copy the code

For simplicity, we use an AndroidViewModel to access the Context directly, and we don’t deal with different edge cases about location permissions and Settings.

Now, all we need to do in the Fragment is listen to the reaction to the viewState update and update the UI.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
        viewModel.viewState
                .onEach { viewState -> binding.render(viewState) }
                .launchIn(this)
}
Copy the code

Fragmentmainbing # Render is an extension that updates the user interface.

Now, if we try to run the application, when we put it in the background, we see that the LocationObserver is still listening for location updates and then fetching nearby places, even though the user interface ignores them.

Our first attempt to solve this problem is to use the new API flowWithLifecycle:

viewLifecycleOwner.lifecycleScope.launchWhenStarted { 
    viewModel.viewState
             .flowWithLifecycle(viewLifecycleOwner.lifecycle)
             .onEach { viewState -> binding.render(viewState) } 
             .launchIn(this) 
}
Copy the code

If we run the application now, we’ll notice that it prints the following line to Logcat every time it goes into the background.

D/LocationObserver: stop observing location updates
Copy the code

So the new API fixes that, but there’s a problem, every time the application goes into the background, and then we come back, we lose the previous data, and even though the location hasn’t changed, we hit the API again, This happens because flowWithLifecycle cancels upstream each time the life cycle of use falls below the passed state (the start for us) and restarts again when the state is restored.

Solution using the official APIs

While maintaining flowWithLifecycle, the official solution, as explained in Jose Alcerreca’s article, is to use stateIn, but with a special timeout set to 5 seconds to allow for configuration changes, So we need to add the following statement to the Flow of viewState to do this.

stateIn(
         scope = viewModelScope,
         started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000L),
         initialValue = ViewState(isLoading = true)
)
Copy the code

This is fine, but stopping/restarting Flow every time the application goes into the background creates another problem. For example, we don’t need to fetch nearby places unless the location has changed by a minimum distance, so let’s change the code to the following.

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)
        )
Copy the code

If we now run the application, and then put it in the background of more than 5 seconds, and then back on, we will notice us to obtain the location of the nearby, even if the position doesn’t change, although in most cases this is not a big problem, but in some cases, it may be expensive: the network is slow, slow or API, or heavy computation.

An alternative solution: making the Flows lifecycle-aware

What if we could make our locationUpdates flow lifecycle aware and stop it without any explicit interaction from the Fragment? This way, we can stop listening for position updates without restarting the whole process, rerun all intermediate operations if the position does not change, and we can even use launchWhenStarted to periodically collect our viewState Flow because we will be sure it will not run, Because we didn’t launch anywhere.

It would be nice if we could have an internal heat flow in our ViewModel that allows us to observe the state of the View.

private val lifeCycleState = MutableSharedFlow<Lifecycle.State>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
Copy the code

Then, we will be able to have an extension that stops and then restarts our upstream stream according to the lifecycle.

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() } }Copy the code

In fact, we can do this using the Life Cycle Evening Server 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)
        }
    }
}
Copy the code

We can use it to connect to the Fragment’s lifecycle events.

fun startObservingLifecycle(lifecycle: Lifecycle) {
    lifecycle.addObserver(lifecycleObserver)
}
Copy the code

With this, we can now update our locationUpdates flow to the following

private val locationUpdates: Flow<Location> = hasLocationPermission
    .filter { it }
    .flatMapLatest { locationObserver.observeLocationUpdates() }
    .whenAtLeast(Lifecycle.State.STARTED)
Copy the code

And we can periodically observe our viewState Flow in the Fragment without having to worry about keeping GPS on when the application goes into the background.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
        viewModel.viewState
                .onEach { viewState -> binding.render(viewState) }
                .launchIn(this)
}
Copy the code

Extending whenAtLeast is flexible because it can be applied to any Flow in the chain, not just during collection, and as we have seen, applying it to trigger flows upstream (in our case position updates) leads to less computation.

  • The intermediate operators, including the fetch of nearby locations, do not run unless needed.
  • We do not re-send the results to the user interface when we come back from the background, because we do not cancel the collection.

If you’d like to see the full code on Github: github.com/hichamboush…

Original link: proandroiddev.com/making-cold…

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit