Reactive Streams on Kotlin: SharedFlow and StateFlow

In this tutorial, you’ll learn about reactive streams in Kotlin and build an app using two types of streams: SharedFlow and StateFlow. By Ricardo Costeira.

5 (20) · 3 Reviews

Download materials
Save for later
Share

Event streams have become standard on Android. For years, RxJava has been the standard for reactive streams. Now, Kotlin provides its own reactive streams implementation, called Flow. Like RxJava, Kotlin Flow can create — and react to — streams of data. Also like RxJava, the event streams can come from cold or hot publishers. The difference between the two is simple: Although cold streams emit events only if there are any subscribers, hot streams can emit new events even without having any subscribers reacting to them. In this tutorial, you’ll learn about Flow’s hot stream implementations, called SharedFlow and StateFlow. More specifically, you’ll learn:

  • What a SharedFlow is.
  • What a StateFlow is and how it relates to SharedFlow.
  • How these hot stream flows compare to RxJava, Channels and LiveData.
  • How you can use them on Android.

You might ask yourself: “Why use Kotlin’s SharedFlow and StateFlow over RxJava though?” Although RxJava gets the job done well, some like to describe it as “using a bazooka to kill an ant”. In other words, although the framework works, it’s fairly easy to get carried away with all its capabilities. Doing so can lead to overly complex solutions and code that’s hard to understand. Kotlin Flow provides more direct and specific implementations for reactive streams.

You also need to be familiar with at least the basics of Kotlin coroutines and flow. For coroutines, you can check out our Kotlin Coroutines Tutorial for Android: Getting Started and Kotlin Coroutines Tutorial for Android: Advanced tutorials. For Flow, you can look at our Kotlin Flow for Android: Getting Started tutorial.

Note: This tutorial assumes you have solid Android development knowledge. If not, you can check out our Beginning Android Development with Kotlin series first.

Getting Started

Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial and open the starter project.

You’ll work on an app called CryptoStonks5000. This app has two screens: The first screen shows the user a few cryptocurrencies, while the second shows the price progression for a cryptocurrency in the past 24 hours.

To learn about shared flows and state flows, you’ll:

  1. Implement an event stream with SharedFlow that emits events shared between screens.
  2. Refactor CryptoStonks5000 to use StateFlow to handle view state.

The project follows a Clean Architecture approach and MVVM pattern.

Crypto Stonks 5000 - Project structure

Build and run the project just to make sure everything is working. After that, it’s time to learn about shared flows!

User exploring stock information on the CryptoStonks5000 app

SharedFlow

Before getting into the code, you need to at least be aware of what a SharedFlow is.

A shared flow is, at its core, a Flow. But it has two main differences from the standard Flow implementation. It:

  • Emits events even if you don’t call collect() on it. After all, it is a hot stream implementation.
  • Can have multiple subscribers.

Notice the term “subscribers” used here instead of “collectors” like you would see with a regular Flow. This change in naming is because shared flows never complete. In other words, when you call Flow.collect() on a shared flow, you’re not collecting all its events. Instead, you’re subscribing to the events that get emitted while that subscription exists.

Although this also means that calls to Flow.collect() on shared flows don’t complete normally, the subscription can still be canceled. As you might expect, this cancellation happens by canceling the coroutine.

Note: Flow-truncating operators such as Flow.take(count: Int) can force a shared flow to complete.

With that out of the way, it’s time to code.

Handling Shared Events

You’ll implement a fake price notification system to mimic coin value variations. It has to be a fake one because the real thing’s just too volatile. :]

Users should be aware of these variations no matter which screen they’re in. To make that possible, you’ll create a shared flow in a ViewModel shared by all screens.

In the presentation package, find and open CoinsSharedViewModel.kt.

To start, you need to know how to create a shared flow. Well, it’s your lucky day, because you’re about to create two in a row! Add this code at the top of the class:

private val _sharedViewEffects = MutableSharedFlow<SharedViewEffects>() // 1

val sharedViewEffects = _sharedViewEffects.asSharedFlow() // 2

In this code:

  1. You call MutableSharedFlow. This creates a mutable shared flow that emits events of type SharedViewEffects, which is a simple sealed class to model the possible events. Note that this is a private property. You’ll use this one internally to emit events while exposing an immutable shared flow to make them visible externally.
  2. You create the public immutable shared flow mentioned above by calling asSharedFlow() on the mutable shared flow. This way, the immutable exposed property always reflects the value of the mutable private one.

Having these two properties is a good practice. Not only does it give you the freedom to emit whatever you want internally through _sharedViewEffects, but it also makes it so external code can only react to those emissions by subscribing to sharedViewEffects. As such, the subscribing code has no power to change the shared flow, which is a neat way of forcing a robust design and separation of concerns and avoiding mutability bugs.

Event Emission With SharedFlow

OK, you have your flows. Now, you need to emit something with them: price variations. CoinsSharedViewModel calls getPriceVariations() in its init block, but the method doesn’t do anything yet.

Add this code to getPriceVariations():

viewModelScope.launch { // 1
  for (i in 1..100) { // 2
    delay(5000) // 3
    _sharedViewEffects.emit(SharedViewEffects.PriceVariation(i)) // 4
  }
}

This code does a few different things. It:

  1. Launches a coroutine.
  2. Runs a for loop from one to 100 inclusive.
  3. Delays the coroutine for five seconds. delay() checks for cancellation, so it’ll stop the loop if the job gets canceled.
  4. Calls emit on the mutable shared flow, passing it an instance of PriceVariation, which is an event from SharedViewEffects.

That emit(value: T) is one of the two event emission methods you can call on a shared flow. The alternative is to use tryEmit(value: T).

The difference between the two is that emit is a suspending function, while tryEmit isn’t. This small difference results in a huge behavioral contrast between the two methods. To explain this, though, you need to dive deep into shared flow’s replay cache and buffering. Buckle up!