Goodbye LiveData, Hello SharedFlow
Replacing LiveData with SharedFlow

Recently, I was working on refactoring an old module in our app, and had the chance to see if using SharedFlow
as a replacement for LiveData
would be practical. The goal of this article is showcasing a few scenarios you can stumble upon, and how coroutines handle them (hopefully without writing extra code for functionality).
Some of the cases were not as straightforward as just collecting stuff, so I thought it’d be interesting to share my findings.
P.S. I’ll not focus on collecting / lifecycle awareness in this article.
Overview of ViewModel
Before going into details, I want to give an overview of what kind of requirements we had in ViewModel.
We have two data fields, Ratings and Reviews. Each is requested separetly, and returns a Result
which can be Success
, Failure
or a State
. All these have their own data holders that are being observed (eg, ratings
, ratingsError
, ratingsState
). They have different handling requirements than others.
We also have a userEvent
data container. An event
is when user interacts with app that should change ui as a result, eg. when user wants to see sorting options, an Event.SortingOptions(options)
is fired with available sorting options.
Pretty straightforward!
Setting up SharedFlows for data fields
Ratings
Let’s start with defining our data containers for a successfull Ratings
fetch:
First of all, I don’t have a default or initial value for Ratings
. Thus I use SharedFlow
instead of StateFlow
.
SharedFlow
has three constructor parameters: replay
, extraBufferCapacity
and onBufferOverflow
. By default, it has 0 replay
and extraBufferCapacity
, and BufferOverflow.SUSPEND
as overflow strategy.
Ratings
is using replay = 1
because whenever I start observing ratings, I want to access current data. This would cover cases where I have multiple subscribers or resubscribe after a configuration change.
On a successfull fetch, I just set data to it via tryEmit
.
tryEmit
is non-suspending version of emit
, and returns a Boolean
indicating if it succesfully emmited the value or not. As explained per docs, if you create a SharedFlow
without any replay
or extraBufferCapacity
, tryEmit
will always return false
. You can think of it as offer
method of a deque that has capacity of replay + extraBufferCapacity
.
Although I request Ratings
once and there’s no other case where it’s requested again, checking the result and printing an error on failure guarantees that if any other team member tries to fetch Ratings
more then once later on, and emit fails, they’ll see it in console.
Reviews
Reviews
can be emitted multiple times based on events — like sorting or category. On top of this, there’s a different message to be shown if Reviews
has gone empty after selecting a category or not. If you have no reviews for a product from the start, it would say There're no reviews for this product
whilst if you chose a category and there are no reviews there, it’d say There're no reviews in category you chose
.
First and foremost, I’m interested in last two results, so replay = 2
. In case user is spamming categories, I won’t be caring about old results anymore, thus usingBufferOverflow.DROP_OLDEST
as overflow strategy.
Again, using tryEmit
to emit Reviews
, but not checking result this time. Since I’m using DROP_OLDEST
, tryEmit
will never fail, and keep dropping oldest values that are emitted.
Now, whenever I start collecting reviews
I’ll be getting latest two values. On top of this, to fullfill requirement for setting text, I can make use of reviews.replyCache
, eg. in fragment / activity
Voilà.
State
Handling state is not really different than Reviews
. State
, in this case, is just a simple Loading
and Loaded
.
They both have one replay, and drops oldest as new states are coming in. If you think of how I described data logic above, _ratingsState
will actually just receiveLoading
and Loaded
once, then return Loaded
every time it’s collected. _reviewState
however, may be updated as long as user is interacting.
On UI side, I don’t need two separate states for displaying loading indicator, so I combine _ratingsState
and _reviewsState
into state
as follows:
If both states are Loaded
, state
will emit Loaded
. However, combine
block will be called each time _ratingsState
or _reviewsState
is changed, and I don’t need to handle multiple Loading
states as loading will be visible until Loaded
is emited. Thus adding distinctUntilChanged()
flow operator to eliminate in-between states. This will keep posting correct states as user interacts with app and _reviewState
changes, and won’t trigger collection unless state actually changes!
Lastly, both states are set in a suspending manner, so non of the states are missed.
Events
Events are handled differently than other types: they are one shot. If there’s an event, it should be suspended until it is consumed. There shouldn’t be multiple subscribers to events.
Currently, there’s no out-of-the-box support in SharedFlow
for suspending sending values until there’s a subscriber, nor consuming sent value (to prevent multiple subscribers from processing same value).
For this, we can use a Channel
and expose it as flow by using consumeAsFlow
function.
This guarantees that _events
is suspended until there’s a receiver for the events. Since consumeAsFlow
is used, it’ll throw an IllegalStateException
if multiple subscriptions happen. If receiveAsFlow
is used, resulting flow
can be collected by multiple collectors, but collectors won’t receive same values, which can be handy when required.
Wrapping up
In the end, SharedFlow
& Channel
was able to cover up all cases I need for refactoring, and without having to code single extension / subclass for functionalities I required, which was what I aimed for :)
Few small tips I found out along the way for ease of use:
- To behave like
LiveData
create aSharedFlow
withreplay = 1
andonBufferOverflow = BufferOverflow.DROP_OLDEST
- Be conscious when using
tryEmit
- Remember collection is not lifecycle aware like
LiveData
collect
is suspending. If you have some code inlaunch
block but after where you collect the flow, it may not get executed as you expect. RememberSharedFlow
is a hot flow !