Lifecycle Aware Data Loading with Architecture Components

In my previous blog post, I talked about how you can use Loaders to load data in a way that automatically handles configuration changes.

With the introduction of Architecture Components, there’s an alternative that provides a modern, flexible, and testable solution to this use case.

Separation of concerns

Two of the largest benefits of Loaders were:

  • They encapsulate the process of data loading
  • They survive configuration changes, preventing unnecessarily reloading data

With Architecture Components, these two benefits are now handled by two separate classes:

  • LiveData provides a lifecycle aware base class for encapsulating loading data
  • ViewModels are automatically retained across configuration changes

One significant advantage of this separation is that you can reuse the same LiveData in multiple ViewModels, compose multiple LiveData sources together through a MediatorLiveData, or use them in a Service, avoiding the effort of trying to munge a Loader into a scenario where you don’t have a LoaderManager.

While Loaders espoused a separation between your UI and data loading (one of the first steps to a testable app!), this model expands on that advantage — your ViewModel can be completely tested by mocking out your data sources and the LiveData can be tested in complete isolation. A clean, testable architecture was a large focus in the Guide to App Architecture.

Keep it simple

That all sounds good in theory. An illustrative example recreating our AsyncTaskLoader might help make the ideas a bit more concrete:

public class JsonViewModel extends AndroidViewModel {
// You probably have something more complicated
// than just a String. Roll with me
private final MutableLiveData<List<String>> data =
new MutableLiveData<List<String>>();
  public JsonViewModel(Application application) {
super(application);
loadData();
}
  public LiveData<List<String>> getData() {
return data;
}
  private void loadData() {
new AsyncTask<Void,Void,List<String>>() {
@Override
protected List<String> doInBackground(Void… voids) {
File jsonFile = new File(getApplication().getFilesDir(),
"downloaded.json");
List<String> data = new ArrayList<>();
// Parse the JSON using the library of your choice
return data;
}
      @Override
protected void onPostExecute(List<String> data) {
this.data.setValue(data);
}
}.execute();
}
}

Wait, an AsyncTask? How is this safe? There’s two safety features in play here:

  1. The AndroidViewModel (a subclass of ViewModel) only has a reference to the application Context, so we’re very importantly not referencing the Context of an Activity, etc. that could present a leak — there’s even a Lint check to avoid these kind of issues.
  2. LiveData only delivers the results if there’s something observing it

But we haven’t quite captured the essence of the Architecture Components: our ViewModel is directly building and managing our LiveData.

public class JsonViewModel extends AndroidViewModel {
private final JsonLiveData data;
  public JsonViewModel(Application application) {
super(application);
data = new JsonLiveData(application);
}
  public LiveData<List<String>> getData() {
return data;
}
}
public class JsonLiveData extends LiveData<List<String>> {
private final Context context;
  public JsonLiveData(Context context) {
this.context = context;
loadData();
}
  private void loadData() {
new AsyncTask<Void,Void,List<String>>() {
@Override
protected List<String> doInBackground(Void… voids) {
File jsonFile = new File(getApplication().getFilesDir(),
"downloaded.json");
List<String> data = new ArrayList<>();
// Parse the JSON using the library of your choice
return data;
}
      @Override
protected void onPostExecute(List<String> data) {
setValue(data);
}
}.execute();
}
}

So our ViewModel gets considerably simpler, as you’d expect. Our LiveData now completely encapsulates the loading process, loading the data only once.

Data changes in a LiveData world

Just like how Loaders can react to changes elsewhere, this same functionality is key when working with LiveData — as the name implies, the data is expected to change! We can easily rework our class to continue to load data while there’s an observer:

public class JsonLiveData extends LiveData<List<String>> {
private final Context context;
private final FileObserver fileObserver;
public JsonLiveData(Context context) {
this.context = context;
String path = new File(context.getFilesDir(),
"downloaded.json").getPath();
fileObserver = new FileObserver(path) {
@Override
public void onEvent(int event, String path) {
// The file has changed, so let’s reload the data
loadData();
}
};
loadData();
}
  @Override
protected void onActive() {
fileObserver.startWatching();
}
  @Override
protected void onInactive() {
fileObserver.stopWatching();
}
  private void loadData() {
new AsyncTask<Void,Void,List<String>>() {
@Override
protected List<String> doInBackground(Void… voids) {
File jsonFile = new File(getApplication().getFilesDir(),
"downloaded.json");
List<String> data = new ArrayList<>();
// Parse the JSON using the library of your choice
return data;
}
      @Override
protected void onPostExecute(List<String> data) {
setValue(data);
}
}.execute();
}
}

Now that we’re interested in listening to changes, we can take advantage of LiveData’s onActive() and onInactive() callbacks to only listen when there’s an active observer on our data — as long as someone is observing, they can be guaranteed to get the latest data.

Observing data

In the Loader world, getting your data to your UI would involve a LoaderManager, calling initLoader() in the right place, and building a LoaderCallbacks. The world is a bit more straightforward in the Architecture Components world.

There’s two things we need to do:

  1. Get a reference to our ViewModel
  2. Start observing our LiveData

But explaining that is almost as long as the code itself:

public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
JsonViewModel model =
ViewModelProviders.of(this).get(JsonViewModel.class);
model.getData().observe(this, data -> {
// update UI
});
}
}

You’ll note there’s no need to clean things up after the fact: ViewModels automatically live only as long as they are needed and LiveData automatically only passes you data when calling Activity/Fragment/LifecycleOwner is started or resumed.

Load all the things

Now, if you’re still in the vehemently-against-AsyncTask camp, I’m totally okay with that: LiveData is a lot more flexible than being tied to only that construct.

For example, Room lets you have observerable queries — database queries that return LiveData so that database changes automatically propagate up through your ViewModel to your UI. Kind of like a CursorLoader without touching Cursors or Loaders.

We can also rewrite the FusedLocationApi example with a LiveData class:

public class LocationLiveData extends LiveData<Location> implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener,
LocationListener {
private GoogleApiClient googleApiClient;
  public LocationLiveData(Context context) {
googleApiClient =
new GoogleApiClient.Builder(context, this, this)
 .addApi(LocationServices.API)
 .build();
}
  @Override
protected void onActive() {
// Wait for the GoogleApiClient to be connected
googleApiClient.connect();
}
  @Override
protected void onInactive() {
if (googleApiClient.isConnected()) {
LocationServices.FusedLocationApi.removeLocationUpdates(
googleApiClient, this);
}
googleApiClient.disconnect();
}
  @Override
public void onConnected(Bundle connectionHint) {
// Try to immediately find a location
Location lastLocation = LocationServices.FusedLocationApi
.getLastLocation(googleApiClient);
if (lastLocation != null) {
setValue(lastLocation);
}
    // Request updates if there’s someone observing
if (hasActiveObservers()) {
LocationServices.FusedLocationApi.requestLocationUpdates(
googleApiClient, new LocationRequest(), this);
}
}
  @Override
public void onLocationChanged(Location location) {
// Deliver the location changes
setValue(location);
}
  @Override
public void onConnectionSuspended(int cause) {
// Cry softly, hope it comes back on its own
}
  @Override
public void onConnectionFailed(
@NonNull ConnectionResult connectionResult) {
// Consider exposing this state as described here:
// https://d.android.com/topic/libraries/architecture/guide.html#addendum
}
}

Just scratching the surface of the Architecture Components

There’s a lot more to the Android Architecture Components, so make sure to check out all of the documentation.

I’d personally strongly recommend reading through the entire Guide to App Architecture to give you an idea on how all of these components come together to form a solid architecture for your entire app.