I recently updated PeopleInSpace project to make use of Kotlin Flow in shared multiplatform code to poll for the position of the International Space Station (ISS). As part of that work I had initially updated PeopleInSpaceViewModel in the Android app to use StateFlow (along with stateIn()) instead of LiveData. However there were some lifecycle implications of this change when used alongside Jetpack Compose that have caused me to revert, for now, to using LiveData. This article is intended to capture results of that comparison. Note that this is based on Jetpack Compose 1.0.0-alpha07.

I should also point out that it was following interaction on Twitter that helped trigger realisation about some of these differences.


So, to start off with, the following is the implementation of code in PeopleInSpaceRepository that returns a flow that performs that polling.

fun pollISSPosition(): Flow<IssPosition> = flow {
    while (true) {
        val position = peopleInSpaceApi.fetchISSPosition().iss_position
        emit(position)
        delay(POLL_INTERVAL)
    }
}

That then had been converted to StateFlow in PeopleInSpaceViewModel as follows.

val issPosition = peopleInSpaceRepository.pollISSPosition()
	.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), IssPosition(0.0, 0.0))

And then in Jetpack Compose code we convert this to State using

val issPosition = peopleInSpaceViewModel.issPosition.collectAsState()

At this point, even in a non-Compose application, there are lifecycle implications of observing/collecting StateFlow in UI code (when compared to use of LiveData). These differences are very well captured in this article including, in particular:

LiveData.observe() automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not.

Advice in that article then is to use launchWhenStarted to collect the flow, so that the coroutine that triggers the flow collection suspends when the activity goes to the background. However, as noted, the underlying producers still remain active in this case.

With hot implementations, be careful when collecting when the UI is not on the screen, as this could waste resources. You can instead manually stop collecting the flow.

So, to avoid that we need to to explicitly cancel in onStop() (and restart collection in onStart()) as shown here:

override fun onStart() {
    super.onStart()
    // Start collecting when the activity is visible
    uiStateJob = lifecycleScope.launch {
        someViewModel.uiState.collect { uiState -> ... }
    }
}

override fun onStop() {
    // Stop collecting when the activity goes to the background
    uiStateJob?.cancel()
    super.onStop()
}


Compose behaviour

When using Compose, and in particular collectAsState(), there aren’t any options to influence lifecycle behaviour like this. Under the hood collectAsState makes use of LaunchedEffect and as such the collection will only be cancelled when the composition is disposed (which doesn’t happen when we go in to background and underlying activity goes in to STOPPED state). Therefore the “producer” (the flow created by pollISSPosition above) will continue to keep polling. BTW there’s nice overview of Jetpack Compose’s effect handlers, like LaunchedEffect, in this article.


Converting to use asLiveData

So, the change that was made then was to make use of asLiveData() (a Flow extension function)

val issPosition: LiveData<IssPosition> 
			= peopleInSpaceRepository.pollISSPosition().asLiveData()

The documentation for asLiveData includes following key points.

  • upstream flow collection starts when the returned LiveData becomes active.
  • if the LiveData becomes inactive the flow collection will be cancelled.
  • after cancellation, if the LiveData becomes active again, the upstream flow collection will be re-executed.

Now, when the app goes in to background, issPosition will become inactive and flow collection (and associated polling) will be cancelled. It will then be restarted if/when app comes back in to the foreground.

Our Compose code then was updated then to use observeAsState()

val issPosition = 
	peopleInSpaceViewModel.issPosition.observeAsState(IssPosition(0.0, 0.0))

I also made similar changes to these (for same reason) to BikeShare project.

Future Changes

There have seemingly been some discussions in Compose team around changing default behaviour for onStop() so that may have impact on what’s been described here. For example these LifecycleCoroutineScope Cancelling APIs changes look like they could potentially be used to provide this behaviour.

This CL adds LifecycleCoroutineScope#launchUntilX APIs that cancel the block when the event X comes as opposed to the launchWhenX APIs that suspend the block instead.


Update 24th March 2021: This has now been addressed with new Flow.flowWithLifecycle() api (as described here)


Featured in Android Weekly Issue #442