Preface – What do you get from reading this article?

The more Jetpack components are used, the more likely they are to be asked in interviews

This article mainly includes the following contents

  • There are four ways to use a ViewModel
  • Why can the ViewModel recover data after an Activity rotates the screen
  • Why can ViewModel in Fragment recover data after screen rotation
  • Why can fragments share data via ViewModel

With the release of Jetpack, there are a lot of things that are convenient for us to develop, and the ViewModel is one of them. 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.

The ViewModel is used in four ways

Here are four ways to use the ViewModel

  • Saved State in ViewModel — Data recovery in ViewModel when background process restarts;
  • Using ViewModel in NavGraph — integration of ViewModel with Navigation component library;
  • ViewModel with data-binding — Simplify data binding by using ViewModel and LiveData;
  • ViewModelScope – Integration of the Kotlin coroutine with the ViewModel

Here is my common ViewModel data recovery and ViewModel and coroutines combined with two, more visible, knowledge | ViewModel four integrated way

Saved State of ViewModel – The ViewModel data is restored when the background process restarts

As we all know, the ViewModel can hold data while the screen rotates. But when the application is killed by the system in the background, the ViewModel data does not recover when the page is reopened. In this case, you need to combine with SavedStateHandle to save the data when the background process reclaims it.

Let’s look at how to use SavedStateHandle

Step 1: Add dependencies

SaveStateHandle is currently in a separate module and you need to add the following dependencies:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version
Copy the code
Step 2: Change how the ViewModelProvider is called

Next, you need to create a ViewModel that holds the SaveStateHandle. In the onCreate method of your Activity or Fragment, change the call to the ViewModelProvider to:

// The following Kotlin extensions need to rely on the following or newer versions of the KTX library:/ / androidx. Fragments: fragments - KTX: 1.0.0 or (latest version 1)/ / androidx. Activity: activity - KTX: 1.0.0 (the latest version 1.1.0)val viewModel by viewModels { SavedStateViewModelFactory(application, this) }
// Do not use KTXval viewModel = ViewModelProvider(this, SavedStateViewModelFactory(application, this))  .get(MyViewModel::class.java) Copy the code

Create ViewModel classes is the ViewModel factory (the ViewModel factory), and create contains SaveStateHandle View Model factory class is SavedStateViewModelFactory. The ViewModel created through this factory will hold a SaveStateHandle based on the Activity or Fragment passed in.

Step 3: Use SaveStateHandle

When the previous steps are ready, you can use SavedStateHandle in the ViewModel. Here is an example of saving a user ID:

class MyViewModel(state :SavedStateHandle) :ViewModel() {

// Declare Key as a constant    companion object {
        private val USER_KEY = "userId"
 }   private val savedStateHandle = state   fun saveCurrentUser(userId: String) { // Store the data corresponding to the userId savedStateHandle.set(USER_KEY, userId)  }   fun getCurrentUser(): String { // Retrieve the current userId from saveStateHandle returnsavedStateHandle.get(USER_KEY)? :""  } } Copy the code

If you want to use in the ViewModel LiveData, can call SavedStateHandle. GetLiveData (), the sample is as follows:

The getLiveData method gets a MutableLiveData associated with the key// MutableLiveData is updated when the value corresponding to the key changes.private val _userId : MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)

// Only one immutable LiveData case is exposedval userId : LiveData<String> = _userId Copy the code

ViewMode and Kotlin coroutine: viewModelScope

Typically, we use callbacks to handle asynchronous calls, which can result in nested layers of callbacks and unintelligible code when the logic is complex. Kotlin Coroutines are also useful for handling asynchronous calls, keeping the logic simple while ensuring that operations do not block the main thread

A simple coroutine code:

Do not use GlobalScope in a real world scenarioGlobalScope.launch {
    longRunningFunction()
    anotherLongRunningFunction()
}
Copy the code

This example code only starts one coroutine, but it would be easy to create many coroutines in a real-world environment, which would inevitably result in some coroutines whose state could not be tracked. If one of these coroutines happens to have a task you want to stop, it causes a work leak.

To prevent task leakage, you need to add coroutines to a CoroutineScope. CoroutineScope keeps track of the execution of coroutines, which can be cancelled. When the CoroutineScope is cancelled, all coroutines it tracks are cancelled. In the code above, I used GlobalScope, which is generally not recommended, just as we don’t recommend arbitrary use of global variables. So, if you want to use coroutines, you either qualify a scope or get access to a scope. In ViewModel, we can use viewModelScope to manage the scope of coroutines.

viewModelScope

When a ViewModel is destroyed, there are usually some operations associated with it that should be stopped.

For example, suppose you are preparing to display a bitmap on the screen. This operation meets some of the characteristics we mentioned earlier: it neither blocks the main thread during execution, nor requires execution to stop when the user exits the relevant interface. When you use coroutines for such operations, you should use viewModelScope.

ViewModelScope is a Kotlin extension property of the ViewModel. As mentioned earlier, it can exit when the ViewModel is destroyed (when the onCleared() method is called). This way, as long as you use the ViewModel, you can use the viewModelScope to start various coroutines in the ViewModel without worrying about task leakage

class MyViewModel() : ViewModel() {

    fun initialize() {
        viewModelScope.launch {
            processBitmap()
 }  }   suspend fun processBitmap() = withContext(Dispatchers.Default) { // Do time-consuming operations here }  } Copy the code

Why can the ViewModel save data when rotated?

We all know that the ViewModel will not be destroyed when the rotation of the Activity changes. The ViewModel scope is as follows:When an Activity changes due to rotation, the system creates a new Activity. So how does the ViewModel from the old Activity get passed to the new Activity?

Let’s start with a look at some common ways to recover data

Use onSaveInstanceState and onRestoreInstanceState

When your Activity starts and stops, the onSaveInstanceState() method is called so that your Activity can save state information to the instance state Bundle.

After rebuilding the previously destroyed Activity, you can restore the saved instance state from the Bundle passed to the Activity by the system. The onCreate() and onRestoreInstanceState() callback methods both receive the same Bundle containing the instance state information.

Use setRetainInstance of fragments

When configuration changes, the Fragment is destroyed and rebuilt along with the host Activity. When we call the setRetainInstance(True) method in the Fragment, the system allows the Fragment to bypass the destruction-rebuild process. Using this method, signals are sent to the system to keep the Fragment instance while the Activity rebuilds. Note that:

  • After using this method, the onDestory() method of the Fragment is not called, but the onDetach() method is still called
  • After using this method, the onCreate(Bundle) method of the Fragment is not called. Because the Fragment was not rebuilt.
  • After using this method, the Fragment’s onAttach(Activity) and onActivityCreated(Bundle) methods are still called.

Use onRetainNonConfigurationInstance with getLastNonConfigurationInstance

Provides onRetainNonConfigurationInstance method in the Activity, used for handling configuration changes data preservation. Then in recreating the Activity call getLastNonConfigurationInstance get saved data last time. We can’t override the above methods directly. If we want to customize the data we want to restore in the Activity, we need to call the internal methods of the above two methods:

  • onRetainCustomNonConfigurationInstance()
  • getLastCustomNonConfigurationInstance()

Note: System calls onRetainNonConfigurationInstance method timing between onStop – onDestory, GetLastNonConfigurationInstance method can call in onCreate and onStart method.

Summary of several data recovery methods

By understanding several ways of data recovery, we can get the following comparison diagram:

The recovery of the ViewModel

The ViewModel was officially designed with a preference for data recovery when configuration changes were made. Considering the efficiency of data recovery, officials eventually adopted onRetainNonConfigurationInstance way to restore the ViewModel.

Now that we know how to restore the ViewModel, let’s solve our previous puzzle. When the Activity changes due to configuration, the system creates a new Activity. How does the ViewModel from the old Activity get passed to the new Activity?

In the latest code, the Activity of Androidx official rewrite the onRetainNonConfigurationInstance method, This method saves the ViewModelStore (ViweModelStore stores the ViewModel), which in turn saves the ViewModel as follows:

    public final Object onRetainNonConfigurationInstance() {
        Object custom = onRetainCustomNonConfigurationInstance();

        ViewModelStore viewModelStore = mViewModelStore;
        if (viewModelStore == null) {
 NonConfigurationInstances nc =  (NonConfigurationInstances) getLastNonConfigurationInstance();  if(nc ! = null) { viewModelStore = nc.viewModelStore;  }  }   if (viewModelStore == null && custom == null) {  return null;  }  / / will be stored in NonConfigurationInstances ViewModel objects NonConfigurationInstances nci = new NonConfigurationInstances();  nci.custom = custom;  nci.viewModelStore = viewModelStore;  return nci;  } Copy the code

So we know that after the screen rotates, when the new Activity is recreated and calls viewModelproviders.of (this).get(xxxModel.class), The ViewModelStore saved by the old Activity is retrieved in the getViewModelStore() method.

    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) { / / 👇 save NonConfigurationInstances, NonConfigurationInstances nc =  (NonConfigurationInstances) getLastNonConfigurationInstance();  if(nc ! = null) {//👇 gets the ViewModelStore from this object mViewModelStore = nc.viewModelStore;  }  if (mViewModelStore == null) {  mViewModelStore = new ViewModelStore();  }  }  return mViewModelStore;  } Copy the code

And the ViewModel is actually stored in the ViewModelStore, and when the ViewModelStore is restored, then you get the ViewModel. The specific code is as follows: Get the relevant code of ViewModel from ViewModelStroe

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

 if (modelClass.isInstance(viewModel)) {  //noinspection unchecked  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);  }  mViewModelStore.put(key, viewModel);  //noinspection unchecked  return (T) viewModel;  } Copy the code

That’s why the ViewModel can save data when the screen is rotated, right

Why data in fragments can be saved after screen rotation?

If we call the following code in the Fragment:

    val model: MyViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
Copy the code

Tracing their calls, you can see that the fetch ViewModel is stored through mNonConfig

 @NonNull
    ViewModelStore getViewModelStore(@NonNull Fragment f) {
        return mNonConfig.getViewModelStore(f);
    }
Copy the code

So when was mNonConfig created? Where is it stored?

void attachController(@NonNull FragmentHostCallback<? > host,            @NonNull FragmentContainer container, @Nullable final Fragment parent) {
// omit more...        if(parent ! = null) {            mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
 } else if (host instanceof ViewModelStoreOwner) { / / 👇 go here ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();  mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);  } else {  mNonConfig = new FragmentManagerViewModel(false);  }  } Copy the code

This is called when adding the Fragment to the FragmentManager because parent = null is passed in, and the Activity implements the ViewModelStoreOwner interface by default, So we get the ViewModelStore in the Activity, and then call FragmentManagerViewModel’s getInstance() method:

    static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
        ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
                FACTORY);
        return viewModelProvider.get(FragmentManagerViewModel.class);
    }
Copy the code

In this method, the FragmentManagerViewModel is created and added to the ViewModelStore in the Activity.

The overall process is as follows: Viewmodels in fragments will not be destroyed due to configuration changes

Viewmodels in fragments are not destroyed by configuration changes because the declared ViewModel is stored in the FragmentManagerViewModel. The FragmentManagerViewModel is stored in the ViewModelStore of the host Activity, and the ViewModelStore of the Activity is not destroyed due to configuration changes. Therefore, the Fragment ViewModel will not be destroyed due to configuration changes.

Principle that viewModels can be shared in fragments

Another feature of the ViewModel is the ability to share data in fragments.

If we want Fragment D to retrieve the data in Fragment A, we can only add the ViewModel in the ViewModelStore of the Activity. Only in this way can we get the same data in different fragments. This is why when using a shared ViewModel in a Fragment, we need to pass in getActivity() when we call ViewModelProvider.of() to create the ViewModel.

Examples are as follows:

    public class SharedViewModel extends ViewModel {
        private final MutableLiveData<Item> selected = new MutableLiveData<Item>();

        public void select(Item item) {
            selected.setValue(item);
 }   public LiveData<Item> getSelected() {  return selected;  }  }   public class FragmentA extends Fragment {  private SharedViewModel model;  public void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState); //👇 passes in the host Activity model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);  itemSelector.setOnClickListener(item -> {  model.select(item);  });  }  }   public class FragmentD extends Fragment {  public void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState); //👇 passes in the host Activity SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);  model.getSelected().observe(this, { item ->  // Update the UI.  });  }  } Copy the code

The resources

Knowledge | ViewModel ViewModel four integration way you know all this knowledge?