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:
- Pass
this
intoof()
to get theViewModelProvider
tied to the fragment, or - Pass
getActivity()
intoof()
to get theViewModelProvider
tied to the activity
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:
- Use lifecycle-aware objects for that (e.g.,
LiveData
), or - Override
onCleared()
and clean up the objects at that point
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.