LiveData Tutorial for Android: Deep Dive
In this Android tutorial, you’ll learn about LiveData which is a core architecture component, and how to use it to its full potential in your app. By Prateek Sharma.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
LiveData Tutorial for Android: Deep Dive
30 mins
- Getting Started
- Lifecycle Aware Components
- LiveData and MutableLiveData
- Initialize, Update, Observe LiveData
- Synchronous vs Asynchronous Update
- LiveData in Action: Example One – Implement Search
- LiveData in Action: Example Two – Update Progress
- Transformations
- Transformations.map v/s Transformations.switchMap
- Transformations.switchMap – Example
- MediatorLiveData
- Example
- Custom LiveData
- Create Custom LiveData
- Consume Custom LiveData
- Optimize API Calls
- Handle Phone Rotation
- Event State Handling
- Multiple Event Emit Problem
- Solution
- RxJava vs LiveData
- Where to Go From Here?
LiveData in Action: Example Two – Update Progress
You’ve created your first LiveData in no time. But, the app is missing out on good UX. Having a ProgressBar
to give the loading feedback will bring life to the application and provide a good user experience.
It’s time to take a look at fragment_movie_list.xml file under layout directory in res folder. You’ll notice it has a ProgresBar
. Also checkout MovieLoadingState
enum, which defines three states. Using those states, the visibility of the views inside the fragment is changed. Checkout the code inside onMovieLoadingStateChanged()
function in MovieListFragment.kt.
Next, you need to make changes inside MainViewModel.kt to utilize the MovieLoadingState
enum and call to update the views.
Open MainViewModel.kt and add following to the top where other variables are declared:
val movieLoadingStateLiveData = MutableLiveData<MovieLoadingState>()
This declares LiveData to hold MovieLoadingState
value. Now, replace fetchMovieByQuery()
function with the following:
private fun fetchMovieByQuery(query: String) {
viewModelScope.launch(Dispatchers.IO) {
try {
//1
withContext(Dispatchers.Main) {
movieLoadingStateLiveData.value = MovieLoadingState.LOADING
}
val movies = repository.fetchMovieByQuery(query)
searchMoviesLiveData.postValue(movies)
//2
movieLoadingStateLiveData.postValue(MovieLoadingState.LOADED)
} catch (e: Exception) {
//3
movieLoadingStateLiveData.postValue(MovieLoadingState.INVALID_API_KEY)
}
}
}
Here’s what this code does:
- Before fetching movies from repository, update
movieLoadingState
withLOADING
state. One way is to useDispatchers.MAIN
(foreground) because you are updating LiveData from withinDispatcher.IO
(background) block of coroutine. - Alternatively, you can use
postValue
withoutDispatchers.MAIN
context, as explained in the Synchronous vs Asynchronous section above. UpdatemovieLoadingState
toLOADED
after you fetch movies from repository. - Update
movieLoadingState
withMovieLoadingState.INVALID_API_KEY
state, when repository throws exception.
As a last step, open MovieListFragment.kt, and add the following inside initialiseObservers()
function:
mainViewModel.movieLoadingStateLiveData.observe(viewLifecycleOwner, Observer {
onMovieLoadingStateChanged(it)
})
This adds an observer that will show and hide progress bar and recycler view based on movieLoadingState
.
movieLoadingState
without Dispatchers.Main
in coroutine with Dispatcher.IO
context, the app will crash because only UI thread should observe LiveData and not the background thread. As soon as LiveData updates, active observers receive the latest value.You’ve made good amendments to the app. Build and run the app. You should be able to see the progress bar now based on the state of the search.
Transformations
Transformations help to perform operations on the LiveData before dispatching values to the active Observers.
An example where Transformations are beneficial is when the repository itself returns LiveData. When repository returns LiveData, ViewModel needs to be a Lifecycle owner to observe the LiveData. ViewModel can’t be a Lifecycle owner because they depend on other Lifecycle owners like Fragment or Activity.
What do you do then? That’s where Transformations.switchMap
helps, by creating a new LiveData that reacts to changes in other LiveData instances.
Transformations.map v/s Transformations.switchMap
LiveData provides two types of transformations. As per the official documentation:
-
Transformations.map
like RxJava’smap
, applies an operation on value stored in LiveData and propagates the result value downstream. The function passed tomap
returns type of the data that LiveData holds. Use it when you want to manipulate the data before it goes to UI. -
Transformations.switchMap
like RxJava’sflatMap
, applies an operation on value stored in LiveData, unwraps and dispatches the result downstream. The function passed toswitchMap
returns LiveData. Use it when your repository itself returns LiveData.
Transformations.switchMap – Example
Let’s see how you can use Transformations in the app.
First, replace the fetchMovieByQuery()
function in MainViewModel.kt with the function below:
//1
private fun fetchMovieByQuery(query: String): LiveData<List<Movie>> {
//2
val liveData = MutableLiveData<List<Movie>>()
viewModelScope.launch(Dispatchers.IO) {
val movies = repository.fetchMovieByQuery(query)
//3
liveData.postValue(movies)
}
//4
return liveData
}
Here’s what this code does:
- Update the
fetchMovieByQuery()
to return LiveData of List of Movies. - Use local variable
liveData
which will hold movies from the repository. - Instead of
searchMoviesLiveData
, update theliveData
with movies. - Returns LiveData of List of Movies, you’ll see how to use this returned list in the code snippets below.
Next, in the same class, replace the declaration of the existing searchMoviesLiveData
variable with the following:
//1
var searchMoviesLiveData: LiveData<List<Movie>>
Add the following at the top of the class below the searchMoviesLiveData
variable:
//2
private val _searchFieldTextLiveData = MutableLiveData<String>()
//3
init {
searchMoviesLiveData = Transformations.switchMap(_searchFieldTextLiveData) {
fetchMovieByQuery(it)
}
}
Lastly, inside the onSearchQuery()
function, replace the call to fetchMovieByQuery(query)
method, with the following:
//4
_searchFieldTextLiveData.value = query
Here’s what this code does:
- Makes
searchMoviesLiveData
a non final LiveData as it will update when_searchFieldTextLiveData
updates. -
_searchFieldTextLiveData
will hold the search term entered by the user. - When
_searchFieldTextLiveData
changes, it callsfetchMovieByQuery()
and assigns list of movies tosearchMoviesLiveData
. - Triggers the call to
Transformations.switchMap()
with latest search term entered by user. Notice coding convention for private fields in Kotlin.
Build and run the app. You shouldn’t experience any changes in the behavior. However you’ve now stored the search query in _searchFieldTextLiveData
, which will be helpful if you rotate the screen and want to populate the search field with the earlier search query.
MediatorLiveData
It’s surprising to know that Transformations use MediatorLiveData
under the hood. It merges more than one LiveData into one. Observers of MediatorLiveData gets value when any of the LiveData’s value changes. And the fun part is, you can also create your custom Transformations using MediatorLiveData like map
and switchMap
.
Imagine, if you want to show popular movies when the MovieApp loads, together with the existing search functionality. Without MediatorLiveData you’ll end up adding movies in a third LiveData manually, which in longer-term will hinder the app’s scalability.
Example
First, open the MainViewModel.kt. Inside the class, declare the following at the top where other variables are declared.
//1
private val _popularMoviesLiveData = MutableLiveData<List<Movie>>()
//2
val moviesMediatorData = MediatorLiveData<List<Movie>>()
Then replace the searchMoviesLiveData
variable with the following:
//3
private var _searchMoviesLiveData: LiveData<List<Movie>>
Here’s what this code does:
- Declares a movies data holder for popular movies. Declaring it as
private
makes it inaccessible outside MainViewModel.kt. - Declares a movie data holder
moviesMediatorData
, which is public and the only source of movies for UI. - Updates
searchMoviesLiveData
visibility to private._popularMoviesLiveData
and_searchMoviesLiveData
will be merged in onemoviesMediatorData
.
Next, add sources to moviesMediatorData
by replacing init
block with the following code:
init {
_searchMoviesLiveData = Transformations.switchMap(_searchFieldTextLiveData) {
fetchMovieByQuery(it)
}
//1
moviesMediatorData.addSource(_popularMoviesLiveData) {
moviesMediatorData.value = it
}
//2
moviesMediatorData.addSource(_searchMoviesLiveData) {
moviesMediatorData.value = it
}
}
Next, inside onFragmentReady()
function, replace TODO
comment by the following:
//3
fetchPopularMovies()
Here’s what this code does:
- This adds source to
moviesMediatorData
. When_popularMoviesLiveData
value changes,moviesMediatorData
also updates and thus the updating the UI. - This attaches a second source to
moviesMediatorData
. Depending on specific business use case, you can show searched results and popular movies combined or one at a time. - You’ll invoke
onFragmentReady()
from MovieListFragment.kt in the code snippets below.fetchPopularMovies()
fetches movies from repository.
Next, replace fetchPopularMovies()
with the following:
private fun fetchPopularMovies() {
//1
movieLoadingStateLiveData.value = MovieLoadingState.LOADING
viewModelScope.launch(Dispatchers.IO) {
try {
//2
val movies = repository.fetchPopularMovies()
_popularMoviesLiveData.postValue(movies)
//3
movieLoadingStateLiveData.postValue(MovieLoadingState.LOADED)
} catch (e: Exception) {
//4
movieLoadingStateLiveData.postValue(MovieLoadingState.INVALID_API_KEY)
}
}
}
Here’s what this code does:
- Before fetching movies from repository, update the UI with
MovieLoadingState.LOADING
state. - Fetches movies from repository and stores them in
_popularMoviesLiveData
. This will also update the movies inmoviesMediatorData
for UI to observe. - After fetching movies from repository, update the UI with
MovieLoadingState.LOADED
state. This will hide the loading indicator and show the recyclerview. - Update
movieLoadingState
withMovieLoadingState.INVALID_API_KEY
state, when repository throws exception.
Next, open MovieListFragment.kt and inside onActivityCreated() add the following code:
//1
mainViewModel.onFragmentReady()
Lastly, remove observer for searchMoviesLiveData
from initialiseObservers()
function. Then add an observer for moviesMediatorData
in it:
//2
mainViewModel.moviesMediatorData.observe(viewLifecycleOwner, Observer {
movieAdapter.updateData(it)
})
Here’s what this code does:
- This tells
mainViewModel
that UI is ready with basic initialization.onFragmentReady()
callsfetchPopularMovies()
. - This observes
moviesMediatorData
, which is the only source of data formovieAdapter
. It combines updates of both_popularMoviesLiveData
and_searchMoviesLiveData
.
You’ve seen how scalable and easy it is to combine two or more LiveDatas into one. Now, build and run the app.