1. Introduce the ViewModel

The ViewModel is an important component of Jetpack AAC and also has an abstract class of the same name.

A ViewModel is a model that prepares data for an interface. Simply put, the ViewModel provides data for the UI layer. The official document defines:

The ViewModel stores and manages data related to the interface in a life-cycle manner. (role)

The ViewModel class allows data to persist after configuration changes such as screen rotation. (features)

1.1 Background

  1. The Activity may destroy and recreate the interface in certain scenarios (such as screen rotation), and the interface data stored in it will be lost. For example, if the interface contains a list of user information, when the Activity is recreated due to configuration changes, the new Activity must re-request the list of users, resulting in a waste of resources. Can you directly recover the previous data? For simple data, an Activity can use the onSaveInstanceState() method to save and then recover the data from the Bundle in onCreate(), but this method is only suitable for small amounts of data that can be serialized and deserialized (IPC has a 1M limit on bundles). It is not suitable for potentially large amounts of data, such as lists of user information or bitmaps. How do you recover data after a new Activity is created due to a configuration change?
  2. UI layers (such as activities and fragments) often need to make asynchronous requests through logical layers (such as Presenter in MVP), which can take some time to return results. If the logical layer holds UI-layer applications (such as context), the UI layer needs to manage these requests. Make sure these calls are cleaned up after the interface is destroyed to avoid potential memory leaks, but this administration requires a lot of maintenance work. So how best to avoid memory leaks caused by asynchronous requests?

This is where the ViewModel comes in — the ViewModel is used instead of the Presenter in the MVP to prepare data for the UI layer to solve the above two problems.

1.2 the characteristics of

Specifically, viewModels have the following characteristics compared to presenters:

1.2.1 Life cycle longer than Activity

The most important feature of a ViewModel is that it has a longer lifecycle than an Activity. The ViewModel has not been destroyed by the time the page onDestroy is completed, so do not hold references to the Context or the Context class in the ViewModel. Take a look at an image from the official website:

See that the ViewModel object remains after the Activity is recreated due to screen rotation. The ViewModel is cleared only when the Activity actually finishes.

That is, the ViewModel object remains and is associated with the new Activity when the Activity is destroyed and rebuilt due to a system configuration change. The ViewModel object is cleared when the Activity is normally destroyed (the system does not rebuild the Activity).

Naturally, the data stored inside the ViewModel becomes available to the newly created Activity instance when the Activity is destroyed and rebuilt due to a system configuration change. That solves the first problem.

1.2.2 No UI layer references are held

As we know, the MVP Presenter needs to hold the IView interface to call results back and forth to the interface.

The ViewModel doesn’t need to hold references to the UI layer, so how do we get results to the UI layer? The answer is to use LiveData based on the observer pattern introduced in the previous article. Also, viewModels cannot hold UI layer references because viewModels have a longer lifetime.

As a result, the ViewModel does not need and cannot hold UI layer references, thus avoiding possible memory leaks and achieving decoupling. That solves the second problem.

2. The ViewModel

2.1 Basic Usage

Now that you know what ViewModel does, let’s look at how it works with LivaData.

Steps:

  1. Customize MyViewModel by inheriting ViewModel
  2. Write the logic to get the UI data in MyViewModel
  3. Use LiveData to throw the captured UI data
  4. Use the ViewModelProvider in your Activity/Fragment to get the MyViewModel instance
  5. Observe the LiveData data in MyViewModel and make corresponding UI updates.

For example, if you need to display user information in your Activity, you need to put the user information retrieval operation into the ViewModel, like this:

public class UserViewModel extends ViewModel {

    private MutableLiveData<String> userLiveData ;
    private MutableLiveData<Boolean> loadingLiveData;

    public UserViewModel(a) {
        userLiveData = new MutableLiveData<>();
        loadingLiveData = new MutableLiveData<>();
    }

    // Get user information, pretend network request 2s back to the user information
    public void getUserInfo(a) {

        loadingLiveData.setValue(true);

        new AsyncTask<Void, Void, String>() {
            @Override
            protected void onPostExecute(String s) {
                loadingLiveData.setValue(false);
                userLiveData.setValue(s);// Throws user information
            }
            @Override
            protected String doInBackground(Void... voids) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String userName = "UserName";
                return userName;
            }
        }.execute();
    }

    public LiveData<String> getUserLiveData(a) {
        return userLiveData;
    }
    public LiveData<Boolean> getLoadingLiveData(a) {
        returnloadingLiveData; }}Copy the code

UserViewModel inherits from ViewModel, and the logic is simple: pretend network request 2s and return user information, where userLiveData is used to throw user information and loadingLiveData is used to control the progress bar display.

Now look at the UI layer:

public class UserActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); . Log.i(TAG,"onCreate: ");

        TextView tvUserName = findViewById(R.id.textView);
        ProgressBar pbLoading = findViewById(R.id.pb_loading);
    	// Get the ViewModel instance -- be sure to create the ViewModel through the ViewModelProvider
        ViewModelProvider viewModelProvider = new ViewModelProvider(this);
        UserViewModel userViewModel = viewModelProvider.get(UserViewModel.class);
        // Observe user information - Write a separate observe method for each LiveData
        userViewModel.getUserLiveData().observe(this.new Observer<String>() {
            @Override
            public void onChanged(String s) {
                // update ui.tvUserName.setText(s); }}); userViewModel.getLoadingLiveData().observe(this.new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean aBoolean) {
                pbLoading.setVisibility(aBoolean?View.VISIBLE:View.GONE);
            }
        });
        // Click the button to get user information
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) { userViewModel.getUserInfo(); }}); }@Override
    protected void onStop(a) {
        super.onStop();
        Log.i(TAG, "onStop: ");
    }
    @Override
    protected void onDestroy(a) {
        super.onDestroy();
        Log.i(TAG, "onDestroy: "); }}Copy the code

The page has a button to click to get user information, and a TextView to display user information. The ViewModelProvider instance is created in onCreate(). The ViewModelStoreOwner is passed in as the argument, and both the Activity and Fragment are implemented. We then get the ViewModel instance through the ViewModelProvider’s get method, and then observe the LiveData in the ViewModel.

After running, click the button will pop up the progress bar, 2s after the display of user information. Then we rotate the phone, and we see that the user’s information is still there. After rotating the phone, the Activity is indeed rebuilt. The log is printed as follows:

2021-01-06 20:35:44.984 28269-28269/com.hfy.androidlearning I/UserActivity: onStop: 
2021-01-06 20:35:44.986 28269-28269/com.hfy.androidlearning I/UserActivity: onDestroy: 
2021-01-06 20:35:45.025 28269-28269/com.hfy.androidlearning I/UserActivity: onCreate: 
Copy the code

Summary:

  1. Viewmodels are simple to use and act like presenters. Just combine LiveData with UI layer observations.
  2. The ViewModel must be created through the ViewModelProvider.
  3. Notice that no UI-related references are held in the ViewModel.
  4. After rotating the phone to rebuild the Activity, the data did recover.

2.2 Data Sharing among Fragments

It is common for multiple fragments in an Activity to need to communicate with each other. Suppose there is a ListFragment. If the user selects an item from the list, another DetailFragment displays the details of the selected item. Previously, you might have defined interfaces or used EventBus to deliver and share data.

You can now do this using the ViewModel. The two fragments can handle such communication using their Activity scope shared ViewModel, as shown in the following sample code:

//ViewModel
public class SharedViewModel extends ViewModel {
// The selected Item
    private final MutableLiveData<UserContent.UserItem> selected = new MutableLiveData<UserContent.UserItem>();

    public void select(UserContent.UserItem user) {
        selected.setValue(user);
    }
    public LiveData<UserContent.UserItem> getSelected() {
        returnselected; }}//ListFragment
public class MyListFragment extends Fragment {...privateSharedViewModel model; .public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // Get the ViewModel. Note that the ViewModelProvider instance is passed in as the host Activity
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        adapter.setListner(new MyItemRecyclerViewAdapter.ItemCLickListner(){
            @Override
            public void onClickItem(UserContent.UserItem userItem) { model.select(userItem); }}); }}//DetailFragment
public class DetailFragment extends Fragment {

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        TextView detail = view.findViewById(R.id.tv_detail);
        // Get the ViewModel and observe the selected Item
        SharedViewModel model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), new Observer<UserContent.UserItem>() {
            @Override
            public void onChanged(UserContent.UserItem userItem) {
                // Display detailsdetail.setText(userItem.toString()); }}); }}Copy the code

The code is simple: The ListFragment updates the ViewModel’s LiveData when an Item is clicked, and the DetailFragment listens for that LiveData.

Note that both fragments pass in their host Activity when they get the ViewModel through the ViewModelProvider. This way, when both fragments get their own ViewModelProvider, they receive the same SharedViewModel instance (whose scope is limited to that Activity).

This approach has the following advantages:

  1. The Activity does not need to perform any action, nor does it need to know anything about this communication.
  2. Fragments do not need to know each other except for the SharedViewModel convention. If one Fragment disappears, the other Fragment continues to work as usual.
  3. Each Fragment has its own life cycle and is not affected by the life cycle of the other Fragment. If one Fragment replaces another, the interface will continue to work without any problems.

Finally, the effect: