Jetpack AAC Series:

(1) Lifecycle will be fully mastered!

“Finally get it” series: Jetpack AAC complete analysis (2) Fully master LiveData!

“Finally get it” series: Jetpack AAC complete analysis (3) fully master the ViewModel!

“Finally Get it” series: Jetpack AAC Complete Analysis (iv) MVVM – Android Architecture Exploration!

“Finally Get it” series: Jetpack AAC Complete Analysis (5) DataBinding re-cognition!

Welcome to follow my public account, wechat search Hu Feiyang, article updates can be the first time to receive.

The last article introduced LiveData, the data processing component of Jetpack AAC, which enables updates of data to be perceived by observers in observer mode, and this awareness only occurs in active lifecycle states. This article introduces the ViewModel component used in conjunction with LiveData.

Note that if you are not familiar with both THE VM in the MVVM architecture and the ViewModel in this article, then do not associate the two. For now, you understand it to be nothing. A section on MVVM will follow.

Introduction to ViewModel

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

ViewModel, ViewModel, is the model that prepares the data for the interface. The simple idea is that the ViewModel provides data to the UI layer. The official document defines it as follows:

The ViewModel stores and manages data related to the interface in a lifecycle focused manner. (role)

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

At this point, you’re probably not sure what a ViewModel does, so don’t worry, look down.

1.1 Appearance Background

Before I dive into the details of ViewModel, let’s take a look at the background and problem spots.

  1. The Activity may destroy and recreate the interface in certain scenarios (such as screen rotation), and the data stored in the interface will be lost. For example, if an interface contains a list of user information, and an Activity is recreated due to a configuration change, the new Activity must re-request the list of users, resulting in a waste of resources. Can you directly restore the previous data? For simple data, an Activity can use the onSaveInstanceState() method to save and then recover data from the onCreate() Bundle, but this method is only suitable for small amounts of data that can be serialized and then deserialized (IPC has a limit of 1 MB for bundles). They are not suitable for potentially large amounts of data, such as lists of user information or bitmaps. So how do you restore 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). It may take some time to return results. If the logical layer holds UI layer applications (such as context), then 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 management requires a lot of maintenance. So how best to avoid memory leaks caused by asynchronous requests?

This is where the ViewModel comes in — instead of a Presenter in MVP, the ViewModel is used to prepare data for the UI layer and solve both of these problems.

1.2 the characteristics of

Specifically, compared to a Presenter, a ViewModel has the following features:

1.2.1 Longer lifecycle than Activity

The most important feature of the ViewModel isLife cycle is longer than Activity. Take a look at this image on the website:

You can see that the ViewModel object remains after the Activity is recreated due to screen rotation. The ViewModel is cleared only when the Activity is actually finished.

That is, the ViewModel object is retained and associated with the new Activity when the Activity is destroyed and rebuilt due to a system configuration change. During normal destruction of the Activity (the system does not rebuild the Activity), the ViewModel object is cleared.

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

1.2.2 Does not hold UI layer references

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

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

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

2. Use ViewModel

2.1 Basic Usage

Now that you’ve seen the features of the ViewModel solution, let’s take a look at how it works with LivaData. (Gradle dependencies were covered in the first article.)

Steps:

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

For example, if you want to display user information in an Activity, you need to place the action of retrieving user information into the ViewModel as follows:

public class UserViewModel extends ViewModel {

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

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

    // Get the user information, pretend to make a network request for 2 seconds and return 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);// Throw user information
            }
            @Override
            protected String doInBackground(Void... voids) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String userName = "My name is Hu Feiyang, the public account name is also Hu Feiyang, welcome to follow ~";
                return userName;
            }
        }.execute();
    }
    
    public LiveData<String> getUserLiveData(a) {
        return userLiveData;
    }
    public LiveData<Boolean> getLoadingLiveData(a) {
        returnloadingLiveData; }}Copy the code

The UserViewModel inherits from the ViewModel, and then the logic is simple: pretend to make a network request for 2 seconds and return the user information, where userLiveData is used to throw the user information and loadingLiveData is used to control the display of the progress bar.

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
        ViewModelProvider viewModelProvider = new ViewModelProvider(this);
        UserViewModel userViewModel = viewModelProvider.get(UserViewModel.class);
        // Observe user information
        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

There’s a button that you can click to get the user information, and there’s a TextView that shows the user information. Create the ViewModelProvider instance in onCreate(), passing in the ViewModelStoreOwner, and implementing the Activity and Fragment. Then get the ViewModel instance through the ViewModelProvider’s get method, and then observe the LiveData in the ViewModel.

After running, click the button and the progress bar will pop up. After 2 seconds, the user information will be displayed. Then we rotated the phone, and we found that the user information was still there. Here’s the effect:

The Activity is indeed rebuilt after rotating the phone, and 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. ViewModel is easy to use and works as a Presenter. Just look at the UI layer in conjunction with LiveData.
  2. The ViewModel must be created through the ViewModelProvider.
  3. Notice that the ViewModel does not hold any UI-related references.
  4. After rotating the phone to rebuild the Activity, the data did recover.

2.2 Data sharing between Fragments

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

You can do that now using ViewModel. The two fragments can share the ViewModel using their Activity scope to handle this communication, as shown in the following example 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 to 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) {
                // Show detailsdetail.setText(userItem.toString()); }}); }}Copy the code

The code is simple. The ListFragment updates the LiveData of the ViewModel when clicking on the Item, and the DetailFragment listens for the LiveData.

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

This method has the following advantages:

  1. The Activity does not need to perform any action or have any knowledge of this communication.
  2. Apart from the SharedViewModel convention, fragments do not need to know each other. If one Fragment disappears, the other will continue to work as usual.
  3. Each Fragment has its own lifecycle and is not affected by the lifecycle of another Fragment. If one Fragment replaces another, the interface continues to work without any problems.

Finally, the effect:

Three, source code analysis

We know that the core point of ViewModel is that the ViewModel instance still exists after the interface (Activity/Fragment) is rebuilt due to configuration updates. This is the focus of our source analysis.

When we get the ViewModel instance, we don’t just get new, we use the ViewModelProvider to get it, and guess that’s where the key point is.

3.1 Storage and acquisition of ViewModel

Let’s start with the ViewModel class:

public abstract class ViewModel {...private volatile boolean mCleared = false;
    // called when the ViewModel will be cleared
    // When the ViewModel looks at some data, you can do a deregister here to prevent memory leaks
    @SuppressWarnings("WeakerAccess")
    protected void onCleared(a) {}@MainThread
    final void clear(a) {
        mCleared = true; . onCleared(); }... }Copy the code

The ViewModel class is abstract and has no internal logic. There is a clear() method that is called when the ViewModel is about to be cleared.

The ViewModel instance is then obtained by the ViewModelProvider class. See the name, ViewModelProvider. Look at its constructor:

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }
Copy the code

In this example, we use a constructor that just passes the ViewModelStoreOwner and then goes to the ViewModelStore and Factory constructor that takes two arguments. ViewModelStoreOwner — Owner of ViewModel memory; ViewModelStore – ViewModel store, a place to store viewModels; Factory – The Factory where ViewModel instances are created.

ViewModelStoreOwner is an interface:

public interface ViewModelStoreOwner {
	// Get the ViewModelStore
    ViewModelStore getViewModelStore(a);
}
Copy the code

There is an Activity/Fragment class, that is, the Activity/Fragment is the owner of the ViewModel memory, how to achieve the ViewModelStore?

Let’s take a look at how the ViewModelStore stores the ViewModel and how the ViewModel instance is fetched.

/** * is used to store ViewModels. * The ViewModelStore instance must survive system configuration changes. * /
public class ViewModelStore {
    private final HashMap<String, ViewModel> mMap = new HashMap<>();
    
    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if(oldViewModel ! =null) { oldViewModel.onCleared(); }}final ViewModel get(String key) {
        return mMap.get(key);
    }
    Set<String> keys(a) {
        return new HashSet<>(mMap.keySet());
    }
    /** * Call the clear() method of the ViewModel and then clear the ViewModel * if the owner of the ViewModelStore (Activity/Fragment) is destroyed and will not be rebuilt, then call this method */
    public final void clear(a) {
        for(ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }}Copy the code

The ViewModelStore code is simple; the viewModel is stored as a Value in the HashMap.

Let’s look at the Factory that created the ViewModel instance, which is called NewInstanceFactory:

    public static class NewInstanceFactory implements Factory {...@Override
        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
            //noinspection TryWithIdenticalCatches
            try {
                return modelClass.newInstance();
            } catch (InstantiationException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Cannot create an instance of "+ modelClass, e); }}}Copy the code

Simply get the ViewModel instance through the class reflection passed in.

Back in the example, we use viewModelProvider.get(userViewModel.class) to get the UserViewModel instance, so look at the get() method:

public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        
	// Get the Key that holds the ViewModel from the Map in the ViewModelStore
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }
    
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
	// Get the ViewModel instance from ViewModelStore
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            // If it is obtained from ViewModelStore, return it directly
            return (T) viewModel;
        } 
        
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
        } else {
        // If no, create it using Factory
            viewModel = (mFactory).create(modelClass);
        }
        // Save the ViewModelStore and return
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }
Copy the code

Logic is very clear, try to get the ViewModel instance from ViewModelStore, the key is “androidx. Lifecycle. ViewModelProvider. DefaultKey: XXX. SharedViewModel”, if there is no access to, Just create it using Factory and store it in ViewModelStore.

At this point, we know how the ViewModel is stored and how the instance is retrieved, but we have not analyzed the key point of the initial analysis: “The ViewModel instance still exists after the interface is rebuilt due to configuration updates.”

3.2 Storage and Obtaining of ViewModelStore

ViewModelStoreOwner ComponentActivity (ComponentActivity, ViewModelStoreOwner, ViewModelStoreOwner)

//ComponentActivity.java
    public ViewModelStore getViewModelStore(a) {
        if (getApplication() == null) {
        // The activity is not associated with the Application, so it cannot fetch the viewModel before onCreate
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mViewModelStore == null) {
        / / if the memory is empty, just try from lastNonConfigurationInstance from capture
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if(nc ! =null) {
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
            / / if lastNonConfigurationInstance does not exist, then the new one
                mViewModelStore = newViewModelStore(); }}return mViewModelStore;
    }
Copy the code

That’s the point. First try to get a ViewModelStore instance from NonConfigurationInstance. If NonConfigurationInstance does not exist, then create a new mViewModelStore. And also noted that in the onRetainNonConfigurationInstance () method will give NonConfigurationInstances mViewModelStore assignment:

    // This method is called when an Activity is about to be destroyed due to a configuration change, and a new Activity is created immediately
    public final Object onRetainNonConfigurationInstance(a) { Object custom = onRetainCustomNonConfigurationInstance(); ViewModelStore viewModelStore = mViewModelStore; .if (viewModelStore == null && custom == null) {
            return null;
        }

	/ / new a NonConfigurationInstances mViewModelStore assignment
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        return nci;
    }
Copy the code

OnRetainNonConfigurationInstance () method is important: for configuration changes in the Activity And was about to destroy, and can immediately create a new Activity, the system will invoke this method. Also said, configuration changes In the system the NonConfigurationInstances viewModelStore exist.

NonConfigurationInstances is a what?

//ComponentActivity
    static final class NonConfigurationInstances {
        Object custom;
        ViewModelStore viewModelStore;
    }
Copy the code

ComponentActivity: ComponentActivity: ComponentActivity: ComponentActivity: ComponentActivity: ComponentActivity: ComponentActivity So configuration changes like screen rotation and so on will not affect this instance? Let’s see if this conjecture is correct.

We see getLastNonConfigurationInstance () :

//Acticity.java

NonConfigurationInstances mLastNonConfigurationInstances;

/ / return onRetainNonConfigurationInstance () returns the instance
public Object getLastNonConfigurationInstance(a) {
    returnmLastNonConfigurationInstances ! =null ? mLastNonConfigurationInstances.activity : null;
}

static final class NonConfigurationInstances {
        Object activity;
        HashMap<String, Object> children;
        FragmentManagerNonConfig fragments;
        ArrayMap<String, LoaderManager> loaders;
        VoiceInteractor voiceInteractor;
    }
Copy the code

Method is the Acticity. In Java, it returns the Acticity. In Java NonConfigurationInstances attribute activity, Namely onRetainNonConfigurationInstance () method returns the instance. (note that the above is the NonConfigurationInstances ComponentActivity, two class)

To continue to see mLastNonConfigurationInstances is which come of, by looking for calls to find in the attach () method:

final void attach(Context context, ActivityThread aThread, ... NonConfigurationInstances lastNonConfigurationInstances,... ) {... mLastNonConfigurationInstances = lastNonConfigurationInstances; . }Copy the code

MLastNonConfigurationInstances is the Activity of the attach assignment method. The Attach method attaches context to an Activity. It is called in the performLaunchActivity method of the ActivityThread, the core process of an Activity’s launch. Here there is a component of ActivityClientRecord lastNonConfigurationInstances information.

An ActivityClientRecord exists in mActivities of an ActivityThread:

//ActivityThrtead.java
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
Copy the code

Then the ActivityClientRecord ActivityThread is not influenced by activity reconstruction, so ActivityClientRecord lastNonConfigurationInstances also is not affected, The Object of the activity is not affected, so the NonConfigurationInstances ComponentActivity viewModelStore unaffected, so the viewModel would be unaffected.

So, here is the core problem “configuration changes after the reconstruction of the ViewModel still exist” principle analysis.

OnSaveInstanceState ()

The system provides onSaveInstanceState() to let the developer save some data to facilitate the interface destroy reconstruction data recovery. What’s the difference between using the ViewModel to restore data?

4.1 Application Scenarios

As I mentioned a long time ago in an article called The Activity Lifecycle:

OnSaveInstanceState call timing:

When an activity becomes “easy” for the system to destroy, the activity’s onSaveInstanceState is executed unless the activity was destroyed voluntarily by the user, such as when the user presses the BACK key. Notice the double quotation marks above. What is “easy”? The implication is that the activity has not been destroyed, but is only a possibility.

What are the possibilities? There are several cases:

1. When the user presses the HOME button. Obviously, the system doesn’t know how many other programs will run after you press HOME, and it certainly doesn’t know if Activity A will be destroyed, so it calls onSaveInstanceState to give the user the opportunity to save some non-permanent data. The following situations are analyzed in accordance with this principle.

2. Long press the HOME button and choose to run other programs.

3. Press the power button (turn off the screen display).

4. Start A new activity from Activity A.

5. When switching screen orientation, such as from portrait to landscape. The system destroys Activity A before the screen switch and automatically creates activity A after the screen switch, so onSaveInstanceState must be executed.

All in all, onSaveInstanceState calls follow an important principle: onSaveInstanceState is called when the system destroys your activity “without your permission.” This is the system’s responsibility. It has to provide an opportunity for you to save your data (if you don’t, it’s up to you).

Using the ViewModel to restore data is only possible if the rebuild is destroyed due to a configuration change interface.

4.2 Storage Mode

ViewModel is stored in memory and is fast to read and write, while onSaveInstanceState is serialized to disk.

4.3 Restrictions on Data Storage

ViewModel, you can store complex data, and the size limit is the amount of memory available for your App. OnSaveInstanceState can only store serializable and deserializable objects and has a limited size (generally bundles limit the size to 1 MB).

Five, the summary

This article first introduced the concept of ViewModel — the model that prepares data for the interface, and then its characteristics: the interface is destroyed and rebuilt due to configuration changes, and does not hold the UI application; Then it introduces the usage and Fragment data sharing. Finally, detailed analysis of ViewModel source code and core principles.

And you can see that LiveData and ViewModel can be used together to replace Presenter in MVP to solve many problems. ViewModel is an important component of our subsequent MVVM architecture. This is something we have to grasp and understand.

The next article will introduce the MVVM architecture based on LifeCycle, LiveData and ViewModel.

That’s all for today

.

Thanks and reference:

ViewModel official documentation

.

Your likes and comments are a great encouragement to me!

Welcome to follow my public account, wechat search Hu Feiyang, article updates can be the first time to receive.