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.
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
or observeForever(Observer
.
-
observe(LifecycleOwner, Observer
: This is the standard way to observe a) LiveData
. It ties the observer to aLifecycle
, changingLiveData
‘s active and inactive state according to theLifecycleOwner
current state. -
observeForever(Observer
: This method doesn’t use a) LifecycleOwner
, so theLiveData
won’t be able to respond toLifecycle
events. When using this method, it is very important to callremoveObserver(Observer
, otherwise the observer may not be garbage collected, causing a memory leak.)
// called from a LifecycleOwner location.observe( // LifecycleOwner this, // creating an observer Observer { location -> info("location: ${location!!.latitude}, ${location.longitude}") }) } // Observing without LifecycleOwner val observer = Observer { location -> info("location: ${location!!.latitude}, ${location.longitude}") }) location.observeForever(observer) // when observer without a LivecyleOwner // it is necessary to remove the observers at some point location.removeObserver( observer )
3. Implementing LiveData
The generic type in the LiveData
class defines the type of data that will be held. For example, LiveData
holds Location
data. Or LiveData
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.
class LocationLiveData @Inject constructor( context: Context ) : LiveData(), LocationListener, AnkoLogger { private val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager @SuppressLint("MissingPermission") override fun onInactive() { info("onInactive") locationManager.removeUpdates(this) } @SuppressLint("MissingPermission") fun refreshLocation() { info("refreshLocation") locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER, this, null ) } }
4. The MutableLiveData
Helper Class
The MutableLiveData
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
to hold a String
, and create a new instance.
val myData: MutableLiveData= 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.
fun updateData() { // must be called from the main thread myData.value = api.getUpdate } fun updateDataFromBG(){ // may be called from bg thread myData.postValue(api.getUpdate) }
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.
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.
// LiveData that calls api // when 'searchLive' changes its value val apiLive: LiveData= Transformations.map( searchLive, { query -> return@map api.call(query) } ) // Every time that 'searchLive' have // its value updated, it will call // 'apiLive' Transformation.map fun updateSearch( query: String ) { searchLive.postValue( query ) }
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
.
- Our
Transformation.switchMap
observesLocationLiveData
changes. - The
LocationLiveData
updated value is used to call theMainRepository
to get the weather for the specified location. - The repository calls the
OpenWeatherService
that produces aLiveData
as a result.> - Then, the returned
LiveData
is observed by aMediatorLiveData
, which is responsible for modifying the received value and updating the weather exhibited in the view layer.
class MainViewModel @Inject constructor( private val repository: MainRepository ) : ViewModel(), AnkoLogger { // Location private val location: LocationLiveData = repository.locationLiveDa() private var weatherByLocationResponse: LiveData> = Transformations.switchMap( location, { l -> info("weatherByLocation: nlocation: $l") return@switchMap repository.getWeatherByLocation(l) } ) }
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 LiveData
s at once.
To observe a LiveData
, call addSource(LiveData
, making the observer react to the , Observer)onChanged
method from the given LiveData
. To stop the observation, call removeSource(LiveData
.)
val mediatorData: MediatorLiveData= MediatorLiveData() mediatorData.addSource( dataA, { value -> // react to value info("my value $value") } ) mediatorData.addSource( dataB, { value -> // react to value info("my value $value") // we can remove the source once used mediatorData.removeSource(dataB) } )
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.
class MainViewModel @Inject constructor( private val repository: MainRepository ) : ViewModel(), AnkoLogger { // ... // Value observed by View. // It transform a WeatherResponse to a WeatherMain. private val weather: MediatorLiveData> = MediatorLiveData() // retrieve weather LiveData fun getWeather(): LiveData > { info("getWeather") return weather } private fun addWeatherSources(){ info("addWeatherSources") weather.addSource( weatherByCityResponse, { w -> info("addWeatherSources: nweather: ${w!!.data!!}") updateWeather(w.data!!) } ) weather.addSource( weatherByLocationResponse, { w -> info("addWeatherSources: weatherByLocationResponse: n${w!!.data!!}") updateWeather(w.data!!) } ) } private fun updateWeather(w: WeatherResponse){ info("updateWeather") // getting weather from today val weatherMain = WeatherMain.factory(w) // save on shared preferences repository.saveWeatherMainOnPrefs(weatherMain) // update weather value weather.postValue(ApiResponse(data = weatherMain)) } init { // ... addWeatherSources() } }
In the MainActivity
, the weather is observed and its result is shown to the user.
private fun initModel() { // Get ViewModel viewModel = ViewModelProviders.of(this, viewModelFactory) .get(MainViewModel::class.java) if (viewModel != null) { // observe weather viewModel!!.getWeather().observe( this@MainActivity, Observer { r -> if ( r != null ) { info("Weather received on MainActivity:n $r") if (!r.hasError()) { // Doesn't have any errors info("weather: ${r.data}") if (r.data != null) setUI(r.data) } else { // error error("error: ${r.error}") isLoading(false) if (r.error!!.statusCode != 0) { if (r.error!!.message != null) toast(r.error.message!!) else toast("An error occurred") } } } } ) } }
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
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!
-
Android SDKHow to Use the Google Cloud Vision API in Android Apps
-
JavaAndroid Design Patterns: The Observer Pattern
-
KotlinKotlin From Scratch: Variables, Basic Types, and Arrays
-
KotlinKotlin From Scratch: Nullability, Loops, and Conditions
-
Android SDKWhat Are Android Instant Apps?
Powered by WPeMatico