Advertisement
  1. Code
  2. Coding Fundamentals
  3. Design Patterns

Android Architecture Components: LiveData

Scroll to top
Read Time: 10 min
This post is part of a series called Android Architecture Components.
Android Architecture Components: Lifecycle and LiveModel
Android Architecture Components: the Room Persistence Library

We've already covered a lot of ground in our Android Architecture Components series. We started out talking about the idea behind the new architecture and looking at the key components presented at Google I/O. In the second post, we began our deep exploration of the main components of the package, taking a close look at the Lifecycle and LiveModel components. In this post, we'll continue to explore the Architecture Components, this time analyzing the awesome LiveData component.

I'm assuming that you're familiar with the concepts and components covered in the last tutorials, like Lifecycle, LifecycleOwner, and LifecycleObserver. If you're not, take a look at the first post in this series, where I discuss the general idea behind the new Android Architecture and its components. 

We'll continue to build on the sample application we started in the last part of this series. You can find it in the tutorial GitHub repo.

1. The LiveData Component

LiveData is a data holder. It is capable of being observed, it can hold any kind of data, and on top of that, it is also lifecycle-aware. In practical terms, LiveData can be configured to only send data updates when its observer is active. Thanks to its lifecycle awareness, when observed by a LifecycleOwner the LiveData component will send updates only when the observer's Lifecycle is still active, and it will remove the observed relation once the observer's Lifecycle is destroyed.

The LiveData component has many interesting characteristics:

  • prevents memory leaks when the observer is bound to a Lifecycle
  • prevents crashes due to stopped activities 
  • data is always up to date
  • handles configuration changes smoothly
  • makes it possible to share resources
  • automatically handles lifecycle

2. Observing LiveData

A LiveData component sends data updates only when its observer is "active". When observed by a LifecycleOwner, the LiveData component considers the observer to be active only while its Lifecycle is on the states STARTED or RESUMED, otherwise it will regard the observer as inactive. During the observer's inactive state, LiveData will stop the data update stream, until its observer becomes active once again. If the observer is destroyed, LiveData will remove its reference to the observer.

LiveData update flowLiveData update flowLiveData update flow

To achieve this behavior, LiveData creates a tight relation with the observer's Lifecycle when observed by a LifecycleOwner. This capacity makes it easier to avoid memory leaks when observing LiveData. However, if the observing process is called without a LifecycleOwner, the LiveData component won't react to Lifecycle state, and the observer's status must be handled manually.

To observe a LiveData, call observe(LifecycleOwner, Observer<T>) or observeForever(Observer<T>)

  • observe(LifecycleOwner, Observer<T>): This is the standard way to observe a LiveData. It ties the observer to a Lifecycle, changing LiveData's active and inactive state according to the LifecycleOwner current state.
  • observeForever(Observer<T>): This method doesn't use a LifecycleOwner, so the LiveData won't be able to respond to Lifecycle events. When using this method, it is very important to call removeObserver(Observer<T>), otherwise the observer may not be garbage collected, causing a memory leak.
1
// called from a LifecycleOwner
2
location.observe(
3
        // LifecycleOwner
4
        this, 
5
        // creating an observer
6
        Observer {
7
            location ->
8
            info("location: ${location!!.latitude}, 
9
                ${location.longitude}")
10
        })
11
}
12
13
// Observing without LifecycleOwner
14
val observer = Observer {
15
        location ->
16
        info("location: ${location!!.latitude}, 
17
        ${location.longitude}")
18
    })
19
location.observeForever(observer)
20
21
// when observer without a LivecyleOwner
22
// it is necessary to remove the observers at some point
23
location.removeObserver( observer )

3. Implementing LiveData

The generic type in the LiveData<T> class defines the type of data that will be held. For example, LiveData<Location> holds Location data. Or LiveData<String> holds a String

There are two main methods that must be considered in the component implementation: onActive() and onInactive(). Both methods react to the observer's state.

Example Implementation

In our sample project we used lots of LiveData objects, but we only implemented one: LocationLiveData. The class deals with GPS Location, passing the current position only for an active observer. Notice that the class updates its value on the onLocationChanged method, passing to the current active observer the refreshed Location data.

1
class LocationLiveData
2
    @Inject
3
    constructor(
4
            context: Context
5
    ) : LiveData<Location>(), LocationListener, AnkoLogger {
6
7
    private val locationManager: LocationManager =
8
            context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
9
10
    @SuppressLint("MissingPermission")
11
    override fun onInactive() {
12
        info("onInactive")
13
        locationManager.removeUpdates(this)
14
    }
15
16
    @SuppressLint("MissingPermission")
17
    fun refreshLocation() {
18
        info("refreshLocation")
19
        locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER, this, null )
20
    }
21
}

4. The MutableLiveData Helper Class

The MutableLiveData<T> is a helper class that extends LiveData, and exposes postValue and setValue methods. Other than that, it behaves exactly like its parent. To use it, define the type of data that it holds, like MutableLiveData<String> to hold a String, and create a new instance.

1
val myData: MutableLiveData<String> = MutableLiveData()

To send updates to an observer, call postValue or setValue. The behavior of these methods is quite similar; however, setValue will directly set a new value and can only be called from the main thread, while postValue creates a new task on the main thread to set the new value and can be called from a background thread.

1
fun updateData() {
2
    // must be called from the main thread
3
    myData.value = api.getUpdate
4
}
5
fun updateDataFromBG(){
6
    // may be called from bg thread
7
    myData.postValue(api.getUpdate)
8
}

It is important to consider that, since the postValue method creates a new Task and posts on the main thread, it will be slower than direct calls to setValue.

5. Transformations of LiveData

Eventually, you'll need to change a LiveData and propagate its new value to its observer. Or maybe you need to create a chain reaction between two LiveData objects, making one react to changes on another. To deal with both situations, you may use the Transformations class.

Map Transformations

Transformations.map applies a function on a LiveData instance and dispatches the result to its observers, giving you the opportunity to manipulate the data value.

Transformationsmap flowTransformationsmap flowTransformationsmap flow

It is very easy to implement Transformations.map. All you'll have to do is provide a LiveData to be observed and a Function to be called when the observed LiveData changes, remembering that the Function must return the new value of the transformed LiveData.

Suppose that you have a LiveData that must call an API when a String value, like a search field, changes.

1
// LiveData that calls api
2
// when 'searchLive' changes its value
3
val apiLive: LiveData<Result> = Transformations.map(
4
            searchLive,
5
            {
6
                query -> return@map api.call(query)
7
            }
8
    )
9
10
// Every time that 'searchLive' have
11
// its value updated, it will call
12
// 'apiLive' Transformation.map
13
fun updateSearch( query: String ) {
14
    searchLive.postValue( query )
15
}

SwitchMap Transformations

The Transformations.switchMap is quite similar to Transformations.map, but it must return a LiveData object as a result. It's a little harder to use, but it allows you to construct powerful chain reactions. 

In our project, we used Transformations.switchMap to create a reaction between LocationLiveData and ApiResponse<WeatherResponse>

  1. Our Transformation.switchMap observes LocationLiveData changes.
  2. The LocationLiveData updated value is used to call the MainRepository to get the weather for the specified location.
  3. The repository calls the OpenWeatherService that produces a LiveData<ApiResponse<WeatherResponse>> as a result.
  4. Then, the returned LiveData is observed by a MediatorLiveData, which is responsible for modifying the received value and updating the weather exhibited in the view layer.
1
class MainViewModel
2
@Inject
3
constructor(
4
        private val repository: MainRepository
5
)
6
    : ViewModel(), AnkoLogger {
7
    
8
    // Location
9
    private val location: LocationLiveData = 
10
        repository.locationLiveDa()
11
    
12
    private var weatherByLocationResponse:
13
            LiveData<ApiResponse<WeatherResponse>> = 
14
                Transformations.switchMap(
15
            location,
16
            {
17
                l ->
18
                info("weatherByLocation: \nlocation: $l")
19
                return@switchMap repository.getWeatherByLocation(l)
20
            }
21
    )
22
            
23
}

Watch out for time-consuming operations in your LiveData transformations, though. In the code above, both Transformations methods run on the main thread.

6. The MediatorLiveData

MediatorLiveData is a more advanced type of LiveData. It has capabilities very similar to those of the Transformations class: it is able to react to other LiveData objects, calling a Function when the observed data changes. However, it has many advantages when compared with Transformations, since it doesn't need to run on the main thread and can observe multiple LiveDatas at once.

To observe a LiveData, call addSource(LiveData<S>, Observer<S>), making the observer react to the onChanged method from the given LiveData. To stop the observation, call removeSource(LiveData<S>).

1
val mediatorData: MediatorLiveData<String> = MediatorLiveData()
2
        mediatorData.addSource(
3
                dataA,
4
                {
5
                    value -> 
6
                    // react to value
7
                    info("my value $value")
8
                }
9
        )
10
        mediatorData.addSource(
11
                dataB,
12
                {
13
                    value ->
14
                    // react to value
15
                    info("my value $value")
16
                    // we can remove the source once used
17
                    mediatorData.removeSource(dataB)
18
                }
19
        )

In our project, the data observed by the view layer that contains the weather to exhibit is a MediatorLiveData. The component observes two other LiveData objects: weatherByLocationResponse, which receives weather updates by location, and weatherByCityResponse, which receives weather updates by city name. Each time that these objects are updated, weatherByCityResponse will update the view layer with the current weather requested.

In the MainViewModel, we observe the LiveData and provide the weather object to view.

1
class MainViewModel
2
@Inject
3
constructor(
4
        private val repository: MainRepository
5
)
6
    : ViewModel(), AnkoLogger {
7
    
8
    // ...
9
    // Value observed by View.
10
    // It transform a WeatherResponse to a WeatherMain.
11
    private val weather:
12
            MediatorLiveData<ApiResponse<WeatherMain>> = 
13
                MediatorLiveData()
14
    
15
    // retrieve weather LiveData
16
    fun getWeather(): LiveData<ApiResponse<WeatherMain>> {
17
        info("getWeather")
18
        return weather
19
    }
20
    
21
    private fun addWeatherSources(){
22
        info("addWeatherSources")
23
        weather.addSource(
24
                weatherByCityResponse,
25
                {
26
                    w ->
27
                    info("addWeatherSources: \nweather: ${w!!.data!!}")
28
                    updateWeather(w.data!!)
29
                }
30
        )
31
        weather.addSource(
32
                weatherByLocationResponse,
33
                {
34
                    w ->
35
                    info("addWeatherSources: weatherByLocationResponse: \n${w!!.data!!}")
36
                    updateWeather(w.data!!)
37
                }
38
        )
39
40
    }
41
    
42
    private fun updateWeather(w: WeatherResponse){
43
        info("updateWeather")
44
        // getting weather from today
45
        val weatherMain = WeatherMain.factory(w)
46
        // save on shared preferences
47
        repository.saveWeatherMainOnPrefs(weatherMain)
48
        // update weather value
49
        weather.postValue(ApiResponse(data = weatherMain))
50
    }
51
    
52
    init {
53
        // ...
54
        addWeatherSources()
55
    }
56
}

In the MainActivity, the weather is observed and its result is shown to the user.

1
    private fun initModel() {
2
        // Get ViewModel
3
        viewModel = ViewModelProviders.of(this, viewModelFactory)
4
                .get(MainViewModel::class.java)
5
6
        if (viewModel != null) {
7
8
            // observe weather
9
            viewModel!!.getWeather().observe(
10
                    this@MainActivity,
11
                    Observer {
12
                        r ->
13
                        if ( r != null ) {
14
                            info("Weather received on MainActivity:\n $r")
15
                            if (!r.hasError()) {
16
                                // Doesn't have any errors
17
                                info("weather: ${r.data}")
18
                                if (r.data != null)
19
                                    setUI(r.data)
20
                            } else {
21
                                // error
22
                                error("error: ${r.error}")
23
                                isLoading(false)
24
                                if (r.error!!.statusCode != 0) {
25
                                    if (r.error!!.message != null)
26
                                        toast(r.error.message!!)
27
                                    else
28
                                        toast("An error occurred")
29
                                }
30
                            }
31
                        }
32
                    }
33
            )
34
35
        }
36
    }

The MediatorLiveData was also used as the basis of an object that handles calls to the OpenWeatherMap API. Take a look at this implementation; it's more advanced than the ones above, and it's really worth studying. If you're interested, take a look at OpenWeatherService, paying special attention to the Mediator<T> class.

7. Conclusion

We're almost at the end of our exploration of Android's Architecture Components. By now, you should understand enough to create some powerful apps. In the next post, we'll explore Room, an ORM that wraps SQLite and can produce LiveData results. The Room component fits perfectly in this Architecture, and it is the final piece of the puzzle.

See you soon! And in the meantime, check out some of our other posts on Android app development!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.