Clean, Easy & New- How To Architect Your App: Part 4 — LiveData Transformations

How to use map() and switchMap() to easily transform the value received from LiveData, for elegant code.

In the architecture I presented on previous posts (part1, part2, part3) I explained I like to use a UseCase object which is in charge of the interaction between the data layer and the UI / presentation layer. [If you haven’t yet, reading them will give you some needed context]

In other words, the UseCase either:

  • Gets the data model from the data layer’s Repository, transforms it into a view model object that the UI can easily display.

Or

  • Takes the view model which represents the current UI state, transforms it into a data model which can be sent to the Repository to update the data.

For simplicity let’s consider the case of GetVenues from Repository and display them.

In some more details about the implementation:

  • Repository fetches the data
  • Repository updates LiveData value with the data
  • UseCase (which observes Repository.LiveData) is notified on the new data.
  • UseCase transforms LiveData into LiveData and updates its LiveData’s value
  • Presentation (which observes UseCase.LiveData) is notified on the new data.
  • Presentation updates the UI according to the new ViewModel.

The flow is supposed to be pretty clear by now. Only thing left to sort out is the transformation part mentioned.

Transformations.map()

Is a very easy and elegant way to create this transformation. map() takes a LiveData and a Function.

It observes the LiveData. Whenever a new value is available, it takes the value, applies the Function on in, and sets the Function’s output as a value on the LiveData it returns.

So we can do something like this:

LiveData> getVenues(String location) {

LiveData> dataModelsLiveD =
repository.getVenues(location);
    viewModelsLiveD =
Transformations.map(dataModelsLiveD,
newData -> createVenuesViewModel(newData));
    return venuesViewModel;
}

To complete the picture, createVenuesViewModel() simply makes a list of viewModels out of list of dataModels:

List createVenuesViewModel(List list) {
List venuesVM = new ArrayList<>();
VenueViewModel venueViewModel;
    for (VenueData item : list) {
venueViewModel = createViewModel(item);
venuesVM.add(venueViewModel);
}
    return venuesVM;
}

What’s the flow here?

  • dataModelsLiveD changes
  • createVenuesViewModel() is applied on dataModelsLiveD’s value
  • This value is set on viewModelsLiveD which is returned by Transformations.map()

Pretty simple. 😁

There are tons of more nice useful stuff you might want to do with map(). To name a few:

  • format strings for display,
  • sorting , filtering or grouping the items
  • if you don’t really want the list but to calculate something from the result. like: number of items

Notice that we didn’t need to attach a LifecycleOwner object here, as opposed to the case where we’d attach an Observer to dataModelsLiveD.

Only if there’s an active observer attached to the trigger LiveData: map() will be calculated, with the LifecycleOwner given on the trigger LiveData — easier and safer for us!

We can say that map() is for applying a synchronous action with the new LiveData’s value. Whenever dataModel changes → create a viewModel out of it.

What if I have an asynchronous action to perform?

For example,

What if I want to get the new venues whenever a new location is set?

I have a LiveData that holds the location. Whenever it updates its value I want to get the new venues accordingly, meaning to perform repository.getVenues(location) with the new location.

After all, getVenue() is an async action. For that reason it returns a LiveData rather than a list of venues, so it can update asynchronously, when venues return from network call or database or wherever it takes time to return.

So we can’t use map(), since we need a LiveData to return, rather than a value. For these kind of tasks we have:

Transformations.switchMap()

LiveData locationLiveD = ...;
LiveData> dataModelsLiveD = 

Transformations.switchMap(locationLiveD,
newLocation -> repository.getVenues(newLocation));

Whenever locationLiveD is set with a new value (I called it newLocation) → repository.getVenues(newLocation) is called.

Observing dataModelsLiveD will result in a callback whenever locationLiveD is set.

A nice thing to remember:

Whenever a new value is set, the old value’s task won’t be calculated anymore.

For example:

  • We observe dataModelsLiveD
  • location is set to Tel AvivdataModelsLiveD is notified with venues in Tel Aviv.
  • For whatever reason, data, meaning venues in Tel Aviv change → dataModelsLiveD is notified with new venues in Tel Aviv.
  • location is set to New York dataModelsLiveD is notified with venues in New York
  • Venues in Tel Aviv change again- assuming no one else observes dataModelsLiveD, no one is even notified about it.

Remember that same as for map(), we didn’t give switchMap() any LifecycleOwner — so again, no memory leaks to be worried about.

Note that the Functions in both map() and switchMap() run on the Main Thread,

so no long running operations can be done there.

The transformations

allow us to do a chain of tasks we want to perform when a value is changed.

LiveData<double[]> locationLiveD = ...;
LiveData locationStrLiveD = Transformations.map(locationLiveD, newLocation ->
String.format("%s,%1s", newLocation[0], newLocation[1]));

LiveData> dataModelsLiveD = Transformations.switchMap(locationStrLiveD,
newLocationStr -> repository.getVenues(newLocationStr));

LiveData> viewModelsLiveD = Transformations.map(dataModelsLiveD, newData -> createVenuesViewModel(newData));

(* some safety checks were omitted for readability)

What happens here:

  1. The UI set locationLiveD with a new location of the type double[]. Why? Let’s say that it’s easier for the UI.
  2. locationStrLiveD observes locationLiveD, so whenever a new location is available, it updates the value from double[] to the String format that Repository needs in order to actually fetch the venues.
  3. dataModelsLiveD observes locationStrLiveD, so whenever a new location string is available it uses it to ask the repository for the venues (repository.getVenues())
  4. viewModelsLiveD observes dataModelsLiveD, so whenever new venues are available from the Repository, it transforms them to View Model objects that the UI can use to display.
  5. The UI observes viewModelsLiveD, so whenever a list of new venue view models are available it just goes a head and displays it.

It is a powerful tool!

Be careful, though, not to chain too many tasks that can cause you to end up with quite a mess.. The key to avoid this mess, in my opinion, is to remember how we divided our classes, which object is responsible for what, and although chaining could be elegant and cool.. having clear responsibilities and separation of concerns is so much better 😎

Next times

We’ll discuss how to update the UI’s list efficiently.

On a later post I’ll show some example on how to create your own Transformation, and what does it mean.

Thank you for reading!! 🙏❤️👏

If you’re interested, check out the previous posts:

Part1: Structure, Part2: Persistence, part3: Network Calls

And Transformations documentation: https://developer.android.com/reference/android/arch/lifecycle/Transformations.html


Clean, Easy & New- How To Architect Your App: Part 4 — LiveData Transformations was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Please follow and like us:
0

Gurupriyan is a Software Engineer and a technology enthusiast, he’s been working on the field for the last 6 years. Currently focusing on mobile app development and IoT.