Durable Android REST Clients
Series: android April 07, 2014
For something as common as interfacing with a REST API, one would think that there would be tons of great information about best practices for Android.
Maybe I’m not looking under the right rocks, because I certainly haven’t found
much. If you stick to the official Android docs, you will be immersed in a world
of components with names like AsyncTask
, ContentProvider
,
BroadcastReceivers
, and Loaders
. Are these components still widely-used
outside of Google? Should I trudge through the unfriendly APIs of Android or
look elsewhere?
If you dig a bit deeper, you may find a mystical link to a four-year old Google I/O talk that represents the “gold-standard”. Is this approach still relevant? It looks like a lot of code, are their any sample apps or do I just piece together everything from the PowerPoint slides? Surely the APIs have changed since 2009, right?
Head on over to the third-party blogs and tutorials and get ready to try to make sense of things like “non-UI retained Fragments” and “service binding”. Maybe I have just not spent enough time immersed in the Android SDK, but I find it incredibly difficult to understand some of these concepts.
This post will discuss the first steps of adding durability to your Android REST clients: moving network requests outside of the Activity lifecycle. As an added bonus, I find this approach to generate clear, understandable, and testable code that will not explode if the user gasp rotates their phone.
My approach is not perfect and I will highlight areas for improvement at the end of the post. My views have been heavily influenced by this enlightening post by Yigit Boyar (Path) and piecing together various chunks of wisdom from Jake Wharton (Square).
Let’s get to it!
Rule number one: you should never be making network requests directly from an
Activity (I will just say Activity from here on out, but everything applies to
Fragments as well). Even using an AsyncTask
is asking for trouble; when the
task finally completes, you have to guard against the fact that your host
activity could be gone (rotated, destroyed in the back stack, back pressed). Are
you leaking memory by holding Activity references? Are you sure?
So many example apps just shovel everything into Activities. It is not uncommon for an Activity in a tutorial (or even official SDK samples) to have 500-800 lines of code. This is analogous to having a 500-800 line controller in your web application! This is not good!
So where should I be making network requests then? My recommendation is in a plain-old Java object (POJO) that is tied to your Application lifecycle, not your Activities.
If we can come up with an approach that allows a POJO to a) easily make network calls and handle the responses and b) communicate with our activities without keeping direct references, then we are in business.
Retrofit (combined with Gson) is a joy to use when it comes to making network calls. For communication with Activities, we can use a library that implements the data bus pattern (Otto or EventBus are the popular options).
At a high-level, our architecture will look something like this:
- Our
Application
will create POJOs to manage API interaction; I call these POJOs “Services” (in the Service Oriented Architecture sense). Regrettably, this name is overloaded and is already a concept in Android, so use a different name if you find this too confusing (maybe “Repository” from Domain-driven design would be better) - Activities and Services will register on the bus and use this to communicate
- When we wish to load data from the API, we will post an event to the bus which will cause the Service to start the network request
- When a network request finishes, the Service will post the result back on the bus for any listening Activity to handle
The devil is in the details, so let’s look at some code for a simple news-reader app.
public class StoryActivity extends Activity {
private Bus mBus;
private StoryListAdapter mAdapter;
private ListView mListView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.story_activity);
mListView = findViewById(this, R.id.story_list);
mAdapter = new StoryListAdapter(this);
mListView.setAdapter(mAdapter);
}
@Override
public void onResume() {
super.onResume();
getBus().register(this);
getBus().post(new LoadStoriesEvent());
}
@Subscribe
public void onStoriesLoaded(StoriesLoadedEvent event) {
mAdapter.setStories(event.getStories());
}
@Override
public void onPause() {
super.onPause();
getBus().unregister(this);
}
// Use some kind of injection, so that we can swap in a mock for tests.
// Here we just use simple getter/setter injection for simplicity.
private Bus getBus() {
if (mBus == null) {
mBus = BusProvider.getInstance();
}
return mBus;
}
public void setBus(Bus bus) {
mBus = bus;
}
}
Everything is pretty standard in onCreate
. We setup the view elements and
wire up our list adapter.
We register/unregister from the Bus
in onResume
/onPause
— basically
if our activity is in the foreground, we want to be listening for events. This
can (and should) be moved to a base class (along with the getter/setters for
mBus
).
We kick off the process in onResume
by posting a LoadStoriesEvent
on the
bus. Our StoryService
will be subscribed to this event and kick off the API
calls.
Our activity then subscribes (with Otto’s @Subscribe
annotation) to a
corresponding StoriesLoadedEvent
— which will be posted after the API
call is returned and contain a list of Story
objects to display.
Notice how event-driven our activity is and how simple the methods are.
Testing becomes straight-forward: inject a mock bus and assert that we post the
correct event to the bus when resuming. Call onStoriesLoaded()
directly with
test data and assert we display the correct items.
Here is what the StoryService
looks like:
public class StoryService {
private StoryApi mApi;
private Bus mBus;
public StoryService(StoryApi api, Bus bus) {
mApi = api;
mBus = bus;
}
@Subscribe
public void onLoadStories(LoadStoriesEvent event) {
mApi.loadStories(new Callback<StoryResponse> {
@Override
public void success(StoryResponse response, Response rawResponse) {
mBus.post(new StoriesLoadedEvent(response.stories));
}
@Override
public void failure(RetrofitError error) {
mBus.post(new ApiErrorEvent(error));
}
});
}
}
Both dependencies (StoryApi
and Bus
) are injected via the constructor, so we
can easily pass in mocks for testing. We can make use of Mockito’s
ArgumentCaptor (see this post for more details) to test the asynchronous
callbacks. In fact, this whole class can be tested with regular old jUnit, so
the tests run super fast.
Our service accepts events from the bus and posts back new events after making the appropriate API calls. If an activity initiated an API call and then gets destroyed or backgrounded, we will still post the resulting response event, but no one will be there to listen.
For completeness, here is how we connect everything up using a custom Application class:
class ReaderApplication extends Application {
private StoryService mStoryService;
private Bus mBus = BusProvider.getInstance();
@Override
public void onCreate() {
super.onCreate();
mStoryService = new StoryService(buildApi(), mBus);
bus.register(mStoryService);
bus.register(this); //listen for "global" events
}
private StoryApi buildApi() {
return new RestAdapter.Builder()
.setServer(API_URL)
.build()
.create(StoryApi.class);
}
@Subscribe
public void onApiError(ApiErrorEvent event) {
toast("Something went wrong, please try again.");
Log.e("ReaderApp", event.getErrorMessage());
}
}
So back to our original goal, is this approach more durable?
Configuration changes (rotation, phone call, etc) will not crash the app. We
don’t have a bunch of defensive programming or null checks sprinkled every
where. We can even swap out the data source without making changes to our
activity. If we want to retrieve data from a database instead, we just need to
make sure to post the StoriesLoadedEvent
event once we retrieve the data. We
even have a more testable and less coupled design in the process!
Bear in mind that this approach is not the complete solution. We are always
re-querying data whenever our activity is resumed — this is not ideal for
battery life or necessary for data “freshness”. We could address this with
proper use of onSaveInstanceState()
or some state-keeping to determine if a
request is already in-flight.
We don’t have much resilience against network errors or loss of connection. The next step for addressing these issues would probably be to introduce a queue/job manager to handle retries and perserving user input.
We still aren’t fully satisfying the “gold standard” from the Google I/O talk. We will probably want to introduce local storage using SQLite and use this for querying the data (while updating the underlying database in the background). This provides a better user experience because we can update the UI immediately.
There is a lot of work required to create a truly robust REST client on Android (or any mobile platform for that matter). Moving your network operations out of the activity lifecycle using the approach outlined above is a good first step down this path.
I’d love to hear your thoughts, criticisms, or suggestions; get in touch with me on Twitter.