LiveData Tutorial for Android: Deep Dive


Reactive Programming has been a buzz word nowadays. LiveData being one of the core Architecture Components, you can’t imagine not having it in a modern application. It is a simple yet powerful component, that can do wonders if you use it to its full potential.

LiveData helps to reduce memory leaks and manual lifecycle handling. It also ensures that UI is always up to date with the data even when the app’s activity is restarted while in use.

Through a real-world example of a MovieApp, you’ll learn:

  • Types of LiveData
  • Synchronous & Asynchronous Update
  • Transformations & MediatorLiveData
  • Extending LiveData
  • State management by LiveData

You’ll work on an app named MovieApp. This app loads popular movies from TMDb API and allows to search for movies using a search term. You’ll also create a custom LiveData to handle internet connectivity status.

Final App Presentation

OK, it’s time to watch some movies now.

Getting Started

Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial.

Launch Android Studio 4.0 or later and select Open an existing Android Studio project. Then navigate to and select the starter project folder.

This is how the project is structured:

Starter Project Structure

Explore the project a bit, especially MainViewModel.kt in ui package and MovieListFragment.kt in movies package. In this tutorial, you will spend the most time in these files.

Open the app’s build.gradle and add the following dependency and sync the project:

implementation 'androidx.appcompat:appcompat:1.1.0'

This library alone contains ViewModel and LiveData. These are enough to start with LiveData.

As a first step, you’d need to get a key to use TMDb API. You can get one for free from TMDb official website. If you don’t already have an account, create one from the signup page. After you’ve created the account, log in and go to the API page. Choose the developer option, accept the terms and fill in your details to create a key. Copy the API Key from the API Details page. Once you have the key, open MovieApiModule.kt present in the framework.network package. Replace the placeholder value of TMDb_API_KEY with your key.

Build and run the project. You’ll see only the search field and an empty RecyclerView.
Starter App

Lifecycle Aware Components

AppCompatActivity and Fragment are some of the Lifecycle owner objects in the Android Framework. Lifecycle aware components are classes or objects that react based on lifecycle methods of Lifecycler owner. They react to changes in the lifecycle status of another component like Activity or Fragment. You can relate it with registering and unregistering location updates in an Activity’s onStart and onStop lifecycle methods.

LiveData and MutableLiveData

LiveData is a Lifecycle aware component and an observable data holder class. Before the introduction of LiveData, you might have used the callback mechanism to update your UI. However, callback has its own set of issues like manual handling of lifecycle, memory leaks, keeping up to date correct data to name a few.

The Android team designed LiveData to solve these issues. This is achieved by updating the UI only when the LiveData has an active observer. It also ensures that UI shows the latest data whenever it is visible to the user. In the next section, you’ll see how and where to initialize and update the LiveData and observe the LiveData.

Initialize, Update, Observe LiveData

LiveData is a wrapper on an object which can be observed from any UI component via a getter method. LiveData is generally initialized in ViewModel and updated on some manual or automatic actions.

As an example, you can initialize, update, and observe LiveData as follows:

//1
val liveData: MutableLiveData<Any>()
//2
liveData.value = "Hello"
//3
liveData.observe(this, Observer { 
  // Update UI  
})

Here’s what this code does:

  1. LiveData is always initialized with one of its child class’s constructors with exception of Transformations and MediatorLiveData. More on that later.
  2. liveData declared as val can’t change, but you can always update the value of LiveData because it is MutableLiveData. Any change in value of liveData notifies all of its active observers.
  3. LiveData is always observed inside a UI Lifecycle owner, which can be an Activity or a Fragment. You receive the latest value of liveData in Observer as an implicit parameter it.

Synchronous vs Asynchronous Update

LiveData doesn’t have public methods to update its value. Hence you’ll use MutableLiveData to update the value inside LiveData with the help of the following two options:

  1. setValue: sets the value instantly. This is synchronous update where main thread calls setValue. With Kotlin’s property access syntax, you’ll often use value instead of setValue.
  2. postValue: Asynchronous updating, means Observer doesn’t receive instant update, rather receives update when UI thread is active. When you call postValue more than one time from background thread, LiveData dispatches only the latest value to the downstream. Being asynchronous, this does not guarantee instant update to the Observer.

LiveData in Action: Example One – Implement Search

Now that you know how to initialize, update, and observe LiveData, it’s the right time to add the search feature. This will enable you to search for movies by matching title with the query text in the search field in MovieApp.

Open MovieListFragment.kt file, inside searchTextWatcher. You’ll see afterTextChanged() function calling the onSearchQuery() with the search term as the only parameter. Here the search is initiated after the text changes inside the EditText. In the steps below, you’ll implement what happens when the search is initiated.

First, open the MainViewModel.kt. Inside the class, declare the following at the top where other variables are declared.


val searchMoviesLiveData = MutableLiveData<List<Movie>>()

This declares LiveData as a class member. Movies fetched from the repository will go in searchMoviesLiveData.

Next, inside the onSearchQuery() function, replace the TODO comment by the following:

fetchMovieByQuery(query)

When text length exceeds 2, the fetchMovieByQuery() fetches movies based on the search query from API.

Lastly, inside the fetchMovieByQuery() function, replace the TODO comment by following:


searchMoviesLiveData.postValue(movies)

This updates searchMoviesLiveData asynchronously after retrieving the list of movies, because fetching movies is a background task.

Till here you’ve done work required to fetch movies and update the LiveData. Now, it’s time to observe and watch some movies.

Open MovieListFragment.kt. At the end of the initialiseObservers() method of the fragment, register this observer as below:


mainViewModel.searchMoviesLiveData.observe(viewLifecycleOwner, Observer {
  movieAdapter.updateData(it)
})

This updates the adapter with fetched movies. The amazing thing about LiveData is, if UI is not in the foreground state, the observer will not receive any updates thus preventing the app from crashing.

You can search for your favorite movies now. Build and run the app.

Movies by search term without progress

Don’t go and watch your favorite movie right away, there’s a lot more to learn about LiveData :].

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:

  1. Before fetching movies from repository, update movieLoadingState with LOADING state. One way is to use Dispatchers.MAIN (foreground) because you are updating LiveData from within Dispatcher.IO (background) block of coroutine.
  2. Alternatively, you can use postValue without Dispatchers.MAIN context, as explained in the Synchronous vs Asynchronous section above. Update movieLoadingState to LOADED after you fetch movies from repository.
  3. Update movieLoadingState with MovieLoadingState.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.

Note: Try updating 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.

Movies by search term with progress

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:

  1. Transformations.map like RxJava’s map, applies an operation on value stored in LiveData and propagates the result value downstream. The function passed to map returns type of the data that LiveData holds. Use it when you want to manipulate the data before it goes to UI.
  2. Transformations.switchMap like RxJava’s flatMap, applies an operation on value stored in LiveData, unwraps and dispatches the result downstream. The function passed to switchMap 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:

  1. Update the fetchMovieByQuery() to return LiveData of List of Movies.
  2. Use local variable liveData which will hold movies from the repository.
  3. Instead of searchMoviesLiveData, update the liveData with movies.
  4. 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:

  1. Makes searchMoviesLiveData a non final LiveData as it will update when _searchFieldTextLiveData updates.
  2. _searchFieldTextLiveData will hold the search term entered by the user.
  3. When _searchFieldTextLiveData changes, it calls fetchMovieByQuery() and assigns list of movies to searchMoviesLiveData.
  4. 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:

  1. Declares a movies data holder for popular movies. Declaring it as private makes it inaccessible outside MainViewModel.kt.
  2. Declares a movie data holder moviesMediatorData, which is public and the only source of movies for UI.
  3. Updates searchMoviesLiveData visibility to private. _popularMoviesLiveData and _searchMoviesLiveData will be merged in one moviesMediatorData.

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:

  1. This adds source to moviesMediatorData. When _popularMoviesLiveData value changes, moviesMediatorData also updates and thus the updating the UI.
  2. 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.
  3. 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:

  1. Before fetching movies from repository, update the UI with MovieLoadingState.LOADING state.
  2. Fetches movies from repository and stores them in _popularMoviesLiveData. This will also update the movies in moviesMediatorData for UI to observe.
  3. After fetching movies from repository, update the UI with MovieLoadingState.LOADED state. This will hide the loading indicator and show the recyclerview.
  4. Update movieLoadingState with MovieLoadingState.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:

  1. This tells mainViewModel that UI is ready with basic initialization. onFragmentReady() calls fetchPopularMovies().
  2. This observes moviesMediatorData, which is the only source of data for movieAdapter. 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.

Popular and Search based movies

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

Multiple Event Emit Problem

You will notice that even when you press back now, you can’t navigate back to the movie list screen. This is because, when you press the back button, observers in MovieListFragment.kt attach to LiveData once again and they dispatch the latest value. Since navigateToDetails already has the tapped movie’s title, it again navigates to the MovieDetailFragment.kt with the same movie title repeatedly.

Navigating to the detail screen is an event and occurs many times. But do you need to react every time the event occurs? No, you can react to an event only once and reject reacting to further events.

Solution

First, in the root directory, create a class Event in Event.kt:


class Event<out T>(private val content: T) {

}

This declares a class which can contain any type of data.

Next, add following code to return the data only once:

//1
private var hasBeenHandled = false

//2
fun getContentIfNotHandled(): T? {
  return if (hasBeenHandled)
    null
  else {
    hasBeenHandled = true
    content
  }
}

Here’s what this code does:

  1. Creates a flag to track if data has been observed or not.
  2. Creates a function that you’ll invoke from outside world to get the data. This returns null if data has already been observed else returns the data. Also, it sets hasBeenHandled to true to block further reacting on data.

Next, open MainViewModel.kt and update _navigateToDetails declaration with the below code:


private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails: LiveData<Event<String>>
  get() = _navigateToDetails

_navigateToDetails now holds Event of String.

Next, replace onMovieClicked() function with the following code:


movie.title?.let {
  _navigateToDetails.value = Event(it)
}

Similarly, _navigateToDetails now holds event of movie title.

Lastly, open MovieListFragment.kt and update navigateToDetails observer with below code. Take note of the import statement.


import androidx.navigation.fragment.findNavController
...

mainViewModel.navigateToDetails.observe(viewLifecycleOwner, Observer {
  it?.getContentIfNotHandled()?.let { movieTitle ->
    findNavController().navigate(
        MovieListFragmentDirections.actionMovieClicked(movieTitle)
    )
  }
})

This uses getContentIfNotHandled() to get data only when it is not handled before, hence avoiding multiple firing of events.

When you select a different movie other than before selected, it will still work because, in onMovieClicked() function you create a new Event each time, that resets the hasBeenHandled flag to false.

Build and run the app for the last time:

RxJava vs LiveData

You’ll often fall into a situation where you’ve to choose between RxJava and LiveData. Both of them have their limitations. It is good practice to not consider LiveData beyond the Presentation Layer as it has a smaller toolkit compared to RxJava. RxJava being a robust toolset has many operations to manipulate data in streams, which can help in dealing with data or backend layers. As RxJava comes with the bigger toolset, it requires a longer time to get comfortable with as compared to LiveData.

You can use Both RxJava and LiveData with Room Persistence Library. LiveData is compatible with Data binding whereas RxJava is not. This suggests that for business logic use RxJava and for UI state management use LiveData. Yes, you can use both of them in the same codebase. LiveDataReactiveStreams offer a simple interface that allows you to convert RxJava streams to LiveData in a direct way.

Congratulations!!

In this tutorial, you’ve successfully learned:

  • How to Transform LiveData
  • How to use Mediator LiveData
  • Create a custom LiveData
  • The recommended way of handling states

Where to Go From Here?

Download the final project using the Download Materials button at the top or bottom of this tutorial.

You’ve covered most of the LiveData in this tutorial, but there’s plenty of places where you can use LiveData. As in the MoviesApp example, you can integrate Room to store favorite marked movies and show them in case there is no internet. For further reading, have a look at LiveData Overview official document.

Hope you enjoyed the tutorial. If you have any questions or comments, please join the forum discussion and comment below!

Source link

Leave a Reply