Jetpack: ViewModel user guide, detailed analysis of implementation principle!

Posted by nlhowell on Sun, 05 Sep 2021 05:56:24 +0200

brief introduction

In order to better divide the functions, Android provides us with the ViewModel class, which is specially used to store the data required by the application page. ViewModel can be understood as something between View and Model. It acts as a bridge so that the View and data can not only be separated, but also maintain communication.

ViewModel vital features

The ViewModel object is usually requested when the onCreate() method of the activity object is called. However, the system may call onCreate() of the activity many times during the existence of the activity to go back to the life cycle (such as configuration changes such as device rotation). However, the time range for the existence of the ViewModel is from the first request for the ViewModel until the activity is completed and destroyed (at this time, the ViewModel will execute the onCleared method). For example, the activity life cycle is as follows, with device rotation in the middle. The ViewModel will be destroyed until the activity is actually destroyed.

ViewModel is an abstract class with only one onCleared() method. When the ViewModel is no longer needed, that is, the related activities are destroyed, this method will be called by the system. We can perform some operations related to resource release in this method. Note that this method will not be called if the Activity is rebuilt due to screen rotation.

rely on

buildscript {
    //android lifecycle version
    ext.lifecycle_version = "2.3.0"
}
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

Basic use of ViewModel

As mentioned earlier, the most important function of ViewModel is to separate the view from the data and independent of the reconstruction of activity. To verify this, we create a timer in the ViewModel and notify the activity every 1s. The activity counts the time and tests whether the data will be re timed after the activity is rotated. Write such a small experiment and show the basic use of ViewModel by the way.
1. Write a class inherited from ViewModel and name it TimingViewModel.

class TimingViewModel : ViewModel() {
    override fun onCleared() {
        super.onCleared()
        // Todo: resource release can be done on September 4, 2021
    }
}

**2. * * write a startTiming method to count down and call back to the main interface to refresh the UI. The complete code is as follows:

class TimingViewModel : ViewModel() {

    @Volatile
    var currentNum = 0

    /**
     * Callback main interface block
     */
    private lateinit var block: (Int) -> Unit

    /**
     * count down
     * @param block The countdown callback is scheduled to the main thread for execution
     */
    fun startTiming(block: (Int) -> Unit) {
        //block assignment
        this.block = block
        //Using collaborative scheduling
        viewModelScope.launch(Dispatchers.Default) {
            //Prevent screen rotation from calling multiple times
            if (currentNum <= 0) {

                while (true) {
                    delay(1000)
                    ++currentNum
                    //Notification interface refresh
                    withContext(Dispatchers.Main) {
                        this@TimingViewModel.block.invoke(currentNum)
                    }
                }
            }


        }
    }

    override fun onCleared() {
        super.onCleared()
        // Todo: resource release can be done on September 4, 2021
    }
}

Note: currentNum and block must be held by the most member variables in the ViewModel. In this way, onCreate will be called again after the activity configuration is rebuilt, and then the startTiming method will be called. With the constant life cycle of ViewModel, the value of currentNum will not be refreshed. Block can be assigned as a new incoming block.

3. Create a TimingActivity and use the ViewModelProvider to instantiate the ViewModel (this can ensure that the ViewModel life cycle remains unchanged when the activity changes, because the ViewModelProvider will judge whether the ViewModel exists. If it exists, it will return directly, otherwise it will create a ViewModel). Then call startTiming and update UI.

class TimingActivity : AppCompatActivity() {

    lateinit var binding: ActivityCommonBinding

    lateinit var timingViewModel: TimingViewModel

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        binding = ActivityCommonBinding.inflate(layoutInflater)
        setContentView(binding.root)

        //Initialize ViewModel
        timingViewModel = ViewModelProvider(this).get(TimingViewModel::class.java)

        //Call to start timing and update the interface
        timingViewModel.startTiming { currentTime ->
            binding.tvContent.text = "currentTime : $currentTime"
        }
    }
}

Run the program and rotate the screen. The result is as follows: the timer does not stop when rotating the screen causes the Activity to rebuild. This means that the ViewModel corresponding to the Activity in the horizontal / vertical screen state is the same, it has not been destroyed, and the data held always exists.

Comprehensive analysis of ViewModel principle

Instantiate the of ViewModel through ViewModelProvider in the page.

timingViewModel = ViewModelProvider(this).get(TimingViewModel::class.java)

Take a look at the constructor of ViewModelProvider:

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

The ViewModelProvider receives a ViewModelStoreOwner object as a parameter. In the above example code, the parameter is this. So where the Activity must inherit ViewModelStoreOwner or its subclass. Find out and finally find that the ViewModelStoreOwner is inherited in the ComponentActivity. The getViewModelStore method is implemented (the getViewModelStore method is the only method in the ViewModelStoreOwner interface, and you can find the source code by yourself).

public class ComponentActivity extends androidx.core.app.ComponentActivity implements
        LifecycleOwner,
        ViewModelStoreOwner,
        SavedStateRegistryOwner,
        OnBackPressedDispatcherOwner {
            ...
          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;
            }
            ...
        }

Analyze this method. If getApplication is null, it throws an exception directly, so it is not possible to call ViewModelProvider to instantiate ViewModel before onCreate (which is instantiated in onCreate). The following logical analysis. getLastNonConfigurationInstance returns the return value of onretainonconfigurationinstance. The onRetainNonConfigurationInstance method will call to save information when the configuration changes. Here, the ViewModelStore is returned. In the onRetainNonConfigurationInstance method of ComponentActivity, the ViewModelStore will be saved before the configuration changes and the activity changes and is destroyed (the code is relatively simple and can be viewed by yourself, so it will not be posted). Therefore, when retrieving the ViewModelStore, first judge whether there is a ViewModelStore in getLastNonConfigurationInstance. If so, retrieve it directly, otherwise a new ViewModelStore will be created. This ensures that the ViewModelStore will not be destroyed due to the change of activity configuration and life cycle, and will remain the same object!

The interface method getViewModelStore() returns ViewModelStore. After talking about ViewModelStore for a long time, let's see what ViewModelStore is.

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

It can be seen from the source code of ViewModelStore that ViewModel is actually cached in the form of * * HashMap < string, ViewModel > * * in the form of. Provide get, put and clear related methods. There is no direct association between ViewModel and page. They are associated through ViewModelProvider. Next, let's take a look at the get method of ViewModelProvider.

public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass){
    //The key passed in by the default get method is a defined constant, so don't worry
    ViewModel viewModel = mViewModelStore.get(key);

    if (modelClass.isInstance(viewModel)) {
        ...
        //If the current viewmodel exists in the ViewModelStore, it will be returned directly.
        return (T) viewModel;
    } else {
        ...
    }
    if (mFactory instanceof KeyedFactory) {
        viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
    } else {
        //Otherwise, a new corresponding is created directly, and the newInstance method is actually called internally
        viewModel = mFactory.create(modelClass);
    }
    mViewModelStore.put(key, viewModel);
    return (T) viewModel;
}

The ViewModelProvider checks whether the ViewModel already exists in the cache. If it exists, it returns directly. If it does not exist, it instantiates one.

There is another problem. How does the ViewModel know that the activity is really destroyed and then call the onCleared method? In fact, the activity life cycle is monitored in the construction method of ComponentActivity. If the activity goes to OnDestroy and the configuration has not changed, call the clear method of ViewModelStore. The code is as follows:

public ComponentActivity() {
    Lifecycle lifecycle = getLifecycle();
   	...
    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();
                }
            }
        }
    });
    ...
}

To sum up:

The ViewModelProvider checks whether the ViewModel already exists in the cache. If it does exist, it returns directly. If it does not exist, it instantiates one, and the collection ViewModelStore storing the ViewModel is saved in the Activity through onRetainNonConfigurationInstance, Judge through Lifecycle that when the Activity is really destroyed, the clear method of ViewModelStore clears the ViewModel callback onCleared method. Therefore, the destruction and reconstruction of Activity due to configuration changes will not affect the ViewModel, which exists independently of the page. Therefore, when using the ViewModel, you should pay special attention not to pass any type of Context or objects with Context references into the ViewModel, which may cause the page to be unable to be destroyed, resulting in memory leakage!!!.

Other gossip

AndroidViewModel

When using the ViewModel, you cannot pass any type of Context or objects with Context references into the ViewModel because this may lead to memory leaks. But what if you really need to use Context in ViewModel? You can use the AndroidViewModel class, which inherits from ViewModel and receives Application as Context.

Difference between ViewModel and onSaveInstanceState()

The onSaveInstanceState() method can only save a small amount of data that can support serialization. But onSaveInstanceState()

ViewModel can support all data in the page. However, the ViewModel does not support data persistence. When the interface is completely destroyed, the ViewModel and its data will not exist.

Topics: Android kotlin jetpack