background

LiveData is an Android Jetpack component that implements Lifecycle consistency with the help of Lifecycle management. Notifications of data changes are controlled to occur when the Lifecycle is active, when STARTED, when (note that this is State in Lifecycle), and that notification becomes the only trusted source of data, the only entry point for the view to get data. LiveData is often used in conjunction with viewModels.

define

The ViewModel class is designed to store and manage data related to an interface in a life-cycle manner. The ViewModel class allows data to persist after configuration changes such as screen rotation.

In my opinion, the ViewModel class allows data to survive configuration changes on the screen. For example, the ViewModel retains the original data after the interface is recreated due to configuration changes. This is of course important, but an equally important function is to share data between fragments. Finally, memory leaks are avoided due to its management style (one-way dependency, only activities/fragments hold viewModels). And the ViewModel works with the Kotlin coroutine, replacing the loader with the ViewModel, and that’s where the ViewModel comes in.

The basic use

Import dependence
/ / LiveData & ViewModel because both are usually used together implementation "androidx. Lifecycle: lifecycle - LiveData - KTX: 2.2.0" implementation "Androidx. Lifecycle: lifecycle - viewmodel - KTX: 2.2.0." "Copy the code
Simple to use

The Android Jetpack architecture component provides the ViewModel helper class for the interface controller, which is responsible for preparing data for the interface. ViewModel objects are automatically retained during configuration changes so that the data they store is immediately available for use by the next Activity/Fragment instance. Here’s an example:

The ViewModel code is as follows:

class AccountViewModel: ViewModel() {

    private val _userAgeLiveData: MutableLiveData<String> = MutableLiveData()
    val userAgeLiveData: LiveData<String>
        get() = _userAgeLiveData

    fun loadUserName(userId: String){
        val accountRepository = AccountRepository()
				Log.i("ViewModel====="."loadUserName: ")
        viewModelScope.launch {
            //suspend method, 2s after the user information
            val result = accountRepository.requestUserInfo(userId)
            / / assignment
            _userAgeLiveData.value = result
        }
    }
  
      override fun onCleared(a) {
        super.onCleared()
        Log.i("ViewModel====="."onCleared: ")}}Copy the code

The Activity code is as follows:

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        txtUserName = findViewById(R.id.txtUserName)
        Log.i(TAG, "onCreate: ")
        val accountViewModel = ViewModelProvider(this).get(AccountViewModel::class.java)

        findViewById<View>(R.id.btnGetUserInfo).setOnClickListener {
            accountViewModel.loadUserName("jackie")
        }
        
        accountViewModel.userAgeLiveData.observe(this, Observer {
            // Notify the UI, and the UI does the operation
            Log.i(TAG, "onCreate: ========observe change=========")
            txtUserName.text = it
        })
    }
Copy the code

After clicking the button to get user information, the loadUserName method in ViewModel is executed

jetpackdemo I/ViewModel=====: loadUserName: 
jetpackdemo I/AccountActivity=====: onCreate: ========observe change=========
Copy the code

After the configuration changes (after the screen is rotated), the following logs are displayed:

jetpackdemo I/AccountActivity=====: onPause: 
jetpackdemo I/AccountActivity=====: onDestroy: 
jetpackdemo I/AccountActivity=====: onCreate: 
jetpackdemo I/AccountActivity=====: onStart: 
jetpackdemo I/AccountActivity=====: onCreate: ========observe change=========
Copy the code

As you can see, the Activity is re-created. Instead of clicking the button to execute the loadUserName method, we call back the Observer onChange method. And the ViewModel onClear method is not executed, indicating that the ViewModel is not destroying the rebuild.

Pay attention to

The ViewModel is created using

val accountViewModel = ViewModelProvider(this).get(AccountViewModel::class.java)
Copy the code

Rather than

val accountViewModel = AccountFactory().create(AccountViewModel::class.java)

class AccountFactory: ViewModelProvider.Factory {
    override fun 
        create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(AccountViewModel::class.java)){
            return AccountViewModel() as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")}}Copy the code
Fragment Data sharing between fragments

It is a common requirement for two or more fragments in an Activity to communicate with each other. Suppose you have a Fragment in which the user selects an item from a list, and another Fragment that displays the contents of the selected item. This situation is not easy to handle because both fragments need to define some kind of interface description, and the owner Activity must bind the two together. In addition, both fragments must deal with situations where the other Fragment has not yet been created or is not visible.

You can use ViewModel objects to solve this common difficulty. Both fragments can handle such communication using their Activity scope shared ViewModel as follows:

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class MasterFragment : Fragment() {

    private lateinit var itemSelector: Selector

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)
        itemSelector.setOnClickListener { item ->
            // Update the UI}}}class DetailFragment : Fragment() {

    // Use the 'by activityViewModels()' Kotlin property delegate
    // from the fragment-ktx artifact
    private val model: SharedViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)
        model.selected.observe(viewLifecycleOwner, Observer<Item> { item ->
            // Update the UI}}})Copy the code

Both fragments retrieve the activities that contain them. This way, when both fragments retrieve the ViewModelProvider, they receive the same SharedViewModel instance (whose scope is limited to that Activity).

This approach has the following advantages:

  • The Activity does not need to perform any action, nor does it need to know anything about this communication.
  • In addition toSharedViewModelFragments do not need to know each other outside of convention. If one Fragment disappears, the other Fragment continues to work as usual.
  • Each Fragment has its own life cycle and is not affected by the life cycle of the other Fragment. If one Fragment replaces another, the interface will continue to work without any problems.

ViewModel lifecycle

The ViewModel object exists for a time Lifecycle that is passed to the VIewModelProvider when the ViewModel is acquired. This is the code we called above:

val accountViewModel = ViewModelProvider(this).get(AccountViewModel::class.java)
Copy the code

Here this is the Activity instance object because our Activity implements the association with Lifecycle. The ViewModel will remain in memory until Lifecycle is gone permanently: for an Activity, when the Activity completes; For fragments, this is when the Fragment is separating.

Below are the various life cycle states of the Activity as the screen rotates and then ends. The diagram also shows the ViewModel life cycle next to the associated Activity life cycle. These basic states also apply to the Fragment lifecycle.

The Activity object is usually called for the first time on the systemonCreate()When the requestViewModel.ViewModelThe time range that exists is from the first requestViewModelUntil the Activity completes and is destroyed.

Source code analysis

Before analyzing the source code, I ask two questions: how does the ViewModel keep the data alive after configuration changes such as screen rotation (i.e., the ViewModel instance still exists)? How do fragments share data through the ViewModel?

Let’s start with the ViewModel class

public abstract class ViewModel {...private volatile boolean mCleared = false;

    /** * This method will be called when this ViewModel is no longer used and will be destroyed. * 

* It is useful when ViewModel observes some data and you need to clear this subscription to * prevent a leak of this ViewModel. */

// This method will be called when the ViewModel is cleared. You can do some deregistration in this method to prevent memory leaks @SuppressWarnings("WeakerAccess") protected void onCleared() { } @MainThread final void clear() { mCleared = true; ... the onCleared (); }...}Copy the code

The ViewModel is just an abstract class. The clear() method is called when the ViewModel is cleared. Users can override the onCleared() method to handle additional actions.

The analysis starts at the invocation

val accountViewModel = ViewModelProvider(this).get(AccountViewModel::class.java)
Copy the code

Let’s look at the ViewModelProvider class again:

	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

And the constructors that you end up calling are all third constructors, so let’s see what ViewModelStore and Factory are, Because we passed is the Activity (is ViewModelStoreOwner and implementer HasDefaultViewModelProviderFactory), so will be the owner. The getViewModelStore (), The second parameter is the getDefaultViewModelProviderFactory ().

ViewModelStoreOwner can be understood as the owner of the ViewModelStore, which means that our Activity/Fragment is the owner of the ViewModel store.

And then let’s see what is the ViewModelStore?

/ / store the ViewModel
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(a) {
        return new HashSet<>(mMap.keySet());
    }

    /** * Clears internal storage and notifies ViewModels that they are no longer used. */
    public final void clear(a) {
        for(ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); }}Copy the code

As you can see, the ViewModelStore code is very simple. It uses a HashMap to store the key(String) and value(ViewModel), where the key name is

// The name of key
DEFAULT_KEY + ":" + canonicalName

private static final String DEFAULT_KEY =
            "androidx.lifecycle.ViewModelProvider.DefaultKey";
String canonicalName = modelClass.getCanonicalName();            
Copy the code

Because the second parameter is through getDefaultViewModelProviderFactory () to get to, said before the Activity is HasDefaultViewModelProviderFactory implementation class, We take a look at the Activity of getDefaultViewModelProviderFactory () method

    @NonNull
    @Override
    public ViewModelProvider.Factory getDefaultViewModelProviderFactory(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 (mDefaultFactory == null) {
            mDefaultFactory = new SavedStateViewModelFactory(
                    getApplication(),
                    this, getIntent() ! =null ? getIntent().getExtras() : null);
        }
        return mDefaultFactory;
    }
Copy the code

Can see through a SavedStateViewModelFactory to obtain ViewModelProvider. Factory, name is also very clear, is to save the state of the ViewModel Factory.

Let’s take a look at the get method (AccountViewModel: : class. Java)

    @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);  / / the name of the key
    }

    @SuppressWarnings("unchecked")
    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key); // Get the viewModel instance from hashMap

        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.}}// Create with Factory
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
        } else {
            viewModel = (mFactory).create(modelClass); 
        }
      	/ / in the viewModelStore
        mViewModelStore.put(key, viewModel);
        return (T) viewModel;
    }
Copy the code

In simple terms, we get the ViewModel from mViewModelStore, and if we don’t get it, we create it using Factory and store it in mViewModelStore.

This logic is clear, ViewModelProvider (this). The get (AccountViewModel: : class. Java) will store the ViewModel in the ViewModelStore.

Because the Activity implements the ViewModelStoreOwner interface, it can be understood as the owner of the ViewModelStore, That is, our Activity/Fragment is the owner of the ViewModel store.

public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore(a);
}
Copy the code

Then let’s look at the implementation of this method in an Activity

    @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) {
          	/ / getLastNonConfigurationInstance can get it
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if(nc ! =null) {
              	/ / viewmodelstore recovery
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;  
            }
          	// If not, create a new one
            if (mViewModelStore == null) {
                mViewModelStore = newViewModelStore(); }}return mViewModelStore;
    }
Copy the code

Can see from this method, the Activity will eventually create a ViewModelStore (ViewModelStoreOwner) internal, used to store the ViewModel, next we see getLastNonConfigurationInstance method

//ComponentActivity.java
NonConfigurationInstances mLastNonConfigurationInstances;
@Nullable
    public Object getLastNonConfigurationInstance() {
        returnmLastNonConfigurationInstances ! =null
                ? mLastNonConfigurationInstances.activity : null;
    }

    static final class NonConfigurationInstances {
        Object custom;
        ViewModelStore viewModelStore;
    }
Copy the code

Why use getLastNonConfigurationInstance method, now let’s look at the Activity state save and restore:

onSaveInstanceStateandOnRetainNonConfigurationInstance usage scenarios

As we know, methods to save state and restore state are performed as the screen rotates

	@Override
	protected void onSaveInstanceState(Bundle outBundle) {  / / save
		super.onSaveInstanceState(outBundle);
	}
	
	@Override 
	protected void onRestoreInstanceState(Bundle savedInstanceState) { / / recovery
		super.onRestoreInstanceState(savedInstanceState);
	}


// Note that if you inherit AppcompactActiviy, the method is already defined as final in its parent ComponentActivity and subclasses cannot be overridden
// Inherit the Activity
public Object onRetainNonConfigurationInstance(a) {
    // TODO Auto-generated method stub
    // Set the content to be saved in the bundle. Instead of the bundle, we can use object instead.
  return super.onRetainNonConfigurationInstance();
} 

@Nullable
@Override
public Object getLastNonConfigurationInstance(a) {
  return super.getLastNonConfigurationInstance();
}
Copy the code

In Android 10, their execution sequence between onStop and onDestory, and onSaveInstanceState before onRetainNonConfigurationInstance execution, generally we saved data is not too big, Use onSaveInstanceState if the data to be saved is not suitable to be stored in the Bundle (for example: A socket) or data is bigger (such as a Bitmap), so this time we should use onRetainNonConfigurationInstance (), And we use the onRetainNonConfigurationInstance () can store any type of object, like AsyncTask and SQLiteDatabse, we can all be saved. These types of data may be reused by a new Activity.

That is to say, the Bundle can only put a few specific types, such as the basic data types, arrays, Serialable Object, but as long as is a onRetainNonConfigurationInstance Object.

At the same time when an activity is becoming “easy” system is destroyed, the activity of onSaveInstanceState will be performed, but even more onRetainNonConfigurationInstance operation when time is in the configuration change, The onSaveInstanceState data is serialized and saved to disk. There are memory and onRetainNonConfigurationInstance saved data.

So here I’m sure you’ll find our ViewModel in onRetainNonConfigurationInstance method, take a look at

   //ComponentActivity.java
   @Override
    @Nullable
    public final Object onRetainNonConfigurationInstance(a) {
        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;
        }
				/ / new NonConfigurationInstances, to save viewModelStore
        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.custom = custom;
        nci.viewModelStore = viewModelStore; 
        return nci;
    }
Copy the code

Can see in this method saved viewModelStore, preserved in the NonConfigurationInstances. And the getLastNonConfigurationInstance truly is in the Activity. The Java classes

    //Activity.java
     // Static inner class
     static final class NonConfigurationInstances {
        Object activity;
        HashMap<String, Object> children;
        FragmentManagerNonConfig fragments;
        ArrayMap<String, LoaderManager> loaders;
        VoiceInteractor voiceInteractor;
    }
    NonConfigurationInstances mLastNonConfigurationInstances;
    @Nullable
    public Object getLastNonConfigurationInstance(a) {
        returnmLastNonConfigurationInstances ! =null
                ? mLastNonConfigurationInstances.activity : null;
    }
Copy the code

While mLastNonConfigurationInstances assignment is in the attach methods mLastNonConfigurationInstances = lastNonConfigurationInstances; .

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, IBinder assistToken) { attachBaseContext(context); ... mLastNonConfigurationInstances = lastNonConfigurationInstances; ...Copy the code
Why does the ViewModel persist after configuration changes?

And attach the incoming parameters lastNonConfigurationInstances ActivityClientRecord is a variable, The ActivityClientRecord is stored in the mActivities(ArrayMap) of the ActivityThread. The ActivityClientRecord is not affected by activity reconstruction. So the lastNonConfigurationInstances ActivityThread also is not affected, so the NonConfigurationInstances ComponentActiviy also had no effect, So ViewModelStore unaffected, and ultimately the ViewModel call getLastNonConfigurationInstance get get after the Activity to create, and this is the reason for the existence of the ViewModel have been.

Earlier versions of viewModels were saved using HolderFragment, SetRetainInstance (Boolean) in Fragment: setRetainInstance(Boolean) in Fragment: setRetainInstance(Boolean) in Fragment: setRetainInstance(Boolean) in Fragment: setRetainInstance(Boolean) in Fragment: setRetainInstance(Boolean) in Fragment: When set to true, the current Fragment can be stored when the Activity is rebuilt. This will allow you to save the ViewModel. See this article for more details.

How do fragments share data through the ViewModel?

From the above analysis, we know that the ViewModel exists in the ViewModelStore of the Activity. Multiple fragments depend on the same Activity. In this case, obtaining the same ViewModel is not a problem.

Why are all LiveData notifications re-executed after rotation

The reason is simple, because LiveData events are sticky events, which means that when the Activity is destroyed, since LiveData in the ViewModel is not destroyed (with a specific value), after the Activity is recreated, LiveData sends this value to the current Activity interface to restore the state of the Activity interface.

How does the ViewModel avoid memory leaks

As you can see in the constructor for ComponentActivity, getLifecycle().addobServer adds an observer to see if the interface is destroyed, and once destroyed, empties all viewModels in the ViewModelStore.

    public ComponentActivity() {
        Lifecycle lifecycle = getLifecycle();
        //noinspection ConstantConditions
        if (lifecycle == null) {
            throw new IllegalStateException("getLifecycle() returned null in ComponentActivity's "
                    + "constructor. Please make sure you are lazily constructing your Lifecycle "
                    + "in the first call to getLifecycle() rather than relying on field "
                    + "initialization.");
        }
        if (Build.VERSION.SDK_INT >= 19) {
            getLifecycle().addObserver(new LifecycleEventObserver() {
                @Override
                public void onStateChanged(@NonNull LifecycleOwner source,
                        @NonNull Lifecycle.Event event) {
                    if (event == Lifecycle.Event.ON_STOP) {
                        Window window = getWindow();
                        finalView decor = window ! =null ? window.peekDecorView() : null;
                        if(decor ! =null) { decor.cancelPendingInputEvents(); }}}}); } 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();// The interface performs onDestroy to clear the ViewModel}}}});if (19 <= SDK_INT && SDK_INT <= 23) {
            getLifecycle().addObserver(new ImmLeaksCleaner(this)); }}Copy the code

Viewmodels are used with coroutines

The ViewModel supports coroutines. The ViewMdoelScope defines the ViewMdoelScope for each ViewModel in the application. If the ViewModel is cleared, coroutines started in this scope are automatically cancelled. Coroutines are useful if you have work that needs to be done only when the ViewModel is active. For example, if you want to calculate some data for your layout, you should limit your work to the ViewModel so that after the ViewModel is cleared, the system automatically cancels work to avoid consuming resources.

viewModelScope.launch {
            // Coroutine that will be canceled when the ViewModel is cleared.
        }
Copy the code

Together, these architectural components of Android Jetpack are powerful enough to save developers a lot of time, while also making it easy to avoid memory leaks.

conclusion

The ViewModel is a very useful Android Jetpack component that survives configuration changes such as screen rotation, can be shared between fragments, is very easy to use with coroutines, and can easily avoid memory leaks.

Note that many developers implement the ViewModel LifecycleObserver interface as a lifecycle sensitive component, performing operations like loadData in lifecycle aware methods, Then through LiveData to notify the UI to make corresponding changes, such a way of losing the original intention of the official design of the ViewModel for us, but a bit of nonconforming. And ViewModel with coroutine is also very convenient, there are many people will also put the network request in ViewModel to call, personal feeling is also very inappropriate, can refer to the official Demo design.

My other articles

【Android Jetpack】LiveData from the beginning to master

【Android Jetpack】Lifecycle goes from beginning to mastery

Android Bitmaps load efficiently, those little things you need to know

Android screen adaptation, those little things you need to know

The past and present of the Android lightweight storage solution

Performance optimization: Why use SparseArray and ArrayMap instead of HashMap?

Have you mastered the basic, intermediate, and advanced methods of Activity?