Principle analysis of JectPack components -- ViewModel

Posted by techrat on Tue, 25 Jan 2022 07:48:49 +0100

In the JectPack component, ViewModel is mainly used to encapsulate the data related to the interface. Similarly, ViewModel has the ability of life cycle awareness. It always exists in memory before the destruction of Activity or Fragment onDetach; Especially after system configuration changes such as screen rotation, the interface data saved by ViewModel still exists

1. Creation of ViewModel

It is also possible to create a ViewModel by directly creating a new object, but the official method is to obtain it through ViewModelProvider + get. In Kotlin, this method is encapsulated into the syntax sugar of viewModels and activityViewModels to create viewModels through these two methods

@MainThread
public inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)

Here, take viewModels, this is an inline advanced function. When it is called in the onCreateView method of Fragment, it directly completes the creation of ViewModel.

val viewModel = viewModels<MyViewModel>()

viewModels has two parameters, ownerProducer. The return is a ViewModelStoreOwner. The default is this, which is the current Fragment calling the function; factoryProducer, the factory that creates the ViewModel, can be empty and cannot be transferred
If you want to use your own factory to create ViewModel, you need to implement viewmodelprovider Factory interface

class ViewModelFactory : ViewModelProvider.Factory {


	override fun <T : ViewModel?> create(modelClass: Class<T>): T {
	
	    return modelClass.getConstructor(MyViewModel::class.java).newInstance()
	}
}

Use as follows

val viewModel = viewModels<MyViewModel>{
	ViewModelFactory()
}

Another way is to create viewModels with activityViewModels, which requires a context. Others are the same as viewModels. The differences between the two will be explained later.

2 ViewModel source code analysis

Both viewModels and activityViewModels will go to the createViewModelLazy method,

@MainThread
public fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        defaultViewModelProviderFactory
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

For viewModels, if the incoming factoryProducer is empty, a default ViewModelProviderFactory will be created

Fragment # getDefaultViewModelProviderFactory

 public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
        if (mFragmentManager == null) {
            throw new IllegalStateException("Can't access ViewModels from detached fragment");
        }
        if (mDefaultFactory == null) {
            Application application = null;
            Context appContext = requireContext().getApplicationContext();
            while (appContext instanceof ContextWrapper) {
                if (appContext instanceof Application) {
                    application = (Application) appContext;
                    break;
                }
                appContext = ((ContextWrapper) appContext).getBaseContext();
            }
            if (application == null && FragmentManager.isLoggingEnabled(Log.DEBUG)) {
                Log.d(FragmentManager.TAG, "Could not find Application instance from "
                        + "Context " + requireContext().getApplicationContext() + ", you will "
                        + "not be able to use AndroidViewModel with the default "
                        + "ViewModelProvider.Factory");
            }
            mDefaultFactory = new SavedStateViewModelFactory(
                    application,
                    this,
                    getArguments());
        }
        return mDefaultFactory;
    }

Finally, a SavedStateViewModelFactory is returned, which is the default factory. Here, it will judge whether the context application is empty. If it is empty, mFactory = NewInstanceFactory; Otherwise, it is AndroidViewModelFactory. When activityViewModels is called, a context will be passed in, and the source code can be viewed by yourself

public SavedStateViewModelFactory(@Nullable Application application,
         @NonNull SavedStateRegistryOwner owner,
         @Nullable Bundle defaultArgs) {
     mSavedStateRegistry = owner.getSavedStateRegistry();
     mLifecycle = owner.getLifecycle();
     mDefaultArgs = defaultArgs;
     mApplication = application;
     mFactory = application != null
             ? ViewModelProvider.AndroidViewModelFactory.getInstance(application)
             : ViewModelProvider.NewInstanceFactory.getInstance();
 }

Finally, the ViewModel is obtained in the ViewModelLazy method

ViewModelProvider # ViewModelLazy

public class ViewModelLazy<VM : ViewModel> (
    private val viewModelClass: KClass<VM>,
    private val storeProducer: () -> ViewModelStore,
    private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
    private var cached: VM? = null

    override val value: VM
        get() {
            val viewModel = cached
            return if (viewModel == null) {
                val factory = factoryProducer()
                val store = storeProducer()
                ViewModelProvider(store, factory).get(viewModelClass.java).also {
                    cached = it
                }
            } else {
                viewModel
            }
        }

    override fun isInitialized(): Boolean = cached != null
}

First, get the ViewModel from the cache. If there is no ViewModel in the cache, get it through ViewModelProvider + get. Here we mainly look at the get method

ViewModelProvider # get

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

Here is the full class name and default of the class_ Key is spliced into a key and retrieved from mViewModelStore. mViewModelStore is the default this in viewModels. Each Activity and Fragment has an mViewModelStore

Both Activity and Fragment implement the ViewModelStoreOwner interface. There is a method in this interface

public interface ViewModelStoreOwner {
    @NonNull
    ViewModelStore getViewModelStore();
}

When createViewModelLazy is called, ViewModelStore is actually passed in. ViewModelStore is actually a HashMap

ComponentActiivty # getViewModelStore

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;
}

@SuppressWarnings("WeakerAccess") /* synthetic access */
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();
        }
    }
}

Through the spliced key, go to mViewModelStore to get the ViewModel. If you haven't created a ViewModel for the first time, you can't get it. Then go to the position of 1

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);
        //Come in for the second time and take this position
        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.
            }
        }
        //1 the first one to come in and take this position
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
        } else {
            viewModel = mFactory.create(modelClass);
        }
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }

Here, we will judge the type of the incoming factory. Generally, we will use else and the create method of the project we created ourselves

ViewModelProvider # Factory – create

public interface Factory {
    @NonNull
    <T extends ViewModel> T create(@NonNull Class<T> modelClass);
}

In this way, a ViewModel is created and saved to mViewModelStore;

Therefore, when creating a ViewModel, viewModels first takes it from the cache. If it is not in the cache, it gets it from the mViewModelStore. If it is not in the mViewModelStore, it is recreated. It is equivalent to a three-level cache, which is a singleton mode.

3 ViewModel storage data

When the screen rotates or other configurations change, the Activity will be destroyed and rebuilt. The page data is saved. Usually, onSaveInstanceState is called when the page is destroyed and onRestoreInstanceState is called when the page is rebuilt. This can only save lightweight data (because of the Bundle). Therefore, the emergence of ViewModel solves the problem that the traditional method can not store a large amount of data

In the activity, there are two non lifecycle methods, onRetainNonConfigurationInstance and getLastNonConfigurationInstance

ComponentActivity # onRetainNonConfigurationInstance

public final Object onRetainNonConfigurationInstance() {
        // Maintain backward compatibility.
        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;
            }
        }
		//If viewModelStore is empty,
		//Or the user-defined status is empty, and there is no user-defined saving status, that is, it does not need to be saved
        if (viewModelStore == null && custom == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore;
        return nci;
    }

Onretainumconfigurationinstance will be called when the screen is rotating. Onretainumcustomnonconfigurationinstance will be called when the page is ready to be destroyed. This method can be rewritten to customize the storage state of page data. At this time, get mViewModelStore, and create NonConfigurationInstances before clearing it from memory, Save viewModelStore and data save status to NonConfigurationInstances

Activity # retainNonConfigurationInstances

NonConfigurationInstances retainNonConfigurationInstances() {
		// Call onRetainNonConfigurationInstance in ComponentActivity
		// What is saved in the activity is the viewModel and the data store state
		
        Object activity = onRetainNonConfigurationInstance();
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

        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;
        nci.children = children;
        nci.fragments = fragments;
        nci.loaders = loaders;
        if (mVoiceInteractor != null) {
            mVoiceInteractor.retainInstance();
            nci.voiceInteractor = mVoiceInteractor;
        }
        return nci;
    }

Finally, before the Activity is destroyed, all the Activity related information is saved in NonConfigurationInstances

After the Activity is destroyed and restarted, lastNonConfigurationInstances is passed in the attach method, which is the status information saved before the page is destroyed

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,
            //Saved page data information
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
            IBinder shareableActivityToken) 

Here's how to transfer it. Add a TODO, and then focus on the source code in AMS. In this way, after the Activity is restarted, go to getViewModelStore again and call getLastNonConfigurationInstance

Activity # getLastNonConfigurationInstance

public Object getLastNonConfigurationInstance() {
    return mLastNonConfigurationInstances != null
            ? mLastNonConfigurationInstances.activity : null;
}

Here, mLastNonConfigurationInstances is no longer empty (passed in the attach), and mLastNonConfigurationInstances is returned activity, which saves the viewModelStore and the saved data state of the page, so as to save the page data

How the ViewModel is destroyed after the page is destroyed is the credit of LifeCycle

Construction method of 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();
            }
        }
    }
});

When supervisor hears Activity on_ After destroy, all viewmodels in the ViewModelStore will be cleared. Of course, after saving the data

Topics: Android kotlin ViewModel