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?
Custom LiveData
You’ve seen various types of LiveData already in the library. And, you can always create a custom one by extending LiveData.
As an example, you can create a LiveData to observe internet connectivity status in your app.
Key things about Custom LiveData:
- LiveData calls
onActive()
when it has active observers. Register system resources like network callbacks or location updates here. - LiveData calls
onInactive()
when it has no active observers. Safe place to unregister system resources. - Invoke
setValue()
orpostValue()
when you want to notify active observers with the latest value.
Create Custom LiveData
First, create connectivity package at root location and add ConnectivityLiveData.kt file in it with class extending LiveData:
//1
class ConnectivityLiveData(private val connectivityManager: ConnectivityManager)
: LiveData<Boolean>() {
}
Next, add parameterized constructor in the class with application instance:
//2
constructor(application: Application) : this(application.getSystemService(Context
.CONNECTIVITY_SERVICE)
as ConnectivityManager)
Now add following code below constructor to handle network availability callbacks:
//3
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
//4
postValue(true)
}
override fun onLost(network: Network) {
super.onLost(network)
//5
postValue(false)
}
}
Next, override onActive()
and onInactive()
from LiveData class, and update them with the below code:
override fun onActive() {
super.onActive()
val builder = NetworkRequest.Builder()
//6
connectivityManager.registerNetworkCallback(builder.build(), networkCallback)
}
override fun onInactive() {
super.onInactive()
//7
connectivityManager.unregisterNetworkCallback(networkCallback)
}
Here’s what this code does:
- Create custom LiveData to hold
boolean
value for network connection available or unavailable. - Creates parameterized constructor with
Application
type parameter to help getContext.CONNECTIVITY_SERVICE
right inside the LiveData. - Handles the network availability that gets callback when network is available or lost. This is the recommended and latest way of handling connectivity changes.
- When network is available, it updates the value asynchronously to notify its active observers with
true
value. - When network is unavailable, it updates the value asynchronously to notify its active observers with
false
value. - Registers for network callbacks when there is at least one active observer for the LiveData.
- Unregisters for network callbacks when there are no active observers for the LiveData.
Custom LiveData also adheres to the S of S.O.L.I.D principles, i.e the Single Responsibility Principle. The Single Responsibility Principle (SRP) states that a class should have only one reason to change. The ConnectivityLiveData
class is responsible only for handling network connectivity availability throughout the application.
Consume Custom LiveData
First, open MovieListFragment.kt. Declare the following on the top where other variables are declared:
private lateinit var connectivityLiveData: ConnectivityLiveData
This declares class level variable connectivityLiveData
.
Next, initialise connectivityLiveData in onCreate()
function:
connectivityLiveData = ConnectivityLiveData(application)
This initializes ConnectivityLiveData
by passing application
as a parameter, to provide access to ConnectivityManager
inside ConnectivityLiveData
.
Next, remove the mainViewModel.onFragmentReady()
call from inside onActivityCreated()
.
Lastly, add observer in initialiseObservers()
function:
//1
connectivityLiveData.observe(viewLifecycleOwner, Observer { isAvailable ->
//2
when (isAvailable) {
true -> {
//3
mainViewModel.onFragmentReady()
statusButton.visibility = View.GONE
moviesRecyclerView.visibility = View.VISIBLE
searchEditText.visibility = View.VISIBLE
}
false -> {
statusButton.visibility = View.VISIBLE
moviesRecyclerView.visibility = View.GONE
searchEditText.visibility = View.GONE
}
}
})
Here’s what this code does:
- Observes the connectivity status and hides necessary UI elements.
- Using
when
statement, it shows or hides the elements on the screen when connectivity is available or lost. - Invoke
onFragmentReady()
when connection is available.
Build and run the app without a network connection.
Popular movies don’t show up because the network is unavailable. This is bad UX as the app is not displaying No internet text to the user.
To handle this, open fragment_movie_list.xml and update the android:visibility
attributes of the elements as below:
<!-- // 1 -->
<EditText
android:id="@+id/searchEditText"
android:visibility="gone" />
<!-- // 2 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/moviesRecyclerView"
android:visibility="gone" />
<!-- // 3 -->
<Button
android:id="@+id/statusButton"
android:visibility="visible" />
Here’s what this code does:
- Keeps the search field hidden initially.
- Hides the
moviesRecyclerView
initially. It becomes visible when movies load. - Makes
statusButton
visible at app launch.
If the network is available, you’ll load popular movies. It will show a loading indicator and hide statusButton
while movieLoadingStateLiveData
is in MovieLoadingState.LOADING
state. If network is unavailable, you’ll see statusButton
with No internet text by default.
Build and run the app with internet connectivity off. Then switch it on again.
Optimize API Calls
By now, you might have thought of a scenario in mind. What happens when you toggle WI-FI on/off two-three times? Unexpectedly, there will be more than one call to load popular movies from the API. This is not ideal and would cause unnecessary API calls, which might be costly. How would you reduce these calls to a single API call per app session?
To implement that, open MainViewModel.kt and replace onFragmentReady()
function with below code:
fun onFragmentReady() {
if (_popularMoviesLiveData.value.isNullOrEmpty()) {
fetchPopularMovies()
}
}
This stops invoking API more than once, by fetching popular movies only when _popularMoviesLiveData.value
is null or empty.
If the network is slow, you can notice the effects of these changes significantly. But, if not then you can add a Log
statement in the repository to track the number of calls to load popular movies. After adding the above code, you fetch popular movies only once.
Handle Phone Rotation
When you switch off the internet, you’ll see that the No internet text becomes visible as expected. However, if you rotate the phone now, you’ll notice that the movie list appears again, even though there is no connectivity. That’s because when the screen is rotated, the observers execute the code again if LiveData has some value.
To handle this, open MovieListFragment.kt and update MovieLoadingState.LOADED
case with the following code:
MovieLoadingState.LOADED -> {
connectivityLiveData.value?.let {
if (it) {
statusButton.visibility = View.GONE
moviesRecyclerView.visibility = View.VISIBLE
} else {
statusButton.visibility = View.VISIBLE
moviesRecyclerView.visibility = View.GONE
}
}
loadingProgressBar.visibility = View.GONE
}
Build and run the app. You’ll notice that this fixes the issue and adjusts the visibility of the statusButton
and moviesRecyclerView
based on internet connectivity.
Event State Handling
Events are an integral part of any application. Take an example of an event that captures the movie details, when you tap on a movie item to navigate to the movie detail screen. This event holds the movie name or movie id to show movie details on the new screen.
To add that, first open MainViewModel.kt. Inside the class, declare the following at the top where other variables are declared.
private val _navigateToDetails = MutableLiveData<String>()
val navigateToDetails: LiveData<String>
get() = _navigateToDetails
This declares and initialses LiveData as a backing property. Value of _navigateToDetails
can change only from MainViewModel
, navigateToDetails
is read-only outside of MainViewModel
.
Next, inside onMovieClicked()
function replace the TODO
comment by following:
movie.title?.let {
_navigateToDetails.value = it
}
Title of movie tapped from movies list will go in _navigateToDetails
.
At last, open MovieListFragment.kt and add an observer for navigateToDetails
in initialiseObservers()
function:
mainViewModel.navigateToDetails.observe(viewLifecycleOwner, Observer {
findNavController().navigate(MovieListFragmentDirections.actionMovieClicked(it))
})
Build and run the app. You’ll see that tapping on a movie in the list navigates to MovieDetailFragment.kt with movie title.