• Britt Barak

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 that 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 helpful 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 on how it will be implemented:

  • Repository fetches the data

  • Repository updates LiveData<DataModel> value with the new data

  • UseCase (which observes Repository.LiveData<DataModel>) is notified on the new data.

  • UseCase transforms LiveData<DataModel> into LiveData<ViewModel> and updates its LiveData<ViewModel>’s value

  • Presentation (which observes UseCase.LiveData<ViewModel>) 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, as it was discussed on previous posts. Only thing left to sort out is the transformation part mentioned.


Transformations.map()

is a simple and elegant way to create the transformation. The map() method takes aLiveData 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<List<VenueViewModel>> getVenues(String location) {

    LiveData<List<VenueData>> 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<VenueViewModel> createVenuesViewModel(List<VenueData> list) {
    List<VenueViewModel> 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

  • Transformations.map() returns the updated viewModelsLiveD


That's all 😁



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: return the number of items, or return the top item…


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 asynchronous action. For that reason it returns a LiveDatarather than a list of venues in the first place: so it can be updated asynchronously when venues return from a network call or a database or wherever it takes some time to return from.


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



Transformations.switchMap()


LiveData locationLiveD = ...;
LiveData<List<VenueData>> dataModelsLiveD = 
    
    Transformations.switchMap(locationLiveD,
        newLocation -> repository.getVenues(newLocation));

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


Therefore, 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 Aviv → then dataModelsLiveD is notified with venues in Tel Aviv.

  • For whatever reason, the data (=meaning venues) in Tel Aviv change → then dataModelsLiveD is notified with new venues in Tel Aviv.

  • location is set to New York → then dataModelsLiveD is notified with venues in New York

  • Venues in Tel Aviv change again → then no one is even notified about it! (- assuming no one else observes dataModelsLiveD)


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 should be done there!



Chaining transformations

Transformations allow us to chain tasks we want to perform once a value changes. For example:


//Holds the location in a the form of [lat,lng], as is convenient for the UI:
LiveData<double[]> locationLiveD = ...;
// Creates a string format from the location, as Repository requires
LiveData<String> locationStrLiveD = 
Transformations.map(locationLiveD, newLocation ->
        String.format("%0s,%1s", newLocation[0], newLocation[1]));


//Gets the venue DataModels at the location from Repository
LiveData<List<VenueData>> dataModelsLiveD = Transformations.switchMap(locationStrLiveD,
        newLocationStr -> repository.getVenues(newLocationStr));


//Transforms DataModels to ViewModels
LiveData<List<VenueViewModel>> 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 an easier format for the UI.

  2. locationStrLiveD observes locationLiveD. Whenever a new location is available, it updates the value from double[] to the String format that Repository requires in order to actually fetch the venues.

  3. dataModelsLiveD observes locationStrLiveD. Whenever a new location string is available it is used to ask Repository for the venues (repository.getVenues())

  4. viewModelsLiveD observes dataModelsLiveD. Whenever new venues are available from Repository, it transforms them to ViewModel objects that the UI can display.

  5. The UI observes viewModelsLiveD. Whenever a list of new venue ViewModels are available, it just goes a head and displays it.


This 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 cooler 😎


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 official documentation.

CONTACT ME

  • Black Twitter Icon
  • Black Facebook Icon
  • Black LinkedIn Icon

Want to record an episode with me? Want to invite me to speak at your conference? let me know!