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 dataViewModels
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:
- The
AndroidViewModel
(a subclass ofViewModel
) only has a reference to the applicationContext
, so we’re very importantly not referencing theContext
of anActivity
, etc. that could present a leak — there’s even a Lint check to avoid these kind of issues. 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:
- Get a reference to our ViewModel
- 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.