Using LiveData Transformations

Now that CrimeFragment has the crime ID, it needs to pull the crime object from the database so it can display the crime’s data. Since this requires a database lookup that you do not want to repeat unnecessarily on rotation, add a CrimeDetailViewModel to manage the database query.

When CrimeFragment requests to load a crime with a given ID, its CrimeDetailViewModel should kick off a getCrime(UUID) database request. When the request completes, CrimeDetailViewModel should notify CrimeFragment and pass along the crime object that resulted from the query.

Create a new class named CrimeDetailViewModel and expose a LiveData property to store and publish the Crime pulled from the database. Use LiveData to implement a relationship where changing the crime ID triggers a new database query.

Listing 12.9  Adding ViewModel for CrimeFragment (CrimeDetailViewModel.kt)

class CrimeDetailViewModel() : ViewModel() {

    private val crimeRepository = CrimeRepository.get()
    private val crimeIdLiveData = MutableLiveData<UUID>()

    var crimeLiveData: LiveData<Crime?> =
        Transformations.switchMap(crimeIdLiveData) { crimeId ->
            crimeRepository.getCrime(crimeId)
        }

    fun loadCrime(crimeId: UUID) {
        crimeIdLiveData.value = crimeId
    }
}

The crimeRepository property stores a handle to the CrimeRepository. This is not necessary, but later on CrimeDetailViewModel will communicate with the repository in more than one place, so the property will prove useful at that point.

crimeIdLiveData stores the ID of the crime currently displayed (or about to be displayed) by CrimeFragment. When CrimeDetailViewModel is first created, the crime ID is not set. Eventually, CrimeFragment will call CrimeDetailViewModel.loadCrime(UUID) to let the ViewModel know which crime it needs to load.

Note that you explicitly defined crimeLiveData’s type as LiveData<Crime?>. Since crimeLiveData is publicly exposed, you should ensure it is not exposed as a MutableLiveData. In general, ViewModels should never expose MutableLiveData.

It may seem strange to wrap the crime ID in LiveData, since it is private to CrimeDetailViewModel. What within this CrimeDetailViewModel, you may wonder, needs to listen for changes to the private ID value?

The answer lies in the live data Transformation statement. A live data transformation is a way to set up a trigger-response relationship between two LiveData objects. A transformation function takes two inputs: a LiveData object used as a trigger and a mapping function that must return a LiveData object. The transformation function returns a new LiveData object, which we call the transformation result, whose value gets updated every time a new value gets set on the trigger LiveData instance.

The transformation result’s value is calculated by executing the mapping function. The value property on the LiveData returned from the mapping function is used to set the value property on the live data transformation result.

Using a transformation this way means the CrimeFragment only has to observe the exposed CrimeDetailViewModel.crimeLiveData one time. When the fragment changes the ID it wants to display, the ViewModel just publishes the new crime data to the existing live data stream.

Open CrimeFragment.kt. Associate CrimeFragment with CrimeDetailViewModel. Request that the ViewModel load the Crime in onCreate(…).

Listing 12.10  Hooking CrimeFragment up to CrimeDetailViewModel (CrimeFragment.kt)

class CrimeFragment : Fragment() {

    private lateinit var crime: Crime
    ...
    private lateinit var solvedCheckBox: CheckBox
    private val crimeDetailViewModel: CrimeDetailViewModel by lazy {
        ViewModelProviders.of(this).get(CrimeDetailViewModel::class.java)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        crime = Crime()
        val crimeId: UUID = arguments?.getSerializable(ARG_CRIME_ID) as UUID
        Log.d(TAG, "args bundle crime ID: $crimeId")
        // Eventually, load crime from database
        crimeDetailViewModel.loadCrime(crimeId)
    }
    ...
}

Next, observe CrimeDetailViewModel’s crimeLiveData and update the UI any time new data is published.

Listing 12.11  Observing changes (CrimeFragment.kt)

class CrimeFragment : Fragment() {

    private lateinit var crime: Crime
    ...

    override fun onCreateView(
        ...
    ): View? {
        ...
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        crimeDetailViewModel.crimeLiveData.observe(
            viewLifecycleOwner,
            Observer { crime ->
                crime?.let {
                    this.crime = crime
                    updateUI()
                }
            })
    }

    override fun onStart() {
        ...
    }

    private fun updateUI() {
        titleField.setText(crime.title)
        dateButton.text = crime.date.toString()
        solvedCheckBox.isChecked = crime.isSolved
    }
    ...
}

(Be sure to import androidx.lifecycle.Observer.)

You may have noticed that CrimeFragment has its own Crime state stored in its crime property. The values in this crime property represent the edits the user is currently making. The crime in CrimeDetailViewModel.crimeLiveData represents the data as it is currently saved in the database. CrimeFragment “publishes” the user’s edits when the fragment moves to the stopped state by writing the updated data to the database.

Run your app. Press a crime in the list. If all goes as planned, you should see the crime detail screen appear, populated with data from the crime you pressed in the list (Figure 12.3).

Figure 12.3  CriminalIntent’s back stack

CriminalIntent’s back stack

When CrimeFragment displays, you may see the checkbox animating to the checked state if you are viewing a crime marked as solved. This is expected, since the checkbox state gets set as the result of an asynchronous operation. The database query for the crime kicks off when the user first launches CrimeFragment. When the database query completes, the fragment’s crimeDetailViewModel.crimeLiveData observer is notified and in turn updates the data displayed in the widgets.

Clean this up by skipping over the animation when you programmatically set the checkbox checked state. You do this by calling View.jumpDrawablesToCurrentState(). Note that if the lag as the crime detail screen loads is unacceptable based on your app’s requirements, you could pre-load the crime data into memory ahead of time (as the app launches, for example) and stash it in a shared place. For CriminalIntent, the lag is very slight, so the simple solution of skipping the animation is enough.

Listing 12.12  Skipping checkbox animation (CrimeFragment.kt)

class CrimeFragment : Fragment() {
    ...
    private fun updateUI() {
        titleField.setText(crime.title)
        dateButton.text = crime.date.toString()
        solvedCheckBox.isChecked = crime.isSolved
        solvedCheckBox.apply {
            isChecked = crime.isSolved
            jumpDrawablesToCurrentState()
        }
    }
    ...
}

Run your app again. Press a solved crime in the list. Notice the checkbox no longer animates as the screen spins up. If you press the checkbox, the animation does happen, as desired.

Now, edit the crime’s title. Press the Back button to go back to the crime list screen. Sadly, the changes you made were not saved. Luckily, this is easy to fix.

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

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