The ViewModel background

The ViewModel class is designed to store and manage interface-related data in a life-cycle oriented manner. The ViewModel class allows data to persist after configuration changes such as screen rotation.

Excerpt from ViewModel Overview

Specifically, the ViewModel has the following characteristics:

  1. For simple data, the onSaveInstanceState() method can be used when an Activity is destroyed to recover its bound data from onCreate, but not for large amounts of data, such as user lists or bitmaps. The Viewmodel supports large amounts of data and does not require serialization or deserialization.
  2. View controllers often require asynchronous calls that may take some time for the process to return. The interface controller needs to manage these calls and ensure that the system cleans them up after they are destroyed to avoid potential memory leaks. The Viewmodel is a good way to avoid memory leaks.
  3. If the interface controller is also responsible for loading data from the database or network, the class becomes bloated. Assigning too much responsibility to an interface controller can cause a single class to try to handle all the work of the application itself, rather than delegating it to another class. The Viewmodel effectively separates the view data logic from the view controller.

As you can see, the VIewmodel stores and manages view-related data in the form of an awareness lifecycle.

The basic use

With a preliminary understanding of the Viewmodel’s characteristics, let’s start with a simple operation.

Custom data retrieval classes

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

Custom ViewModel inherits ViewMode to implement custom ViewModel.

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

MutableLiveData is created and updated with the setVale method of MutableLiveDat.

You can then use TestViewModel in your Activity.

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

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

The ViemodelProvider is used to retrieve the TestViewModel object and to observe its changes with LiveData. Through log printing we get:

MaiinViewModel: nameListResult: [ha, ha, ha]Copy the code

When we rotated the phone again, the printed message was still there.

MaiinViewModel: nameListResult: [ha, ha, ha]Copy the code

Even if MainActivity is rebuilt, the ViewModel instance object still exists, and the internal LiveData has not changed.

How can the ViewModel recover data after the Activity rotates the screen? Now let’s analyze its source code.

Source code analysis

The creation of the Viewmodel

First, let’s look at how the Viewmodel is created.

From the above we can see that the Viewmodel is created as follows:

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

You pass in the class type, and you get the ViewModel. Take a look at the constructor of ViewModelProcider:

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

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

So here we see he’s got the ViewmodelStore. It could be that all we’re passing in is an instance of the Activity, and what’s the ViewModelStoreOwner?

// Have the ViewModelStore scope.
// The implementation of this interface is responsible for retaining the ViewModelStore it owns during configuration changes,
// And call viewModelStore.clear () when the scope will be destroyed.
public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore(a);
}
Copy the code

ViewModelStoreOwner is an interface. Let’s see if ComponentActivity implements it:

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner.ViewModelStoreOwner.HasDefaultViewModelProviderFactory.SavedStateRegistryOwner.OnBackPressedDispatcherOwner {...// Implements ViewModelStoreOwner and overwrites getViewModelStore
      public ViewModelStore getViewModelStore(a) {
        if (getApplication() == null) {
          // The contents of the file are analyzed below.returnmViewModelStore; }}Copy the code

ComponentActivity implements the ViewModelStoreOwner interface.

Back to the example, ViewModelProvider (this). The get (TestViewModel: : class. Java) of the get method is done?

### ViewModelProvider
   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, which is the Map in ViewmodelStore used to store the ViewModel key
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
  	// Get the ViewModel instance
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            if (mFactory instanceof OnRequeryFactory) {
                ((OnRequeryFactory) mFactory).onRequery(viewModel);
            }
            // Return the cached ViewModel object
            return (T) viewModel;
        } else {
            if(viewModel ! =null) {}}// Use factory mode to create ViewModel instance
        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 (T) viewModel;
    }
Copy the code

MViewModelStore = mViewModelStore = mViewModelStore = mViewModelStore = mViewModelStore

### ViewModelStore
public class ViewModelStore {
   // Internally stored by HashMap
    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 viewModel clear method and clear the viewModel
    public final void clear(a) {
        for(ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }}Copy the code

So in ViewModelStore we do that by storing the viewModel as a value in a HashMap.

So having seen how the VIewModel is created, and where the instance is cached, let’s look at how the ViewModelStore is created.

The creation of ViewmodelStore

Let’s go back to the getViewModelStore method above:

# # #ComponentActiivty   
public ViewModelStore getViewModelStore(a) {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mViewModelStore == null) { 
          / / get NonConfigurationInstances
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if(nc ! =null) {
                / / from NonConfigurationInstances ViewModelStore recovery
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = newViewModelStore(); }}return mViewModelStore;
    }
Copy the code

First obtained from the NonConfigurationInstances VIewModelStore examples, if not, will create a new ViewModelSotre. Here we knew, create ViewModelStore will cache the NonConfigurationInstances. And when did the ViewModelStore get cached?

Look at getLastNonConfigurationInstance method:

### Activity 
 / / previous by onRetainNonConfigurationInstance () returns the object
  /**
     * 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()}
     */
public Object getLastNonConfigurationInstance(a) {
        returnmLastNonConfigurationInstances ! =null
                ? mLastNonConfigurationInstances.activity : null;
    }
Copy the code

Through the above comments can know by onRetainNonConfigurationInstance mLastNonConfigurationInstances is returned.

And when the called onRetainNonConfigurationInstance? We finally found it:

# # #ActivityThread
void performDestroyActivity(ActivityClientRecord r, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) {...if (getNonConfigInstance) {
                try {
                    r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();
                } catch (Exception e) {
                    if(! mInstrumentation.onException(r.activity, e)) {throw new RuntimeException("Unable to retain activity "
                                + r.intent.getComponent().toShortString() + ":"+ e.toString(), e); }}}... }Copy the code

Called in the performDestroyActivity method, Can see onRetainNonConfigurationInstance () method returns the Object will be assigned to ActivityClientRecord lastNonConfigurationInstances, like this is preserved. Look at the onRetainNonConfigurationInstance method.

### ComponetActivity    
  // Called when the configuration has changed
  // Preserve all appropriate non-configured states
public final Object onRetainNonConfigurationInstance(a) {
        Object custom = onRetainCustomNonConfigurationInstance();

        ViewModelStore viewModelStore = mViewModelStore;
        if (viewModelStore == null) {
            // No one called getViewModelStore(), so let's see if there are any existing ViewModelStores in our last NonConfigurationInstance
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if(nc ! =null) { viewModelStore = nc.viewModelStore; }}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 knew, the Activity of the reconstruction for configuration changes destroyed when calls onRetainNonConfigurationInstance will save to NonConfigurationInstances ViewModelStore instance. After reconstruction can be used when getLastNonConfigurationInstance method to get the cache before ViewModelStore instance.

NonConfigurationInstances what, how can still remain in the destruction of reconstruction? Look at NonConfigurationInstances stored in where, where were the first to look at the assignment.

# # #Activity
  final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
            IBinder shareableActivityToken) {...// Assign is done in the attach methodmLastNonConfigurationInstances = lastNonConfigurationInstances; . }Copy the code

MLastNonConfigurationInstances is the Activity of the attach assignment method.

# # #ActivityThread

  private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) { r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo, Context.CONTEXT_INCLUDE_CODE); } · · · · · ·// Obtained from ActivityClientRecord
        activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, r.embeddedID, r.lastNonConfigurationInstances, config, r.referrer, r.voiceInteractor, window, r.configCallback, r.assistToken, r.shareableActivityToken); . }Copy the code

In the Activity of performLaunchActivity approach, can see in ActivityClientRecord lastNonConfigurationInstances is kept.

And since the interface calls the performDestroyActivity method at destruction time, Internal and invokes the Activity lastNonConfigurationInstances cache to ActivityClientRecord retainNonConfigurationInstances method, is put into the process of the application itself.

So the page at the time of destruction of reconstruction, save lastNonConfigurationInstances in ActivityClientRecord is not affected.

Here we also know why the ViewModel still exists after the Activity is rebuilt due to configuration changes.

The destruction of the ViewModel

We already know that the ViewModle clear method is called in the ViewmodelStore.

###ViewModelStore
public class ViewModelStore {...public final void clear(a) {
        for(ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }}Copy the code

When does the viewModel store clear method get called in order to iterate through the cache?

# # #ComponentActivity
public ComponentActivity(a) {... getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
              // The activity life cycle is in destory state
                if (event == Lifecycle.Event.ON_DESTROY) {
                  // The non-configuration has changed
                    if(! isChangingConfigurations()) { getViewModelStore().clear(); }}}}); }Copy the code

Here we see that the condition for calling the Clear method in ViewModelStore is called first by observing that the current lifecycle is in DESTORY, and also by detecting that the Configurations are destroyed because of a change.

So when the activity is destroyed, determine that getViewModelStore().clear() is called only if the destruction was not caused by a configuration change.

ViewModelScope understand?

ViewModelScope is a Kotlin extension property of the ViewModel. It can exit when the ViewModel is destroyed (onCleared() method is called). And when did it close?

Let’s take a look at its implementation:

//CoroutineScope is bound to the ViewModel. When the ViewModel cleared, this range will be cancelled, namely call / / ViewModel. The onCleared the range bound to Dispatchers. Main. Immediate
val ViewModel.viewModelScope: CoroutineScope
        get() {
            val scope: CoroutineScope? = this.getTag(JOB_KEY)
            if(scope ! =null) {
                return scope
            }
          // A missed buffer is added to the ViewModel via setTagIfAbsent()
            return setTagIfAbsent(JOB_KEY,
                CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
        }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close(a) {
        coroutineContext.cancel()
    }
}
Copy the code

As you can see from the comments, the Viewmoel is removed from scope when the onClear method of the ViewModel is called. And why does clearing the ViewModel shut it down?

Let’s look at the Viewmodel clear method again:

### ViewModel
 // This method is called when the ViewModel is no longer used and destroyed.
 protected void onCleared(a) {}final void clear(a) {
        mCleared = true;
        // Since clear() is final, this method is still called on the mock object, and mBagOfTags are empty in these cases. It is always empty
  // Since setTagIfAbsent and getTag are not final, we can skip clearing them
        if(mBagOfTags ! =null) {
            synchronized (mBagOfTags) {
                for (Object value : mBagOfTags.values()) {
                    // see comment for the similar call in setTagIfAbsent
                    closeWithRuntimeException(value);
                }
            }
        }
        onCleared();
    }
Copy the code

How is the onClear method empty? Now that this method is called by the clear method, what is mbagofTags?

private final Map<String, Object> mBagOfTags = new HashMap<>();
Copy the code

This is a Map object, and if you look further down you can see that setTagIfAbsent is assigned.

 <T> T setTagIfAbsent(String key, T newValue) {
        T previous;
        synchronized (mBagOfTags) {
            previous = (T) mBagOfTags.get(key);
            if (previous == null) {
              // Put it into map if it has not been saved before
                mBagOfTags.put(key, newValue);
            }
        }
        T result = previous == null ? newValue : previous;
   			//// If this viewModel is marked clear
        if (mCleared) {
            closeWithRuntimeException(result);
        }
        return result;
    }
Copy the code

The setTagIfAbsent method is called when the viewModelScope is first created, which is where the object is added to the ViewModel.

To review, the first time viewModelScope is called, setTagIfAbsent(JOB_KEY, CloseableCoroutineScope) is used for caching.

Look at the CloseableCoroutineScope class, which implements the Closeable interface and cancels the coroutine scoped coroutineContext object in close().

conclusion

We first introduced the background of viewModel, to understand its characteristics: due to configuration changes interface destruction and reconstruction still exist, do not hold UI applications, also introduced the basic use of viewModel. Finally, a large section of the ViewModel analysis of several key core principles.

This is the end of our analytical journey

reference

The ViewModel website

Android-viewmodel summary