Coordinating multiple view visibility with Flow and the combine operator

Aidan Low
ProAndroidDev
Published in
5 min readApr 7, 2021

--

Photo by Devin Lyster on Unsplash

This article will discuss how to use Flow and the combine operator to cleanly coordinate user interface elements whose visibilities are each dependent on multiple pieces of view model state.

When we separate our applications into a view model exposing LiveData objects and a UI layer observing those objects, it’s rare that we have a 1:1 correspondence of state to visibility of a particular view. Often, the visibility of a view will depend on multiple pieces of states within the view model.

Simple requirements

For example, consider a simple search screen in an application. This screen has a progress bar that displays while a search is being executed, and a RecyclerView with search results that is displayed when the search is not executing.

A simple solution

We can create a view model that exposes a LiveData for whether the search is in progress, and another for the actual search results. Our UI layer can simply observe those LiveData objects and update appropriately.

This works well enough for this simple case. We’ve avoided any complex logic outside the view model, and our visibility decisions are governed by the results of one LiveData object exposed by the view model.

More complex requirements

What if things are a bit more complicated? Consider a more complex version of this screen where we display special text when there are no results.

Solution 1: showHideUI() + many observations

A solution I’ve often seen (and sometimes implemented!) is to have a monolithic “showHideUI” function that centralizes all visibility updates in a single place. All observe lambdas would then call that, in addition to whatever other updates they make based on state. (such as populating the RecyclerView)

This will work, but introduces a number of issues.

  1. We’ve introduced complex logic into the UI layer, rather keeping it in the ViewModel where it can be centralized, encapsulated and easily tested.
  2. Each observer lambda now relies on all the other lambdas being configured. If an observer of LiveData 1 relies on the value of LiveData 2 and we forget to attach an observer to LiveData 2, it will never be updated and showHideUI() will never be called.
  3. This solution also doesn’t scale particularly well. If we have a situation where the view model exposes 10 independent LiveData objects, and we have 20 views that each rely on two of them, both showHideUI and all the observe lambdas that call it will become enormous and contain a ton of duplicated code. Not ideal.

Solution 2: MediatorLiveData in the UI layer

We can improve on this by using the same view model but using MediatorLiveData to observe the LiveData objects simultaneously. MediatorLiveData does require a fair amount of boilerplate code, although you can refactor this out into a custom component like Gaurav Goyal’s zipLiveData¹.

We’ve avoided the issue with missing observers and addressed the scaling issue, but we’re still evaluating complex logic in the UI layer, which can make this hard to test. We can do better.

Solution 3: zipLiveData in the view model

Let’s back up and try another approach, specifically exposing individual LiveData objects from the view model that will dictate the specific visibility of each view. We can move the MediatorLiveData objects within the view model to combine the existing MutableLiveData objects and expose a single LiveData for each visibility property that we care about. (as well as a LiveData for the search results)

This is much better. We’ve moved the logic from the UI layer into the view model, and it’s much easier to understand the code in the UI layer and in the view model. We can go one step further, though.

Solution 4: Combining flows in the view model

If we replace our MutableLiveData objects with StateFlow (and rewrite our doSearch function return a Flow) then we can use the Flow combine operator² and remove the MediatorLiveData boilerplate altogether. We can continue using the same simple fragment implementation.

So there we have it. Our visibility logic is all encapsulated in the view model and extremely readable, our UI layer is painfully simple, and we were able to use the Flow combine operator rather than add a ton of MediatorLiveData boilerplate. (or rely on non-standard implementations like zipLiveData)

Conclusion

If we only have a couple views or each view’s state only relies on a single piece of view model state, the original solution will work just fine. But in more complex applications, it simply doesn’t scale well. In our application, for example, search results (and the corresponding empty state) vary based on domain and on user-applied filters, and this approach worked nicely.

Using Flow and combine made this code easier to test, easier to understand, and easier to debug. By avoiding complex interactions between multiple LiveData objects, things are just simpler.

--

--