What is a ViewModel?

ViewModel is part of Jetpack. The ViewModel class is designed to store and manage data related to the interface in a lifecycle focused manner. The ViewModel class allows data to survive configuration changes such as screen rotation. From the official document: ViewModel Overview

Viewmodel-related questions are frequently asked in interviews. The main reason is that it is an important component of the MVVM architectural pattern and that it can retain the ViewModel instance even when the page is destroyed and rebuilt due to configuration changes.

Take a look atViewModelLife cycle ofViewModelOnly in normal timesActivity finishIs cleared.

Here’s the question:

  1. Why does the ViewModel restore data when the Activity rotates the screen
  2. Where are the instances of ViewModel cached
  3. When will ViewModel#onCleared() be called

Before addressing these three issues, let’s review the usage features of the ViewModel

The basic use

MainRepository

class MainRepository {
    suspend fun getNameList(a): List<String> {
        return withContext(Dispatchers.IO) {
            listOf("Zhang"."Bill")}}}Copy the code

MainViewModel

class MainViewModel: ViewModel() {
    private val nameList = MutableLiveData<List<String>>()
    val nameListResult: LiveData<List<String>> = nameList
    private val mainRepository = MainRepository()
    
    fun getNames(a) {
        viewModelScope.launch {
            nameList.value = mainRepository.getNameList()
        }
    }
}
Copy the code

MainActivity

class MainActivity : AppCompatActivity() {
    // Create ViewModel 1
    // Create the ViewModel through the Kotlin delegate feature
    / / need to add dependent on implementation 'androidx. Activity: activity - KTX: 1.2.3'
    // viewModels() is also a ViewModel created internally by creating ViewModel 2
    private val mainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate  (savedInstanceState)
        setContentView(R.layout.activity_main)
        // Create ViewModel mode 2
        val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        mainViewModel.nameListResult.observe(this, {
            Log.i("MainActivity"."mainViewModel: nameListResult: $it")
            Log.i("MainActivity"."MainActivity: ${this@MainActivity} mainViewModel: $mainViewModel  mainViewModel.nameListResult: ${mainViewModel.nameListResult}")
        })
        mainViewModel.getNames()
    }
}
Copy the code

LiveData will be covered in a future article

Test procedure: Open app -> See logs normally

18:03:02.575MainViewModel: nameListResult: [Zhang SAN, Li Si]18:03:02.575 : com.yqy.myapplication.MainActivity@7ffa77 mainViewModel: com.yqy.myapplication.MainViewModel@29c0057  mainViewModel.nameListResult: androidx.lifecycle.MutableLiveData@ed0d744
Copy the code

Then test the steps: open the Settings change system language -> switch to the task where the current app is to see the log

18:03:59.622MainViewModel: nameListResult: [Zhang SAN, Li Si]18:03:59.622 : com.yqy.myapplication.MainActivity@49a4455 mainViewModel: com.yqy.myapplication.MainViewModel@29c0057  mainViewModel.nameListResult: androidx.lifecycle.MutableLiveData@ed0d744
Copy the code

Magic! MainActivity is rebuilt, but the ViewModel instance remains unchanged, and the LiveData object instance in the ViewModel object remains unchanged. That’s the ViewModel feature.

Before the ViewModel, an Activity can use the onSaveInstanceState() method to save and then restore data from the onCreate() Bundle. However, this approach is only suitable for small amounts of data that can be serialized and then deserialized (IPC has a limit of 1M for bundles), and not for potentially large amounts of data, such as lists of user information or bitmaps. ViewModel is the perfect solution to this problem.

Let’s first look at how the ViewModel is created: Through the above example code, what is the final method of creating the ViewModel

val mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
Copy the code

We create the ViewModelProvider object and pass in the this parameter. We then pass in the Class type of the MainViewModel via the ViewModelProvider#get method, and we get the Instance of the MainViewModel.

Constructor of the ViewModelProvider

    public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
    	// Get the owner object's ViewModelStore object
        this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
                ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
                : NewInstanceFactory.getInstance());
    }
Copy the code

The ViewModelProvider constructor takes an argument of type ViewModelStoreOwner, right? What is a ViewModelStoreOwner? We passed in the MainActivity object. Look at the superclasses of MainActivity

public class ComponentActivity extends androidx.core.app.ComponentActivity implements.// The ViewModelStoreOwner interface is implementedViewModelStoreOwner, ... {private ViewModelStore mViewModelStore;

	// Override the only method of the ViewModelStoreOwner interface getViewModelStore()
    @NonNull
    @Override
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        ensureViewModelStore();
        return mViewModelStore;
    }

Copy the code

The ComponentActivity class implements the ViewModelStoreOwner interface. Oh, that solved the problem.

Look at the ViewModelProvider constructor that called this(ViewModelStore, Factory), The ViewModelStore instance returned by ComponentActivity#getViewModelStore is passed in and cached in the ViewModelProvider

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

Now, what does the ViewModelProvider#get method do

    @MainThread
    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");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }
Copy the code

Get the CanonicalName of the ViewModel and call another get method

    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
    	// Try to get it from the mViewModelStore cache
        ViewModel viewModel = mViewModelStore.get(key);
		// Hit the cache
        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            // Return the cached ViewModel object
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if(viewModel ! =null) {
                // TODO: log a warning.}}// Create a ViewModel instance using the factory pattern
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
        } else {
            viewModel = mFactory.create(modelClass);
        }
        // Put the created ViewModel instance into the mViewModelStore cache
        mViewModelStore.put(key, viewModel);
        // Return the newly created ViewModel instance
        return (T) viewModel;
    }
Copy the code

MViewModelStore is what? We know from the ViewModelProvider constructor that mViewModelStore is actually an mViewModelStore object in our Activity that is declared in ComponentActivity. When you look at the PUT method, it’s not hard to guess that it uses a Map structure internally.

public class ViewModelStore {
	// Sure enough, there is a HashMap inside
    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(); }}// Get the ViewModel object from the key
    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /** * Clears internal storage and notifies ViewModels that they are no longer used. */
    public final void clear() {
        for(ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }}Copy the code

The ViewModel object exists in the mViewModelStore object for ComponentActivity. The second problem is solved: where are the instances of ViewModel cached

MViewModelStore appears so often, why not see when it was created?

Remember when we looked at the constructor of the ViewModelProvider, when we got the ViewModelStore object, we actually called mainActivitygetViewModelStore (), GetViewModelStore () is implemented in The parent ComponentActivity of MainActivity.

	// ComponentActivity#getViewModelStore()
    @Override
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        ensureViewModelStore();
        return mViewModelStore;
    }
Copy the code

EnsureViewModelStore () is called before returning the mViewModelStore object

    void ensureViewModelStore() {
        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if(nc ! =null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) { mViewModelStore = new ViewModelStore(); }}}Copy the code

When mViewModelStore = = null call getLastNonConfigurationInstance () to obtain nc NonConfigurationInstances object, when nc! = null, mViewModelStore is assigned to nc.viewModelStore, and viewModelStore is created only when viewModelStore == NULL.

Is not hard to find, the previously created viewModelStore object is cached NonConfigurationInstances

	// This is a static inner class for ComponentActivity
    static final class NonConfigurationInstances {
        Object custom;
        // Sure enough, here it is
        ViewModelStore viewModelStore;
    }
Copy the code

NonConfigurationInstances object through getLastNonConfigurationInstance () to obtain

	// Activity#getLastNonConfigurationInstance
    /**
     * Retrieve the non-configuration instance data that was previously
     * returned by {@link #onRetainNonConfigurationInstance()}.  This will
     * be available from the initial {@link #onCreate} and
     * {@link #onStart} calls to the new instance, allowing you to extract
     * any useful dynamic state from the previous instance.
     *
     * <p>Note that the data you retrieve here should <em>only</em> be used
     * as an optimization for handling configuration changes.  You should always
     * be able to handle getting a null pointer back, and an activity must
     * still be able to restore itself to its previous state (through the
     * normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this
     * function returns null.
     *
     * <p><strong>Note:</strong> For most cases you should use the {@link Fragment} API
     * {@link Fragment#setRetainInstance(boolean)} instead; this is also
     * available on older platforms through the Android support libraries.
     *
     * @return the object previously returned by {@link #onRetainNonConfigurationInstance()}
     */
    @Nullable
    public Object getLastNonConfigurationInstance() {
        returnmLastNonConfigurationInstances ! =null
                ? mLastNonConfigurationInstances.activity : null;
    }
Copy the code

It’s a long note, and it probably means a few things:

  • Methods and getLastNonConfigurationInstance onRetainNonConfigurationInstance come in pairs, with similar onSaveInstanceState mechanism, It is just an optimization that is used only to handle configuration changes.
  • Returns the onRetainNonConfigurationInstance returned object

OnRetainNonConfigurationInstance and getLastNonConfigurationInstance call time in this article is no need to do, subsequent articles will be explained.

Look at the onRetainNonConfigurationInstance method

	/** * Keep all appropriate non-configured states */
    @Override
    @Nullable
    @SuppressWarnings("deprecation")
    public final Object onRetainNonConfigurationInstance() {
        // Maintain backward compatibility.
        Object custom = onRetainCustomNonConfigurationInstance();

        ViewModelStore viewModelStore = mViewModelStore;
        / / if viewModelStore is empty, then try from getLastNonConfigurationInstance ()
        if (viewModelStore == null) {
            // No one called getViewModelStore(), so see if there was an existing
            // ViewModelStore from our last NonConfigurationInstance
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if(nc ! =null) { viewModelStore = nc.viewModelStore; }}// Return null if no cache is required
        if (viewModelStore == null && custom == null) {
            return null;
        }
		
		/ / create NonConfigurationInstances object and assignment viewModelStore
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        return nci;
    }
Copy the code

Here we see, the Activity in the process of destruction of reconstruction by configuration changes will first calls onRetainNonConfigurationInstance save viewModelStore instance. After the reconstruction can be obtained through getLastNonConfigurationInstance before viewModelStore instance.

Now you’ve solved the first question: Why does the ViewModel restore data after the Activity rotates the screen

Let’s look at the third question: when will ViewModel#onCleared() be called

public abstract class ViewModel {
    protected void onCleared() {
    }

    @MainThread
    final void clear() {
        mCleared = true;
        // Since clear() is final, this method is still called on mock objects
        // and in those cases, mBagOfTags is null. It'll always be empty though
        // because setTagIfAbsent and getTag are not final so we can skip
        // clearing it
        if(mBagOfTags ! =null) {
            synchronized (mBagOfTags) {
                for (Object value : mBagOfTags.values()) {
                    // see comment for the similar call in setTagIfAbsentcloseWithRuntimeException(value); } } } onCleared(); }}Copy the code

The onCleared() method was called by clear(). Clear () clear() clear() clear()

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() {
        return new HashSet<>(mMap.keySet());
    }

    /** * Clears internal storage and notifies ViewModels that they are no longer used. */
    public final void clear() {
        for(ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }}Copy the code

In clear() of ViewModelStore, iterate over mMap and call clear() of ViewModel object, then see when clear() of ViewModelStore was called:

	// The constructor for ComponentActivity
    public ComponentActivity() {
        ... 
        getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    // Clear out the available context
                    mContextAwareHelper.clearAvailableContext();
                    // And clear the ViewModelStore
                    if(! isChangingConfigurations()) { getViewModelStore().clear(); }}}}); . }Copy the code

Observe the current activity Lifecycle, when Lifecycle.Event == Lifecycle.Event. And ViewModelStore#clear is called only when isChangingConfigurations() returns false.

	// Activity#isChangingConfigurations()
    /**
     * Check to see whether this activity is in the process of being destroyed in order to be
     * recreated with a new configuration. This is often used in
     * {@link #onStop} to determine whether the state needs to be cleaned up or will be passed
     * on to the next instance of the activity via {@link #onRetainNonConfigurationInstance()}.
     *
     * @return If the activity is being torn down in order to be recreated with a new configuration,
     * returns true; else returns false.
     */
    public boolean isChangingConfigurations() {
        return mChangingConfigurations;
    }
Copy the code

IsChangingConfigurations Detects if the current Activity was destroyed because of a Configuration change. Returns true for a Configuration change, and false for a non-configuration change.

In conclusion, during activity destruction, determine that getViewModelStore().clear() will only be called if the destruction was not caused by a configuration change.

Third question: when will ViewModel#onCleared() be called to resolve?