ViewModel In Action

So, let’s take a look at the Trips/ViewModels sample project. This adds a ViewModel to our app showing a roster of upcoming trips. More specifically, we will use ViewModelProvider, the way Google envisioned it.

Earlier editions of this sample used Android’s native Activity and Fragment classes. Those do not work with ViewModelProviders. So, in this sample, MainActivity has been revised to extend from FragmentActivity and RecyclerViewFragment has been revised to extend from the Support Library edition of Fragment.

Defining a ViewModel

The idea is that a ViewModel should hold the data necessary to render the UI. In our case, that is simply a roster of Trip objects, pulled in from Room.

For ViewModelProvider to work, the class must be public, even though your IDE might suggest otherwise. So, our TripRosterViewModel is public:

package com.commonsware.android.room;

import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import java.util.List;

public class TripRosterViewModel extends AndroidViewModel {
  final LiveData<List<Trip>> allTrips;

  public TripRosterViewModel(Application app) {
    super(app);

    allTrips=TripDatabase.get(app).tripStore().selectAllTrips();
  }
}

Note that TripRosterViewModel extends from AndroidViewModel. AndroidViewModel itself extends ViewModel. The only difference between the two is the constructor: ViewModel has a zero-argument constructor, while AndroidViewModel has a one-argument constructor, supplying the Application instance. In our case, we need the Application instance to get() our TripDatabase (as Room needs a Context for this).

TripRosterViewModel, in its constructor, sets up an allTrips field that is a LiveData of our roster of Trip objects. Since this is LiveData, the actual work will not be done until we ask it to, by registering an observer to use the results.

Getting a ViewModel

Our TripsFragment needs access to the TripRosterViewModel, in order to be able to get to the allTrips data and request the roster of Trip objects.

However, now we have a decision to make: is the TripRosterViewModel tied to the fragment or to the activity?

Since a fragment can get to its hosting activity via getActivity(), a fragment can choose either scope:

Either is perfectly legitimate. Frequently, it will boil down to who needs the data. Data that is only needed by a single fragment should be owned by a ViewModel tied to that fragment. Data needed by multiple fragments, or by a fragment and the activity, or just by the activity, should be owned by a ViewModel tied to the activity. A fragment can also elect to do both, using two ViewModel instances, one for its own data and one that it gets via the activity.

In this case, the only UI is the TripsFragment, so we can say that the TripRosterViewModel is owned by the fragment and retrieve it as part of our onViewCreated() work:

    TripRosterViewModel vm=
      ViewModelProviders.of(this).get(TripRosterViewModel.class);

The first time we run through these lines, we will get a fresh TripRosterViewModel instance. If we undergo a configuration change, when this fragment is recreated, the new fragment instance will get the same TripRosterViewModel as before.

Using the ViewModel

Given our TripRosterViewModel, our TripsFragment can now get at the roster of Trip objects, by registering an Observer (via a lambda expression):

    vm.allTrips.observe(this, trips -> {
      setAdapter(new TripsAdapter(trips, getActivity().getLayoutInflater()));

      if (trips==null || trips.size()==0) {
        final TripStore store=TripDatabase.get(getActivity()).tripStore();

        new Thread() {
          @Override
          public void run() {
            store.insert(new Trip("Vacation!", 10080, Priority.MEDIUM, new Date()),
              new Trip("Business Trip", 4320, Priority.OMG, new Date()));
          }
        }.start();
      }
    });

A typical app would just have the setAdapter() call, to pass the Trip roster over to the TripsAdapter, to show the roster in the RecyclerView. In this case, we want to lazy-create some trips, as otherwise we will have no data. So, if we have no trips, we insert some in a background thread.

However, there are two issues with that approach. One is the possible race condition, where the user rotates the screen while the background thread is going on, and so we fork a second thread. Since this code is not the sort of thing you would do in a production app, what we have here will suffice for now.

But, if you run the app, you will see that our data shows up in the RecyclerView, even after a fresh run of the app, when we did not have any data. Yet, our Thread is not doing anything to refresh the UI. So, the second issue is: how is this working?

The answer is that Room is monitoring our DAO for changes and is automatically updating the LiveData to reflect those changes, as was mentioned in the chapter on LiveData.

Getting Rid of the ViewModel

Ideally, you should not have to do anything to explicitly “get rid of” a ViewModel. If you are using LiveData, it is lifecycle-aware, and so it should clean up itself when the activity or fragment is destroyed. If you have anything else in the ViewModel that needs cleanup when the activity or fragment is destroyed, either:

When the ViewModel will no longer be used, the ViewModel will be called with onCleared(). This is an opportunity for you to release anything that needs to be released and will not just go away as part of normal garbage collection or lifecycle cleanup. So, for example, if you are holding an RxJava Disposable in a ViewModel, onCleared() is a good place to dispose() of it.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.