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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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() or postValue() 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:

  1. Create custom LiveData to hold boolean value for network connection available or unavailable.
  2. Creates parameterized constructor with Application type parameter to help get Context.CONNECTIVITY_SERVICE right inside the LiveData.
  3. Handles the network availability that gets callback when network is available or lost. This is the recommended and latest way of handling connectivity changes.
  4. When network is available, it updates the value asynchronously to notify its active observers with true value.
  5. When network is unavailable, it updates the value asynchronously to notify its active observers with false value.
  6. Registers for network callbacks when there is at least one active observer for the LiveData.
  7. 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:

  1. Observes the connectivity status and hides necessary UI elements.
  2. Using when statement, it shows or hides the elements on the screen when connectivity is available or lost.
  3. Invoke onFragmentReady() when connection is available.

Build and run the app without a network connection.

Without network or with internet connectivity gone

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:

  1. Keeps the search field hidden initially.
  2. Hides the moviesRecyclerView initially. It becomes visible when movies load.
  3. 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.

No Connectivity with Proper UX

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.

No internet with rotation

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.

 

Navigate to Movie detail screen