An Android Jetpack LiveData Tutorial
Previous | Table of Contents | Next |
An Android Jetpack ViewModel Tutorial | An Overview of Android Jetpack Data Binding |
The previous chapter began the process of designing an app to conform to the recommended Jetpack architecture guidelines. These initial steps involved the selection of the Fragment+ViewModel project template and the implementation of the data model for the app user interface within a ViewModel instance.
This chapter will further enhance the app design by making use of the LiveData architecture component. Once LiveData support has been added to the project in this chapter, the next chapters (starting with An Overview of Android Jetpack Data Binding) will make use of the Jetpack Data Binding library to eliminate even more code from the project.
LiveData - A Recap
LiveData was introduced previously in the chapter entitled Modern Android App Architecture with Jetpack. 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 as being mutable using the MutableLiveData class, allowing both the ViewModel and UI controller to make changes to the underlying data value.
Adding LiveData to the ViewModel
Launch Android Studio, open the ViewModelDemo project created in the previous chapter and edit the MainViewModel.java file which should currently read as follows:
package com.ebookfrenzy.viewmodeldemo.ui.main; import android.arch.lifecycle.ViewModel; public class MainViewModel extends ViewModel { private static final Float usd_to_eu_rate = 0.74F; private String dollarText = ""; private Float result = 0F; public void setAmount(String value) { this.dollarText = value; result = Float.valueOf(dollarText)*usd_to_eu_rate; } public Float getResult() { return result; } }
The objective of this stage in the chapter is 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.ui.main; import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.ViewModel; public class MainViewModel extends ViewModel { private static final Float usd_to_eu_rate = 0.74F; private String dollarText = ""; private MutableLiveData<Float> result = new MutableLiveData<>(); public void setAmount(String value) { this.dollarText = value; result = Float.valueOf(dollarText)*usd_to_eu_rate; } public Float getResult() { return result; } }
package com.ebookfrenzy.viewmodeldemo.ui.main; import android.arch.lifecycle.MutableLiveData; import android.arch.lifecycle.ViewModel; public class MainViewModel extends ViewModel { private static final Float usd_to_eu_rate = 0.74F; private String dollarText = ""; private MutableLiveData<Float> result = new MutableLiveData<>(); public void setAmount(String value) { this.dollarText = value; result.setValue(Float.valueOf(dollarText)*usd_to_eu_rate); } public MutableLiveData<Float> getResult() { return result; } }
Implementing the Observer
Now that the conversion result is contained within a LiveData instance, the next step is to configure an observer within the UI controller which, in this example, is the MainFragment class. Locate the MainFragment.java class (app -> java -> <package name> -> MainFragment), double-click on it to load it into the editor and modify the onActivityCreated() method to create a new Observer instance named resultObserver:
package com.ebookfrenzy.viewmodeldemo.ui.main; import android.arch.lifecycle.Observer; . . @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { . . dollarText = getView().findViewById(R.id.dollarText); resultText = getView().findViewById(R.id.resultText); convertButton = getView().findViewById(R.id.convertButton); resultText.setText(mViewModel.getResult().toString()); final Observer<Float> resultObserver = new Observer<Float>() { @Override public void onChanged(@Nullable final Float result) { resultText.setText(result.toString()); } }; . . }
The resultObserver instance declares the onChanged() method which, when called, is passed the current result value which it then converts to a string and displays on the result TextView object. The next step is to add the observer to the result LiveData object, a reference to which 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 public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mViewModel = ViewModelProviders.of(this).get(MainViewModel.class); dollarText = getView().findViewById(R.id.dollarText); resultText = getView().findViewById(R.id.resultText); convertButton = getView().findViewById(R.id.convertButton); resultText.setText(mViewModel.getResult().toString()); final Observer<Float> resultObserver = new Observer<Float>() { @Override public void onChanged(@Nullable final Float result) { resultText.setText(result.toString()); } }; mViewModel.getResult().observe(this, resultObserver); convertButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!dollarText.getText().toString().equals("")) { mViewModel.setAmount(Float.valueOf( dollarText.getText().toString())); resultText.setText(mViewModel.getResult().toString()); } else { resultText.setText("No Value"); } } }); }
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 onActivityCreated() 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 not lost in the event that 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.
Summary
This chapter demonstrated the use of the Android LiveData component to make sure 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 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 by making use of the Data Binding Library, details of which are covered in the next chapter.
Previous | Table of Contents | Next |
An Android Jetpack ViewModel Tutorial | An Overview of Android Jetpack Data Binding |