Using LiveData

LiveData is a data holder class found in the Jetpack lifecycle-extensions library. Room is built to work with LiveData. Since you already included the lifecycle-extensions library in your app/build.gradle file in Chapter 4, you have access to the LiveData class in your project.

LiveData’s goal is to simplify passing data between different parts of your application, such as from your CrimeRepository to the fragment that needs to display the crime data. LiveData also enables passing data between threads. This makes it a perfect fit for respecting the threading rules we laid out above.

When you configure queries in your Room DAO to return LiveData, Room will automatically execute those query operations on a background thread and then publish the results to the LiveData object when the query is done. You can set your activity or fragment up to observe the LiveData object, in which case your activity or fragment will be notified on the main thread of results when they are ready.

In this chapter, you will focus on using the cross-thread communication functionality of LiveData to perform your database queries. To begin, open CrimeDao.kt and update the return type of your query functions to return a LiveData object that wraps the original return type.

Listing 11.15  Returning LiveData in the DAO (database/CrimeDao.kt)

@Dao
interface CrimeDao {

  @Query("SELECT * FROM crime")
  fun getCrimes(): List<Crime>
  fun getCrimes(): LiveData<List<Crime>>

  @Query("SELECT * FROM crime WHERE id=(:id)")
  fun getCrime(id: UUID): Crime?
  fun getCrime(id: UUID): LiveData<Crime?>
}

By returning an instance of LiveData from your DAO class, you signal Room to run your query on a background thread. When the query completes, the LiveData object will handle sending the crime data over to the main thread and notify any observers.

Next, update CrimeRepository to return LiveData from its query functions.

Listing 11.16  Returning LiveData from the repository (CrimeRepository.kt)

class CrimeRepository private constructor(context: Context) {
  ...
  private val crimeDao = database.crimeDao()

  fun getCrimes(): List<Crime> = crimeDao.getCrimes()
  fun getCrimes(): LiveData<List<Crime>> = crimeDao.getCrimes()

  fun getCrime(id: UUID): Crime? = crimeDao.getCrime(id)
  fun getCrime(id: UUID): LiveData<Crime?> = crimeDao.getCrime(id)
  ...
}

Observing LiveData

To display the crimes from the database in the crime list screen, update CrimeListFragment to observe the LiveData returned from CrimeRepository.getCrimes().

First, open CrimeListViewModel.kt and rename the crimes property so it is more clear what data the property holds.

Listing 11.17  Accessing the repository in the ViewModel (CrimeListViewModel.kt)

class CrimeListViewModel : ViewModel() {

    private val crimeRepository = CrimeRepository.get()
    val crimes crimeListLiveData = crimeRepository.getCrimes()
}

Next, clean up CrimeListFragment to reflect the fact that CrimeListViewModel now exposes the LiveData returned from your repository (Listing 11.18). Remove the onCreate(…) implementation, since it references crimeListViewModel.crimes, which no longer exists (and since you no longer need the logging you put in place there). In updateUI(), remove the reference to crimeListViewModel.crimes and add a parameter to accept a list of crimes as input.

Finally, remove the call to updateUI() from onCreateView(…). You will implement a call to updateUI() from another place shortly.

Listing 11.18  Removing references to the old version of ViewModel (CrimeListFragment.kt)

private const val TAG = "CrimeListFragment"

class CrimeListFragment : Fragment() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.d(TAG, "Total crimes: ${crimeListViewModel.crimes.size}")
    }
    ...
    override fun onCreateView(
        ...
    ): View? {
        ...
        crimeRecyclerView.layoutManager = LinearLayoutManager(context)

        updateUI()

        return view
    }

    private fun updateUI() {
    private fun updateUI(crimes: List<Crime>) {
        val crimes = crimeListViewModel.crimes
        adapter = CrimeAdapter(crimes)
        crimeRecyclerView.adapter = adapter
    }
    ...
}

Now, update CrimeListFragment to observe the LiveData that wraps the list of crimes returned from the database. Since the fragment will have to wait for results from the database before it can populate the recycler view with crimes, initialize the recycler view adapter with an empty crime list to start. Then set up the recycler view adapter with the new list of crimes when new data is published to the LiveData.

Listing 11.19  Hooking up the RecyclerView (CrimeListFragment.kt)

private const val TAG = "CrimeListFragment"

class CrimeListFragment : Fragment() {

    private lateinit var crimeRecyclerView: RecyclerView
    private var adapter: CrimeAdapter? = null
    private var adapter: CrimeAdapter? = CrimeAdapter(emptyList())
    ...
    override fun onCreateView(
        ...
    ): View? {
        ...
        crimeRecyclerView.layoutManager = LinearLayoutManager(context)
        crimeRecyclerView.adapter = adapter
        return view
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        crimeListViewModel.crimeListLiveData.observe(
            viewLifecycleOwner,
            Observer { crimes ->
                crimes?.let {
                    Log.i(TAG, "Got crimes ${crimes.size}")
                    updateUI(crimes)
                }
        })
    }
    ...
}

The LiveData.observe(LifecycleOwner, Observer) function is used to register an observer on the LiveData instance and tie the life of the observation to the life of another component, such as an activity or fragment.

The second parameter to the observe(…) function is an Observer implementation. This object is responsible for reacting to new data from the LiveData. In this case, the observer’s code block is executed whenever the LiveData’s list of crimes gets updated. The observer receives a list of crimes from the LiveData and prints a log statement if the property is not null.

If you never unsubscribe or cancel your Observer from listening to the LiveData’s changes, your Observer implementation might try to update your fragment’s view when the view is in an invalid state (such as when the view is being torn down). And if you attempt to update an invalid view, your app can crash.

This is where the LifecycleOwner parameter to LiveData.observe(…) comes in. The lifetime of the Observer you provide is scoped to the lifetime of the Android component represented by the LifecycleOwner you provide. In the code above, you scope the observer to the life of the fragment’s view.

As long as the lifecycle owner you scope your observer to is in a valid lifecycle state, the LiveData object will notify the observer of any new data coming in. The LiveData object will automatically unregister the Observer when the associated lifecycle is no longer in a valid state. Because LiveData reacts to changes in a lifecycle, it is called a lifecycle-aware component. You will learn more about lifecycle-aware components in Chapter 25.

A lifecycle owner is a component that implements the LifecycleOwner interface and contains a Lifecycle object. A Lifecycle is an object that keeps track of an Android lifecycle’s current state. (Recall that activities, fragments, views, and even the application process itself all have their own lifecycle.) The lifecycle states, such as created and resumed, are enumerated in Lifecycle.State. You can query a Lifecycle’s state using Lifecycle.getCurrentState() or register to be notified of changes in state.

The AndroidX Fragment is a lifecycle owner directly – Fragment implements LifecycleOwner and has a Lifecycle object representing the state of the fragment instance’s lifecycle.

A fragment’s view lifecycle is owned and tracked separately by FragmentViewLifecycleOwner. Each Fragment has an instance of FragmentViewLifecycleOwner that keeps track of the lifecycle of that fragment’s view.

In the code above, you scope the observation to the fragment’s view lifecycle, rather than the lifecycle of the fragment itself, by passing the viewLifecycleOwner to the observe(…) function. The fragment’s view lifecycle, though separate from the lifecycle of the Fragment instance itself, mirrors the lifecycle of the fragment. It is possible to change this default behavior by retaining the fragment (which you will not do in CriminalIntent). You will learn more about the view lifecycle and retaining fragments in Chapter 25.

Fragment.onViewCreated(…) is called after Fragment.onCreateView(…) returns, signaling that the fragment’s view hierarchy is in place. You observe the LiveData from onViewCreated(…) to ensure that your view is ready to display the crime data. This is also the reason you pass the viewLifecycleOwner to the observe() function, rather than the fragment itself. You only want to receive the list of crimes while your view is in a good state, so using the view’s lifecycle owner ensures that you will not receive updates when your view is not on the screen.

When the list of crimes is ready, the observer you defined prints a log message and sends the list to the updateUI() function to prepare the adapter.

With everything in place, run CriminalIntent. You should no longer see the crash you saw earlier. Instead, you should see the fake crimes from the database file you uploaded to your emulator (Figure 11.8).

Figure 11.8  Database crimes

Database crimes

In the next chapter you will connect the crime list and crime detail screens and populate the crime detail screen with data for the crime you clicked on from the database.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.200.66