Android builds MVVM architecture from scratch: ——ViewModel

Keywords: Fragment Google Android network

The ViewModel class is designed to store and manage UI-related data in a perceptible lifecycle manner, and the data in the ViewModel will survive even if the activity configuration changes.

What are the advantages of ViewModel?

1. Data persistence

Before the destruction and reconstruction of activity, we can use the onSave Instance State () mechanism of activity to save and restore data, but the disadvantage is obvious. onSave Instance State is only suitable for storing a small amount of data that can be serialized and deserialized. If we need to save a larger bitmap list, this mechanism is obviously inappropriate. ViewModel can solve this problem.


The ViewModel lifecycle runs through the entire activity lifecycle, including the recreation of the activity caused by rotation, until the activity is truly destroyed.

2. Asynchronous callback problem

app requires frequent asynchronous requests for data, such as interface calls for server data. Of course, callbacks to these requests are quite time-consuming, and we received them in Activity or fragment before. So you have to consider potential memory leaks, such as interface requests returned after Activity was destroyed. Dealing with these problems will add a lot of complicated work to us.
But now we can solve this problem by using ViewModel to process data callbacks.

3. Sharing UI controller burden

From the earliest MVC to the current popular MVP and MVVM, the purpose is nothing more than to clarify responsibilities and separate the burden of UI controller s.
UI controller s, such as Activity and Fragment, are designed to render and display data, respond to user behavior, and process certain interactions of the system. If asked to load network or database data, it would be bloated and difficult to manage. So in order to be concise, refreshing and silky, we can separate the responsibility of data manipulation from ViewModel.

4. Sharing data between Fragments

For example, there are multiple fragments in an activity that need to interact with each other. My previous approach was interface callbacks, which needed to be managed uniformly in Activity, and unavoidable fragments had to hold each other's references.

So what about using ViewModel (official website example):
(activity and its internal fragment s can share a ViewModel)

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 MasterFragment extends Fragment {
    private SharedViewModel model;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}

public class DetailFragment extends Fragment {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
        model.getSelected().observe(this, { item ->
           // Update the UI.
        });
    }
}

The benefits are as follows:
1. Activity doesn't need to do anything. It doesn't even know the interaction. It decouples perfectly.
2. Fragment s only need to interact with ViewModel, without knowing the status or even existence of fragments, let alone holding their references. All fragments are destroyed without affecting their own work.
3. fragment life cycle does not affect each other, even if fragments are replaced by other fragments, it does not affect the operation of the system.

Usage Brief

ViewModel is generally used in conjunction with LiveData
1. Get an instance of ViewModel by providing the class ViewModel Providers:

MyViewModel model = ViewModelProviders.of(activity).get(MyViewModel.class);
//or
 MyViewModel model = ViewModelProviders.of(fragment).get(MyViewModel.class);

Or with Factory

MyViewModel model = ViewModelProviders.of(activity,factory).get(MyViewModel.class);

VM internal operation:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

Then, data changes can be observed in activity:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

Principle of Source Code Analysis

ViewModel is usually initialized in onCreate.

1. Instantiated code

ViewModelProviders.of(activity,factory).get(MyViewModel.class)

1) First of all, the method of ViewModel Providers:

 @MainThread
    public static ViewModelProvider of(@NonNull FragmentActivity activity) {
        initializeFactoryIfNeeded(checkApplication(activity));
        return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
    }

    @MainThread
    public static ViewModelProvider of(@NonNull FragmentActivity activity,
            @NonNull Factory factory) {
        checkApplication(activity);
        return new ViewModelProvider(ViewModelStores.of(activity), factory);
    }

I just paste the active parameter with activity and fragment. Focusing on this, I introduce a Factory. The method without Factory initializes an sDefaultFactory (the implementation class of Factory) by initializeFactoryIfNeeded.

 /**
     * Implementations of {@code Factory} interface are responsible to instantiate ViewModels.
     */
    public interface Factory {
        /**
         * Creates a new instance of the given {@code Class}.
         * <p>
         *
         * @param modelClass a {@code Class} whose instance is requested
         * @param <T>        The type parameter for the ViewModel.
         * @return a newly created ViewModel
         */
        @NonNull
        <T extends ViewModel> T create(@NonNull Class<T> modelClass);
    }

There's only one create method, and if you think about it with your toes, you know it must be used to initialize the viewmodel. Put it in first and use it later. Continue to look at the of method:

return  new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory)

Two new classes, ViewModel Provider and ViewModel Stores, have emerged. First, look at ViewModel Provider:

public class ViewModelProvider {

    private static final String DEFAULT_KEY =
            "android.arch.lifecycle.ViewModelProvider.DefaultKey";
 
    private final Factory mFactory;
    private final ViewModelStore mViewModelStore;
    ……
    @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.
            }
        }

        viewModel = mFactory.create(modelClass);
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }

}

This class is simple enough to maintain an mFactory, an emerging class ViewModelStore, and to provide a get method for generating ViewModels with mFactory and ViewModelStore. Wow, you've seen where ViewModel is finally instantiated, but don't worry, there's a lot more to it.

Look again.

ViewModelStores.of(activity)
/**
 * Factory methods for {@link ViewModelStore} class.
 */
@SuppressWarnings("WeakerAccess")
public class ViewModelStores {

    private ViewModelStores() {
    }

    /**
     * Returns the {@link ViewModelStore} of the given activity.
     *
     * @param activity an activity whose {@code ViewModelStore} is requested
     * @return a {@code ViewModelStore}
     */
    @MainThread
    public static ViewModelStore of(@NonNull FragmentActivity activity) {
        return holderFragmentFor(activity).getViewModelStore();
    }

    /**
     * Returns the {@link ViewModelStore} of the given fragment.
     *
     * @param fragment a fragment whose {@code ViewModelStore} is requested
     * @return a {@code ViewModelStore}
     */
    @MainThread
    public static ViewModelStore of(@NonNull Fragment fragment) {
        return holderFragmentFor(fragment).getViewModelStore();
    }
}

There are only two of the methods, the static method initialized by holderFragmentFor for for HolderFragment

public class HolderFragment extends Fragment {
……
    private ViewModelStore mViewModelStore = new ViewModelStore();
    public ViewModelStore getViewModelStore() {
        return mViewModelStore;
    }
……
}

Nothing to say. Look at ViewModel Store:

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.get(key);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
        mMap.put(key, viewModel);
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

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

Obviously, a class is used to store ViewModel instances, and a HashMap is maintained internally to store ViewModel.
The get, put and clear methods are also provided.
What has ViewModel Providers of done so far?
1. Initialize the ViewModel Provider to maintain the Factory for creating VM and the ViewModel Store for storing VM.
2. Initialize the Factory used to generate ViewModel (DefaultFactory by default);
3. Holder Fragment is instantiated through the static method of ViewModel Stores, and ViewModel Store is instantiated.

2. Then the get method of ViewModel Provider:

ViewModelProviders.of(activity,factory).get(MyViewModel.class);

    @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.
            }
        }

        viewModel = mFactory.create(modelClass);
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }

The logic is not complicated. First, see if the ViewModel Store has been saved. If not, instantiate it through the factory and store it in the ViewModel Store.

Death of ViewModel:

public abstract class ViewModel {
    /**
     * This method will be called when this ViewModel is no longer used and will be destroyed.
     * <p>
     * It is useful when ViewModel observes some data and you need to clear this subscription to
     * prevent a leak of this ViewModel.
     */
    @SuppressWarnings("WeakerAccess")
    protected void onCleared() {
    }
}

Vm has only one onCleared method, so when did it call it?
Here's a brief introduction to the Older Fragment mentioned earlier:

public class HolderFragment extends Fragment {
    private static final String LOG_TAG = "ViewModelStores";

    private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();

    /**
     * @hide
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public static final String HOLDER_TAG =
            "android.arch.lifecycle.state.StateProviderHolderFragment";

    private ViewModelStore mViewModelStore = new ViewModelStore();

    public HolderFragment() {
        setRetainInstance(true);
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        sHolderFragmentManager.holderFragmentCreated(this);
    }

   @Override
    public void onDestroy() {
        super.onDestroy();
        mViewModelStore.clear();
    }

    public ViewModelStore getViewModelStore() {
        return mViewModelStore;
    }

    static class HolderFragmentManager {
        private Map<Activity, HolderFragment> mNotCommittedActivityHolders = new HashMap<>();
        private Map<Fragment, HolderFragment> mNotCommittedFragmentHolders = new HashMap<>();
        ……
         private static HolderFragment createHolderFragment(FragmentManager fragmentManager) {
            HolderFragment holder = new HolderFragment();
            fragmentManager.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss();
            return holder;
        }
         HolderFragment holderFragmentFor(FragmentActivity activity) {
            FragmentManager fm = activity.getSupportFragmentManager();
            HolderFragment holder = findHolderFragment(fm);
            if (holder != null) {
                return holder;
            }
            holder = mNotCommittedActivityHolders.get(activity);
            if (holder != null) {
                return holder;
            }

            if (!mActivityCallbacksIsAdded) {
                mActivityCallbacksIsAdded = true;
                activity.getApplication().registerActivityLifecycleCallbacks(mActivityCallbacks);
            }
            holder = createHolderFragment(fm);
            mNotCommittedActivityHolders.put(activity, holder);
            return holder;
        }
  ……
}

Vm was created with the mention of instantiating a Holder Fragment. And when it is instantiated, its fragmentManager.beginTransaction().add(holder, HOLDER_TAG).commitAllowingStateLoss() is implemented by the above createHolder Fragment method.
We know that after commit fragment s will have souls and gain life cycles. Look again at its onDestroy method
Called mViewModelStore.clear();

 public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }

Google took full advantage of the fragment lifecycle features to enable Vm to complete onCleared. Google's lifecycle and ViewModel all use fragment features to play with these lifecycles.

So why doesn't the horizontal and vertical screen switch ViewModel onCleared?
Look at the way Holder Fragment is constructed with a set Retain Instance (true);

Be careful

Because the ViewModel life cycle may be long and the activity life cycle, Google prohibits holding a Context or an activity or view reference in the ViewModel in order to avoid memory leaks. If you have to use Context, you can inherit from the AndroidViewModel class to get ApplicationContext

public class AndroidViewModel extends ViewModel {
    @SuppressLint("StaticFieldLeak")
    private Application mApplication;

    public AndroidViewModel(@NonNull Application application) {
        mApplication = application;
    }

    /**
     * Return the application.
     */
    @NonNull
    public <T extends Application> T getApplication() {
        //noinspection unchecked
        return (T) mApplication;
    }
}

Posted by delfa on Tue, 19 Mar 2019 20:33:27 -0700