ViewModel introduction

As one of the most frequent components in the Jetpack component library, 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. Its emergence relieves the pressure on activities/fragments to manage data, and viewModels are often used with LiveData in MVVM development mode.

Tips: Configuration changes mainly refer to: horizontal and vertical screen switch, resolution adjustment, permission change, system font style change…

Google the ViewModel document

Two, the introduction of ViewModel project

implementation :'androidx. Appcompat: appcompat: 1.2.0'
/ / or
implementation :'androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0'
Copy the code

Iii. Introduction of core classes and usage methods

The stored data can only be reused when the page is destroyed and rebuilt due to a configuration change, using the entire ViewModel instance object as a whole:

class MainActivity : AppCompatActivity() {

    private val myViewModel by lazy {
        ViewModelProvider(this).get(MyViewModel::class.java).apply {
            nameLiveData.observe(this@MainActivity, {})}}override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

}

class MyViewModel : ViewModel() {

    val nameLiveData = MutableLiveData<String>()

    override fun onCleared(a) {
        super.onCleared()
        Log.e("MyViewModel"."onCleared")}}Copy the code

Five, the implementation principle of source code analysis

The ViewModelProvider constructor takes the host ViewModelStore(HasMap) and Factory (used to create ViewModel instances)

private static final String DEFAULT_KEY =
        "androidx.lifecycle.ViewModelProvider.DefaultKey";
private final Factory mFactory;
private final ViewModelStore mViewModelStore;

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

NewInstanceFactory default Factory method creates ViewModel instance via class reflection optimization :(if custom implementation Factory overwrites create method can create ViewModel object in new mode)

public static class NewInstanceFactory implements Factory {

    private static NewInstanceFactory sInstance;

    @NonNull
    static NewInstanceFactory getInstance(a) {
        if (sInstance == null) {
            sInstance = new NewInstanceFactory();
        }
        return sInstance;
    }

    @SuppressWarnings("ClassNewInstance")
    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        //noinspection TryWithIdenticalCatches
        try {
            return modelClass.newInstance();// Create by reflection
        } 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

Create ViewModel instance

@NonNull
@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");
    }
    //key generates rules
    return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}


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

    if (modelClass.isInstance(viewModel)) {
        if (mFactory instanceof OnRequeryFactory) {
            ((OnRequeryFactory) mFactory).onRequery(viewModel);
        }
        return (T) viewModel;
    } else {
        //noinspection StatementWithEmptyBody
        if(viewModel ! =null) {
            // TODO: log a warning.}}if (mFactory instanceof KeyedFactory) {
        viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
    } else {
        viewModel = (mFactory).create(modelClass);// Create a ViewModel object with Factory
    }
    // Save to map
    mViewModelStore.put(key, viewModel);
    return (T) viewModel;
}
Copy the code

The getViewModelStore ViewModel instance object is not destroyed with the host rebuild, so ensure that the ViewModelStore instance object is not destroyed with the host rebuild

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner.ViewModelStoreOwner{
        
  private ViewModelStore mViewModelStore;     
        
  @NonNull
  @Override
  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) {
        / / as can be seen from the source, will begin from NonConfigurationInstances to obtain ViewModelStore instance objects,
       // If it is not empty, then it can be reused.
       When is stored in the / / so the emphasis is on ViewModelStore NonConfigurationInstances inside.
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if(nc ! =null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            mViewModelStore = nc.viewModelStore;
        }
        // No activity or fragment is started properly for the first time
        if (mViewModelStore == null) {
            mViewModelStore = newViewModelStore(); }}returnmViewModelStore; }}Copy the code

NonConfigurationInstances

This method is used to get instances of non-configuration items so that data can be recovered later when the Activity is rebuilt. This method is triggered when the page is recycled for system reasons. So the viewModelStore object is now stored in a NonConfigurationInstance. When the page is restored and rebuilt, the NonConfigurationInstance object is again passed to the new Activity for object reuse

Extension: can rewrite onRetainCustomNonConfigurationInstance ways to save our own custom data And used in rebuilding the activity of the recovery But Google has marked the method has been deprecated And comments: Use a {@ link androidx. Lifecycle. The ViewModel} if you want to retain your own non config state. (any of your data should be stored in the ViewModel)

public class ComponentActivity{
      
    static final class NonConfigurationInstances {
              Object custom;
              ViewModelStore viewModelStore;
    }
        
        
/**
 * Retain all appropriate non-config state.  You can NOT
 * override this yourself!  Use a {@link androidx.lifecycle.ViewModel} if you want to
 * retain your own non config state.
 */
 // Preserve all appropriate non-configured states. You can't rewrite it yourself
  @Override
  @Nullable
  public final Object onRetainNonConfigurationInstance(a) {
        // You need to customize the save data state so that you can override this function when the page is restored
       Object custom = onRetainCustomNonConfigurationInstance();
       
       ViewModelStore viewModelStore = mViewModelStore;
       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; }}// There is no need to save data state without viewModel and custom state
    if (viewModelStore == null && custom == null) {
        return null;
    }
    
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    returnnci; }}Copy the code

retainNonConfigurationInstances

When the configuration changes The method is used for the callback Activity onDestroy method, first to save data before the callback to ActivityClientRecord lastNonConfigurationInstances field

public class Activity{

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


NonConfigurationInstances retainNonConfigurationInstances(a) {
    // There is an instance of ViewModelStore in the activity
    Object activity = onRetainNonConfigurationInstance();
    HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
    FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

    // We're already stopped but we've been asked to retain.
    // Our fragments are taken care of but we need to mark the loaders for retention.
    // In order to do this correctly we need to restart the loaders first before
    // handing them off to the next activity.
    mFragments.doLoaderStart();
    mFragments.doLoaderStop(true);
    ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();

    if (activity == null && children == null && fragments == null && loaders == null
            && mVoiceInteractor == null) {
        return null;
    }

    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.activity = activity;/ / save the NonConfigurationInstances object
    nci.children = children;
    nci.fragments = fragments;
    nci.loaders = loaders;
    if(mVoiceInteractor ! =null) {
        mVoiceInteractor.retainInstance();
        nci.voiceInteractor = mVoiceInteractor;
    }
    returnnci; }}Copy the code

ActivityThread

There is a call to the following method of the ActivityThread class, which calls back to the Activity’s onDestroy method, Before the callback will first to save data to ActivityClientRecord lastNonConfigurationInstances field When restarting the Activity, will attach data to a new Activity instance, As a getLastNonConfigurationInstance () method return values, so as to complete the transfer of data

public final class ActivityThread{
//ArrayMap to store activtiy state data
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {··· Activity Activity =null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if(r.state ! =null) { r.state.setClassLoader(cl); }}catch (Exception e) {
            if(! mInstrumentation.onException(activity, e)) {throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ":"+ e.toString(), e); }}.../ / will r.l astNonConfigurationInstances passed into it Restore data
        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); ...return activity;
    }



// When an Activity Destory is called, it determines whether it was called because of the configuration change and saves the Activity state data

ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) { ActivityClientRecord r = mActivities.get(token); ...if (getNonConfigInstance) {// Configure the change to be called
                try {
                    / / save the Activity returns NonConfigurationInstances
                    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); }}}...// Call the Activity's onDestroy methodmInstrumentation.callActivityOnDestroy(r.activity); ...returnr; }}Copy the code

Restarting the Activity, will attach data to a new Activity instance, as a getLastNonConfigurationInstance () method return values, so as to complete the transfer of data

public class Activity{
   / / NonConfigurationInstances saved the ViewModel object instance
  NonConfigurationInstances mLastNonConfigurationInstances; 
   
  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) {...// Data recoverymLastNonConfigurationInstances = lastNonConfigurationInstances; . }public Object getLastNonConfigurationInstance(a) {
    returnmLastNonConfigurationInstances ! =null
            ? mLastNonConfigurationInstances.activity : null; }}public class ComponentActivity implements ViewModelStoreOwner{

    @NonNull
    @Override
    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) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if(nc ! =null) {
            / / from Activtiy NonConfigurationInstances get viewModel instance
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = newViewModelStore(); }}return mViewModelStore;
 }
}|
Copy the code

The viewModel does the release cleanup when the host lifecycle is destroyed

public ComponentActivity(a) {
    Lifecycle lifecycle = getLifecycle();
    //noinspection ConstantConditions
    getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                   // In the case of no configuration change
                if(! isChangingConfigurations()) {// ViewModel.onCleared is eventually calledgetViewModelStore().clear(); }}}}); }Copy the code

Five, ViewModel summary

Page configuration change data is not lost

When the device is rebuilt due to configuration changes of the Activity/Fragment, the data in the ViewModel will not be lost. With LiveData, the latest saved data can be received immediately after the page is rebuilt to re-render the page

Life cycle induction

The onCleared() method can be copied to terminate the clearing operation and free up memory. This method is called when the host onDestroy

Data sharing

For pages with a single Activity versus multiple fragments, you can use the ViewModel to share data between pages. In fact, different activities can also share data.

ViewModel and onSaveIntanceState

  • OnSaveIntanceState stores only lightweight key-value key-value pair data. This is triggered when a page is reclaimed due to a non-configuration change, and the data is stored in ActivityRecord.

  • The ViewModel can store any Object data, only valid when the page is reclaimed due to configuration changes. Save in ActivityThread#ActivityClientRecord.