Goodbye LiveData, Hello SharedFlow

Replacing LiveData with SharedFlow

Melih Aksoy
Flat Pack Tech
5 min readFeb 16, 2021

--

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 offermethod 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 LiveDatacreate aSharedFlow with replay = 1 and onBufferOverflow = BufferOverflow.DROP_OLDEST
  • Be conscious when using tryEmit
  • Remember collection is not lifecycle aware like LiveData
  • collect is suspending. If you have some code in launchblock but after where you collect the flow, it may not get executed as you expect. Remember SharedFlow is a hot flow !

--

--