StateFlow, End of LiveData?

Ganesh Divekar
6 min readDec 15, 2020

StateFlow and SharedFlow

StateFlow and SharedFlow are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers.

StateFlow

StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property. To update state and send it to the flow, assign a new value to the value property of the MutableStateFlow class.

In Android, StateFlow is a great fit for classes that need to maintain an observable mutable state.

Following the examples from Kotlin flows, a StateFlow can be exposed from the LatestNewsViewModel so that the View can listen for UI state updates and inherently make the screen state survive configuration changes.

class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(exception: Throwable): LatestNewsUiState()
}

The class responsible for updating a MutableStateFlow is the producer, and all classes collecting from the StateFlow are the consumers. Unlike a cold flow built using the flow builder, a StateFlow is hot: collecting from the flow doesn't trigger any producer code. A StateFlow is always active and in memory, and it becomes eligible for garbage collection only when there are no other references to it from a garbage collection root.

When a new consumer starts collecting from the flow, it receives the last state in the stream and any subsequent states. You can find this behavior in other observable classes like LiveData.

The View listens for StateFlow as with any other flow:

class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
// This coroutine will run the given block when the lifecycle
// is at least in the Started state and will suspend when
// the view moves to the Stopped state
lifecycleScope.launchWhenStarted {
// Triggers the flow and starts listening for values
latestNewsViewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}

Note: Using launchWhen() functions from the Lifecycle Kotlin extensions to collect a flow from the UI layer is not always safe. When the view goes to the background, the coroutine suspends, leaving the underlying producer active and potentially emitting values that the view doesn't consume. This behavior could waste CPU and memory resources.

StateFlows are safe to collect using the launchWhen() functions since they're scoped to ViewModels, making them remain in memory when the View goes to the background, and they do lightweight work by just notifying the View about UI states. However, the problem might come with other producers that do more intensive work.

Warning: Never collect a flow from the UI using launch or the launchIn extension function if the UI needs to be updated. These functions process events even when the view is not visible. This behavior can lead to app crashes.

To convert any flow to a StateFlow, use the stateIn intermediate operator.

StateFlow, Flow, and LiveData

StateFlow and LiveData have similarities. Both are observable data holder classes, and both follow a similar pattern when used in your app architecture.

Note, however, that StateFlow and LiveData do behave differently:,

  • StateFlow requires an initial state to be passed in to the constructor, while LiveData does not.
  • 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.

In the previous example that used launchWhenStarted to collect the flow, when the coroutine that triggers the flow collection suspends as the View goes to the background, the underlying producers remain active.

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, as shown in the following example:

class LatestNewsActivity : AppCompatActivity() {
...
// Coroutine listening for UI states
private var uiStateJob: Job? = null
override fun onStart() {
super.onStart()
// Start collecting when the View is visible
uiStateJob = lifecycleScope.launch {
latestNewsViewModel.uiState.collect { uiState -> ... }
}
}
override fun onStop() {
// Stop collecting when the View goes to the background
uiStateJob?.cancel()
super.onStop()
}
}

Another way to stop listening for uiState changes when the view is not visible is to convert the flow to LiveData using the asLiveData() function from the lifecycle-livedata-ktx library:

class LatestNewsActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
latestNewsViewModel.uiState.asLiveData().observe(owner = this) { state ->
// Handle UI state
}
}
}

Making cold flows hot using shareIn

StateFlow is a hot flow—it remains in memory as long as the flow is collected or while any other references to it exist from a garbage collection root. You can turn cold flows hot by using the shareIn operator.

Using the callbackFlow created in Kotlin flows as an example, instead of having each collector create a new flow, you can share the data retrieved from Firestore between collectors by using shareIn. You need to pass in the following:

  • A CoroutineScope that is used to share the flow. This scope should live longer than any consumer to keep the shared flow alive as long as needed.
  • The number of items to replay to each new collector.
  • The start behavior policy.
class NewsRemoteDataSource(...,
private val externalScope: CoroutineScope,
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
}.shareIn(
externalScope,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
}

In this example, the latestNews flow replays the last emitted item to a new collector and remains active as long as externalScope is alive and there are active collectors. The SharingStarted.WhileSubscribed() start policy keeps the upstream producer active while there are active subscribers. Other start policies are available, such as SharingStarted.Eagerly to start the producer immediately or SharingStarted.Lazily to start sharing after the first subscriber appears and keep the flow active forever.

Note: To learn more about patterns for externalScope, check out this article.

SharedFlow

The shareIn function returns a SharedFlow, a hot flow that emits values to all consumers that collect from it. A SharedFlow is a highly-configurable generalization of StateFlow.

You can create a SharedFlow without using shareIn. As an example, you could use a SharedFlow to send ticks to the rest of the app so that all the content refreshes periodically at the same time. Apart from fetching the latest news, you might also want to refresh the user information section with its favorite topics collection. In the following code snippet, a TickHandler exposes a SharedFlow so that other classes know when to refresh its content. As with StateFlow, use a backing property of type MutableSharedFlow in a class to send items to the flow:

// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
private val externalScope: CoroutineScope,
private val tickIntervalMs: Long = 5000
) {
// Backing property to avoid flow emissions from other classes
private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
val tickFlow: SharedFlow<Event<String>> = _tickFlow
init {
externalScope.launch {
while(true) {
_tickFlow.emit(Unit)
delay(tickIntervalMs)
}
}
}
}
class NewsRepository(
...,
private val tickHandler: TickHandler,
private val externalScope: CoroutineScope
) {
init {
externalScope.launch {
// Listen for tick updates
tickHandler.tickFlow.collect {
refreshLatestNews()
}
}
}
suspend fun refreshLatestNews() { ... }
...
}

You can customize the SharedFlow behavior in the following ways:

  • replay lets you resend a number of previously-emitted values for new subscribers.
  • onBufferOverflow lets you specify a policy for when the buffer is full of items to be sent. The default value is BufferOverflow.SUSPEND, which makes the caller suspend. Other options are DROP_LATEST or DROP_OLDEST.

MutableSharedFlow also has a subscriptionCount property that contains the number of active collectors so that you can optimize your business logic accordingly. MutableSharedFlow also contains a resetReplayCache function if you don't want to replay the latest information sent to the flow.

--

--

Ganesh Divekar

Winner at Jharkhand Hackathon 2019 | AndroidX | Kotlin | MVVM | RxJava | Dagger 2 | Room | Data Binding | LiveData