Using LiveData with Room in Android

Ensuring data freshness with Room

In my last blog on using the Room database in Android, we took a look at how to save/fetch our user data to/from a SQL database without having to write a lot of boilerplate code. If you haven’t read it yet, I would recommend that you do so before you go ahead with this one:

While the process works well, there’s a small issue when it comes to fetching data from the database. Once the app is opened, it doesn’t automatically show me the processed images. Instead, I have to manually refresh it every time I want to check the progress or look at the updated images.

While this might be all right for a scenario where you have to fetch data from your database only once, it doesn’t work when:

  1. We have a continuously-running foreground service that’s making changes to our database, or
  2. If we want to have the latest data at all times.

One solution to this problem might be querying our database once every few seconds, but this poses another issue! What if my foreground service completes all the processing in 5 minutes.

After that time, the database needn’t be queried anymore since the data isn’t going to change, but regardless of that, we’re querying our database, which results in us wasting our CPU cycles.

To fix this issue correctly, we’ll be using LiveData. Let’s now see what it’s all about and how it can help us with the issue at hand!

LiveData—some background

Folks coming from the RX world (or any functional programming language) must be familiar with the concept of observables. Observables essentially let you observe the state of a particular variable and let you know whenever the state of that variable changes with a callback.

If you’re unfamiliar with this concept, you can read up more on it here:

With LiveData, the Android framework allows us to wrap any variable with a lifecycle-aware observer, essentially allowing us to observe changes in that variable and update our UI accordingly.

The added bonus here is that the observer is lifecycle-aware, which means if your Activity/Fragment is not active, you won’t be notified of any updates. This is important because we only want to update the UI of our app as long as the app is in use; once the app is stopped, we don’t want any updates to be made on the app’s UI.

Implementing LiveData in AfterShoot

LiveData is extremely crucial for AfterShoot given that, as soon as the app is launched, it processes all the images in a foreground service. Once an image is processed, the app should immediately query and show it in a RecyclerView. In the current scenario, the user has to keep on refreshing the app to see any changes.

Let’s see how to replace the traditional Room database with one backed by LiveData.

Step 1: Adding the necessary dependencies

LiveData isn’t a part of the Android SDK; instead, it’s available under Android Jetpack, and you can add it to your app by adding the following lines to your app’s build.gradle file:

dependencies {
    ...
    implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
}

After this is done, sync your project to make sure that the dependency is downloaded and ready to use.

Step 2: Modifying your Room’s Dao to return a LiveData

Once we have the dependencies in place, we need to alter our Dao’s query() methods so that they return LiveData instead of a List. This is what it looks like for me:

@Dao
interface AfterShootDao {
    
    // query all images
    @Query("SELECT * FROM afterShootImage")
    fun getAllImages(): LiveData<List<Image>>

    @Query("SELECT * FROM afterShootImage WHERE isBlurred = 1")
    fun getBlurredImages(): LiveData<List<Image>>

    ...

}

Step 3: Observe the LiveData and update your UI accordingly

Once you’ve configured the Dao, your existing code will most likely have some errors, most of them having to do with you passing LiveData into a method that expects a List. To fix these errors, we’ll first make a function called observeDb() that will observe our LiveData for changes:

class BadImageFragment : Fragment() {
    ...
  
    // since ROOM only allows queries on a background thread, make this method suspended
    suspend fun observeDb() = withContext(Dispatchers.IO) {
        // first paramter in the observe method is your LifeCycle Owner
        dao.getBlinkImages().observe(viewLifecycleOwner, object : Observer<List<Image>> {
            override fun onChanged(images: List<Image>?) {
                // update the UI here
                CoroutineScope(Dispatchers.Main).launch {
                    itemAdapter.updateData(requireNotNull(images))
                }
            }
        })
    }
    
    ...
}

The first parameter of our observe() method is a LifecycleOwner. This will most likely be your Activity/Fragment, and LiveData will respect the lifecycle of this LifeCycleOwner. When the owner is no longer active, no updates are sent to it. In case of a Fragment, use viewLifecycleOwner() to get the current Fragment’s LifeCycleOwner.

The second parameter is the implementation of the Observer interface (which can be reduced to a lambda if you like!) that has an onChanged() method, called every time there’s a change in the underlying LiveData object. In our case, this is a list of all the images in which a person is blinking.

Inside this onChanged() method, we’ve updated our adapter so that whenever the underlying data in our database for images containing blinks is updated, the observer is called and that results in the adapter receiving a fresh list of images.

Step 4: Removing the observer

In case you no longer want to observe changes in a variable and want to remove it, you can easily do so by calling the removeObserver() method on the LiveData object. To the method, you will have to pass the observer object that you want to be removed.

In my case, I have 5 LiveData objects in the same Fragment, and I’m only observing one at a time, so whenever I switch to observing a new LiveData object, I have to stop observing the earlier ones. Here’s how I do it:

class BadImageFragment : Fragment() {
    ...
    
    // implementation of the observer interface
    private val observer by lazy {
        Observer<List<Image>> { images -> itemAdapter.updateData(requireNotNull(images)) }
    }
    
    // clears all the observers
    private fun clearObservers() {
        blurredImageList.removeObserver(observer)
        overExposeImageList.removeObserver(observer)
        underExposeImageList.removeObserver(observer)
        blinkImageList.removeObserver(observer)
        croppedImageList.removeObserver(observer)
        goodImageList.removeObserver(observer)
    }
    
    private val alertFilter: AlertDialog by lazy {
        AlertDialog.Builder(requireContext())
                .setTitle("Select the filter")
                .setItems(selections) { _, which ->
                    // user is selecting a different category, so clear all the observers
                    clearObservers()
                    when (which) {
                        0 -> {
                            overExposeImageList.observe(this, observer)
                            currentMode = selections[0]
                        }
                        1 -> {
                            underExposeImageList.observe(this, observer)
                            currentMode = selections[1]
                        }
                        2 -> {
                            blinkImageList.observe(this, observer)
                            currentMode = selections[2]
                        }
                        3 -> {
                            croppedImageList.observe(this, observer)
                            currentMode = selections[3]
                        }
                        4 -> {
                            blurredImageList.observe(this, observer)
                            currentMode = selections[4]
                        }
                        5 -> {
                            goodImageList.observe(this, observer)
                            currentMode = selections[5]
                        }
                    }
                    alertFilter.dismiss()
                }
                .create()
    }
    
}

And that’s it! If you run the app after these changes are implemented, you’ll notice that it now automatically updates the feed with the latest images fetched from the database. I don’t have to refresh the app to see any changes—the observer handles listening for updates and updating my adapter automatically.

The source code for the code covered in this post can be found here:

Look under the BadImageFragment.kt file for the relevant code snippets.

Thanks for reading! If you enjoyed this story, please click the 👏 button and share it to help others find it! Feel free to leave a comment 💬 below.

Have feedback? Let’s connect on Twitter.

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *