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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Reactive Streams on Kotlin: SharedFlow and StateFlow
30 mins
- Getting Started
- SharedFlow
- Handling Shared Events
- Event Emission With SharedFlow
- Replay and Buffering
- Subscribing to Event Emissions
- Collecting the SharedFlow
- Applying the Stream Data to the View
- SharedFlow and Channels
- StateFlow
- Handling App State
- Event Emission With StateFlow
- Subscribing to State Updates
- StateFlow and Channels
- Hot Flows, RxJava and LiveData
- Challenge: Using SharedFlow To Handle Screen Events
- Where to Go From Here?
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.
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.
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:
- Implement an event stream with
SharedFlow
that emits events shared between screens. - Refactor CryptoStonks5000 to use
StateFlow
to handle view state.
The project follows a Clean Architecture approach and MVVM pattern.
Build and run the project just to make sure everything is working. After that, it’s time to learn about shared flows!
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.
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:
- You call
MutableSharedFlow
. This creates a mutable shared flow that emits events of typeSharedViewEffects
, 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. - 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:
- Launches a coroutine.
- Runs a
for
loop from one to 100 inclusive. - Delays the coroutine for five seconds.
delay()
checks for cancellation, so it’ll stop the loop if the job gets canceled. - Calls
emit
on the mutable shared flow, passing it an instance ofPriceVariation
, which is an event fromSharedViewEffects
.
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!