Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How ViewModel survives configuration change

I am trying to use ViewModel in my app. The question comes to my mind is How View Model survives configuration changes. I read number of blog posts saying that "

It will create a HolderFragment to add to your activity or your fragment, it's invisible, when the configuration changed, activity destroyed, but holder fragment is still alive

and that make sense.But I tried to explore more on this and found out that in support library 27.1.0+ they have removed the HolderFragment with Description saying

Deprecate ViewModelStores.of() and the HolderFragment it relies on as they are no longer needed link for android.googlesource.

Now the question is How they are doing the same thing right now?

like image 863
FiXiT Avatar asked Nov 17 '19 18:11

FiXiT


People also ask

How does ViewModel survive screen rotation?

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations. Architecture Components provides ViewModel helper class for the UI controller that is responsible for preparing data for the UI.

How does ViewModel retain state?

When using this module, ViewModel objects receive a SavedStateHandle object through its constructor. This object is a key-value map that lets you write and retrieve objects to and from the saved state. These values persist after the process is killed by the system and remain available through the same object.

How does a ViewModel work internally?

ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way.It is the main component in the MVVM architecture. ViewModel can be created with activity context or fragment context. When a ViewModel object is created, it is stored inside Activity OR FragmentManager.

Is ViewModel destroyed when activity destroy?

The ViewModel exists from when you first request a ViewModel until the activity is finished and destroyed.


2 Answers

ViewModels created with ViewModelProviders.of() method are stored in ViewModelStore hashmap, so the real question is how ViewModelStore is stored.

For activities, this logic is straightforward. ViewModelStore is stored using onRetainNonConfigurationInstance method:

@Override
    @Nullable
    public final Object onRetainNonConfigurationInstance() {
        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 == null && custom == null) {
            return null;
        }

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

For fragments, things are a bit more complicated. FragmentManagerImpl now has a field called mNonConfig:

private FragmentManagerViewModel mNonConfig;

which stores a hashmap of a Fragment's UUID and a ViewModelStore.

This mNonConfig field is initialized in FragmentManagerImpl#attachController method:

    public void attachController(@NonNull FragmentHostCallback host,
            @NonNull FragmentContainer container, @Nullable final Fragment parent) {
        if (mHost != null) throw new IllegalStateException("Already attached");
        mHost = host;
        mContainer = container;
        mParent = parent;
        if (mParent != null) {
            // Since the callback depends on us being the primary navigation fragment,
            // update our callback now that we have a parent so that we have the correct
            // state by default
            updateOnBackPressedCallbackEnabled();
        }
        // Set up the OnBackPressedCallback
        if (host instanceof OnBackPressedDispatcherOwner) {
            OnBackPressedDispatcherOwner dispatcherOwner = ((OnBackPressedDispatcherOwner) host);
            mOnBackPressedDispatcher = dispatcherOwner.getOnBackPressedDispatcher();
            LifecycleOwner owner = parent != null ? parent : dispatcherOwner;
            mOnBackPressedDispatcher.addCallback(owner, mOnBackPressedCallback);
        }

        // Get the FragmentManagerViewModel
        if (parent != null) {
            mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
        } else if (host instanceof ViewModelStoreOwner) {
            ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
            mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
        } else {
            mNonConfig = new FragmentManagerViewModel(false);
        }
    }
like image 193
Stanislav Shamilov Avatar answered Nov 11 '22 14:11

Stanislav Shamilov


Basically, in order to retrieve a ViewModel in Activity one should call ViewModelProviders.of(this).get(SomeViewModel.class). Now, if we look into of it looks like below:

public static ViewModelProvider of(@NonNull FragmentActivity activity,
        @Nullable Factory factory) {
    Application application = checkApplication(activity);
    if (factory == null) {
        factory = ViewModelProvider.AndroidViewModelFactory.getInstance(application);
    }
    return new ViewModelProvider(activity.getViewModelStore(), factory);
}

So, the important part is this method - activity.getViewModelStore() because it returns a wrapper object (HashMap holder) for all of your ViewModel objects and if it can survive config changes so can all of your ViewModel objects:

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.");
    }
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

mViewModelStore will be either restored from NonConfigurationInstances or created from scratch. Pretty much, NonConfigurationInstances is the object that survives config changes and hence is used to store the ViewModelStore. That's why the same ViewModelStore object is returned after rotation - it's stored in the config change independent NonConfigurationInstances:

If you look into onRetainNonConfigurationInstance(), you will actually that your ViewModelStore is saved there:

public final Object onRetainNonConfigurationInstance() {
    ...
    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.custom = custom;
    nci.viewModelStore = viewModelStore;
    return nci;
}

Also, it will be cleared only if onDestroy for a non-config change reason is called:

...
    getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }
            }
        }
    });
...   

A similar trick is used for storing a ViewModel for a Fragment.

like image 45
Anatolii Avatar answered Nov 11 '22 13:11

Anatolii