The previous chapter began building an app to conform to the recommended Jetpack architecture guidelines. These initial steps involved implementing the data model for the app user interface within a ViewModel instance.
This chapter will further enhance the app design using the LiveData architecture component. Once LiveData support has been added to the project in this chapter, the next chapters (starting with Data Binding in Android Studio) will use the Jetpack Data Binding library to eliminate even more code from the project.
LiveData – A Recap
LiveData was previously introduced in the Modern Android App Architecture with Jetpack chapter. As described earlier, the LiveData component can be used as a wrapper around data values within a view model. Once contained in a LiveData instance, those variables become observable to other objects within the app, typically UI controllers such as Activities and Fragments. This allows the UI controller to receive a notification whenever the underlying LiveData value changes. An observer is set up by creating an instance of the Observer class and defining an onChange() method to be called when the LiveData value changes. Once the Observer instance has been created, it is attached to the LiveData object via a call to the LiveData object’s observe() method.
LiveData instances can be declared mutable using the MutableLiveData class, allowing both the ViewModel and UI controller to change the underlying data value.
Adding LiveData to the ViewModel
Launch Android Studio, open the ViewModelDemo project created in the previous chapter, and open the MainViewModel.kt file, which should currently read as follows:
package com.ebookfrenzy.viewmodeldemo
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() {
private val rate = 0.74f
private var dollarText = ""
private var result: Float = 0f
fun setAmount(value: String) {
this.dollarText = value
result = value.toFloat() * rate
}
fun getResult(): Float {
return result
}
}
Code language: Kotlin (kotlin)
This stage in the chapter aims to wrap the result variable in a MutableLiveData instance (the object will need to be mutable so that the value can be changed each time the user requests a currency conversion). Begin by modifying the class so that it now reads as follows, noting that an additional package needs to be imported when making use of LiveData:
package com.ebookfrenzy.viewmodeldemo
import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
class MainViewModel : ViewModel() {
private val rate = 0.74f
private var dollarText = ""
// private var result: Float = 0f
private var result: MutableLiveData<Float> = MutableLiveData()
fun setAmount(value: String) {
this.dollarText = value
result = value.toFloat() * rate
}
fun getResult(): Float {
return result
}
}
Code language: Kotlin (kotlin)
Now that the result variable is contained in a mutable LiveData instance, both the setAmount() and getResult() methods must be modified. In the case of the setAmount() method, a value can no longer be assigned to the result variable using the assignment (=) operator. Instead, the LiveData setValue() method must be called, passing through the new value as an argument. As currently implemented, the getResult() method is declared to return a Float value and must be changed to return a MutableLiveData object. Making these remaining changes results in the following class file:
package com.ebookfrenzy.viewmodeldemo
import androidx.lifecycle.ViewModel
import androidx.lifecycle.MutableLiveData
class MainViewModel : ViewModel() {
private val rate = 0.74f
private var dollarText = ""
private var result: MutableLiveData<Float> = MutableLiveData()
fun setAmount(value: String) {
this.dollarText = value
// result = value.toFloat() * rate
result.value = value.toFloat() * rate
}
// fun getResult(): Float {
fun getResult(): MutableLiveData<Float> {
return result
}
}
Code language: Kotlin (kotlin)
Implementing the Observer
Now that the conversion result is contained within a LiveData instance, the next step is configuring an observer within the UI controller, which, in this example, is the FirstFragment class. Locate the FirstFragment.kt class (app ->kotlin+java -> <package name> -> FirstFragment), double-click on it to load it into the editor, and modify the onViewCreated() method to create a new Observer instance named resultObserver:
.
.
import androidx.lifecycle.Observer
.
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.resultText.text = viewModel.getResult().toString()
val resultObserver = Observer<Float> {
result -> binding.resultText.text = result.toString()
}
.
.
}
Code language: Kotlin (kotlin)
The resultObserver instance declares lambda code which, when called, is passed the current result value, which it then converts to a string and displays on the resultText TextView object. The next step is to add the observer to the result LiveData object, a reference that can be obtained via a call to the getResult() method of the ViewModel object. Since updating the result TextView is now the responsibility of the onChanged() callback method, the existing lines of code to perform this task can now be deleted:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// binding.resultText.text = viewModel.getResult().toString()
val resultObserver = Observer<Float> {
result -> binding.resultText.text = result.toString()
}
viewModel.getResult().observe(viewLifecycleOwner, resultObserver)
binding.convertButton.setOnClickListener {
if (binding.dollarText.text.isNotEmpty()) {
viewModel.setAmount(binding.dollarText.text.toString())
// binding.resultText.text = viewModel.getResult().toString()
} else {
binding.resultText.text = "No Value"
}
}
}
Code language: Kotlin (kotlin)
Compile and run the app, enter a value into the dollar field, click on the Convert button, and verify that the converted euro amount appears on the TextView. This confirms that the observer received notification that the result value had changed and called the onChanged() method to display the latest data.
Note in the above implementation of the onViewCreated() method that the line of code responsible for displaying the current result value each time the method was called was removed. This was originally put in place to ensure that the displayed value was recovered if the Fragment was recreated for any reason. Because LiveData monitors the lifecycle status of its observers, this step is no longer necessary. When LiveData detects that the UI controller was recreated, it automatically triggers any associated observers and provides the latest data. Verify this by rotating the device while a euro value is displayed on the TextView object and confirming that the value is not lost.
Before moving on to the next chapter, close the project, copy the ViewModelDemo project folder, and save it as ViewModelDemo_LiveData to be used later when saving the ViewModel state.
Summary
This chapter demonstrated the use of the Android LiveData component to ensure that the data displayed to the user always matches that stored in the ViewModel. This relatively simple process consisted of wrapping a ViewModel data value within a LiveData object and setting up an observer within the UI controller subscribed to the LiveData value. Each time the LiveData value changes, the observer is notified, and the onChanged() method is called and passed the updated value.
Adding LiveData support to the project has gone some way towards simplifying the design of the project. Additional and significant improvements are also possible using the Data Binding Library, details of which will be covered in the next chapter.