Modern techniques for implementing REST clients on Android 4.0 and below – Part 2

This is the second and last part of a tutorial on implementing REST clients using modern APIs introduce in Honeycomb and Ice Cream Sandwich. In the last part of this tutorial I covered how to make REST calls using Loaders and the LoaderManager class. In this part of the tutorial, I will be covering how to make REST calls using Services and the motivations behind this approach.

Why use a Service?

The Loader approach worked well last time. LoaderManager has a very simple API and it manages the state of Loaders so that they are released and restarted when they need to be. However, as I pointed out in the previous blog post, they are inherently short lived tasks. If the Activity or Fragment containing the Loader is destroyed (which may happen when the user opens another app or gets a call), then the REST call will also get destroyed, whether or not the call completed.

This short lived behavior may not be bad for your application. In the demo Twitter app I built last time, this behavior wouldn't really affect much. Once the Activity has been destroyed and recreated, the Loader would also be recreated and it would fetch a new batch of tweets. However, maybe a REST response returns a large file or maybe you need to ensure that the call completes as part of your system. For this you need to have a part of your app that can last longer than your Activities in order to do its work. This is the exact use-case for Android Services.

Another benefit of using a Service is that it is not tied to an Activity or Fragment like a Loader. That allows you to make a REST call from all of your Android components, including other Services and BroadcastReceivers.

While a Service is more powerful, the trade off is a more complicated API with more edge cases to handle.

REST with Services

I will be returning to the simple Twitter demo so that it will be easier to compare the two approaches. I will also be using the Android Compatibility Library so that the app can make use of new Android APIs like Fragments all the way back to Android 1.6. The app is going to contain a RESTService that will replace the RESTLoader from last time, and an abstract RESTResponderFragment that will interact with the RESTService. I am modeling this framework after the Google I/O 2011 app.

You can get the finished app from this tutorial on the Android Market here, or by scanning the QR code below:

QR Code

You will want to get the source code on GitHub here: https://github.com/posco2k8/restservicetutorial, as I will only be referencing code snippets below.

RESTService.java

RESTService has been implemented as a subclass of IntentService. While Services are meant to encapsulate longer running operations in an Android app, they are not inherently threaded. This is something that often confuses new Android developers. IntentService, on the other hand, does provide a thread. It does this with the onIntent() method by calling it on a new thread. So, RESTService will override this method and do its work there:

rest-service.java
@Override
protected void onHandleIntent(Intent intent) {
    // When an intent is received by this Service, this method
    // is called on a new thread.

    Uri    action = intent.getData();
    Bundle extras = intent.getExtras();

    if (extras == null || action == null || !extras.containsKey(EXTRA_RESULT_RECEIVER)) {
        // Extras contain our ResultReceiver and data is our REST action.  
        // So, without these components we can't do anything useful.
        Log.e(TAG, "You did not pass extras or data with the Intent.");

        return;
    }

    // We default to GET if no verb was specified.
    int            verb     = extras.getInt(EXTRA_HTTP_VERB, GET);
    Bundle         params   = extras.getParcelable(EXTRA_PARAMS);
    ResultReceiver receiver = extras.getParcelable(EXTRA_RESULT_RECEIVER);

    try {            
        // Here we define our base request object which we will
        // send to our REST service via HttpClient.
        HttpRequestBase request = null;

        // Let's build our request based on the HTTP verb we were
        // given.
        switch (verb) {
            case GET: {
                request = new HttpGet();
                attachUriWithQuery(request, action, params);
            }
            break;

            case DELETE: {
                request = new HttpDelete();
                attachUriWithQuery(request, action, params);
            }
            break;

            case POST: {
                request = new HttpPost();
                request.setURI(new URI(action.toString()));

                // Attach form entity if necessary. Note: some REST APIs
                // require you to POST JSON. This is easy to do, simply use
                // postRequest.setHeader('Content-Type', 'application/json')
                // and StringEntity instead. Same thing for the PUT case 
                // below.
                HttpPost postRequest = (HttpPost) request;

                if (params != null) {
                    UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(paramsToList(params));
                    postRequest.setEntity(formEntity);
                }
            }
            break;

            case PUT: {
                request = new HttpPut();
                request.setURI(new URI(action.toString()));

                // Attach form entity if necessary.
                HttpPut putRequest = (HttpPut) request;

                if (params != null) {
                    UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(paramsToList(params));
                    putRequest.setEntity(formEntity);
                }
            }
            break;
        }

        if (request != null) {
            HttpClient client = new DefaultHttpClient();

            // Let's send some useful debug information so we can monitor things
            // in LogCat.
            Log.d(TAG, "Executing request: "+ verbToString(verb) +": "+ action.toString());

            // Finally, we send our request using HTTP. This is the synchronous
            // long operation that we need to run on this thread.
            HttpResponse response = client.execute(request);

            HttpEntity responseEntity = response.getEntity();
            StatusLine responseStatus = response.getStatusLine();
            int        statusCode     = responseStatus != null ? responseStatus.getStatusCode() : 0;

            // Our ResultReceiver allows us to communicate back the results to the caller. This
            // class has a method named send() that can send back a code and a Bundle
            // of data. ResultReceiver and IntentService abstract away all the IPC code
            // we would need to write to normally make this work.
            if (responseEntity != null) {
                Bundle resultData = new Bundle();
                resultData.putString(REST_RESULT, EntityUtils.toString(responseEntity));
                receiver.send(statusCode, resultData);
            }
            else {
                receiver.send(statusCode, null);
            }
        }
    }
    catch (URISyntaxException e) {
        Log.e(TAG, "URI syntax was incorrect. "+ verbToString(verb) +": "+ action.toString(), e);
        receiver.send(0, null);
    }
    catch (UnsupportedEncodingException e) {
        Log.e(TAG, "A UrlEncodedFormEntity was created with an unsupported encoding.", e);
        receiver.send(0, null);
    }
    catch (ClientProtocolException e) {
        Log.e(TAG, "There was a problem when sending the request.", e);
        receiver.send(0, null);
    }
    catch (IOException e) {
        Log.e(TAG, "There was a problem when sending the request.", e);
        receiver.send(0, null);
    }
}

If you have read the previous tutorial, you will notice that this code is almost exactly the same as RESTLoader. The fundmental approach to sending the HTTP request has not changed, but where the Android component sending the request has. Instead of using LoaderManager, we now have to send an Intent to start this Service, which will in turn cause the onIntent() method to be called.

Another difference is how we deliver the results back to the caller. With Loaders we could make use of the convient LoaderCallbacks interface, but that no longer exists. Now we have what is more like an IPC (Inter-Process Communication) problem. Android provides an API to make IPC a little easier on the platform, but the good news is that we don't even have to bother with that. IntentService and another class called ResultReceiver abstract away those details. So, in the Intent that gets sent to the Service, we can just attach a ResultReceiver which will handle all the IPC details for us. More on how we do that later.

Note: While I mention IPC above, don't take that to mean that we are dealing with two processes here. Unless you specify otherwise in your AndroidManifest.xml file, a Service runs under the same process as all of your other app code. However, the above code is running in a separate thread, so we are communicating across threads. It makes little difference at the end of the day, since the two problems are so similar, but I just wanted to remove any ambiguity. Again, we don't have to worry about any of this since the IntentService/ResultReceiver pattern makes it a trivial problem to solve.

RESTResponderFragment.java

Now that we have our RESTService, we need a way to interface with it. I described how ResultReceiver will be used to handle communication between the Service and the caller, but we actually need a more robust solution if we are going to be making this call from an Activity. Here is why:

  • As I have mentioned before, Activities are fleeting things, but Services are not. What happens when our REST call completes but our Activity is gone? We need a way to handle this.

  • What happens in the case that our Activity switches from landscape to portrait? When this happens on Android, the Activity is actually recreated. What if there is a call being processed during the time the Activity is being recreated? LoaderManager helped us out last time, but now we need to do a little extra work.

The common theme above is that the Activity can go away, but the Service will remain. This problem is solved by using another new Android API that was introduced during Honeycomb: a Fragment. If you read through the code in the last part of the tutorial, you may have noticed the use of FragmentManager and ListFragment. We are going to be using these again, but in addition we are going to be using a new custom fragment called RESTResponderFragment. It's a small class, so I'll post all of it below:

rest-responder.java
public abstract class RESTResponderFragment extends Fragment {

    private ResultReceiver mReceiver;

    // We are going to use a constructor here to make our ResultReceiver,
    // but be careful because Fragments are required to have only zero-arg
    // constructors. Normally you don't want to use constructors at all
    // with Fragments.
    public RESTResponderFragment() {
        mReceiver = new ResultReceiver(new Handler()) {

            @Override
            protected void onReceiveResult(int resultCode, Bundle resultData) {
                if (resultData != null && resultData.containsKey(RESTService.REST_RESULT)) {
                    onRESTResult(resultCode, resultData.getString(RESTService.REST_RESULT));
                }
                else {
                    onRESTResult(resultCode, null);
                }
            }

        };
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // This tells our Activity to keep the same instance of this
        // Fragment when the Activity is re-created during lifecycle
        // events. This is what we want because this Fragment should
        // be available to receive results from our RESTService no
        // matter what the Activity is doing.
        setRetainInstance(true);
    }

    public ResultReceiver getResultReceiver() {
        return mReceiver;
    }

    // Implementers of this Fragment will handle the result here.
    abstract public void onRESTResult(int code, String result);
}

Notice that this Fragment contains a ResultReceiver, so now this Fragment is capable of communicating with the RESTService. The onCreate() method is also important to note. It calls setRetainInstance(true) to ensure that an Activity only ever stores a single instance to this Fragment during its lifecycle.

Finally, this is an abstract class, because it is meant to be subclassed by other Fragments that can better process specific REST calls.

TwitterSearchResponderFragment.java

There are three methods that are important to cover in our subclass of RESTResponderFragment. Here are the first two:

twitter-search-responder-fragment-1.java
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    // This gets called each time our Activity has finished creating itself.
    setTweets();
}

private void setTweets() {
    RESTServiceActivity activity = (RESTServiceActivity) getActivity();

    if (mTweets == null && activity != null) {
        // This is where we make our REST call to the service. We also pass in our ResultReceiver
        // defined in the RESTResponderFragment super class.

        // We will explicitly call our Service since we probably want to keep it as a private
        // component in our app. You could do this with Intent actions as well, but you have
        // to make sure you define your intent filters correctly in your manifest.
        Intent intent = new Intent(activity, RESTService.class);
        intent.setData(Uri.parse("http://search.twitter.com/search.json"));

        // Here we are going to place our REST call parameters. Note that
        // we could have just used Uri.Builder and appendQueryParameter()
        // here, but I wanted to illustrate how to use the Bundle params.
        Bundle params = new Bundle();
        params.putString("q", "android");

        intent.putExtra(RESTService.EXTRA_PARAMS, params);
        intent.putExtra(RESTService.EXTRA_RESULT_RECEIVER, getResultReceiver());

        // Here we send our Intent to our RESTService.
        activity.startService(intent);
    }
    else if (activity != null) {
        // Here we check to see if our activity is null or not.
        // We only want to update our views if our activity exists.

        ArrayAdapter<String> adapter = activity.getArrayAdapter();

        // Load our list adapter with our Tweets.
        adapter.clear();
        for (String tweet : mTweets) {
            adapter.add(tweet);
        }
    }
}

Here the fragment makes use of the onActivityCreated() lifecycle event. The Fragment uses this method to send the REST call once it has been added to the Activity and the Activity has finished being creating. The method is simple, it just calls the setTweets() helper method.

The setTweets() method does all the work. First it checks to see if it has any cached results, if so then it just uses those. Otherwise it builds the Intent for the RESTService. The Intent contains the REST action and the parameters for that action. The Intent could also store an HTTP verb like POST, PUT, or DELETE, but it is not necessary here since RESTService defaults to GET. Another piece to notice is that the method checks if the Activity is null before doing any of its work. This is to catch the case that the Activity doesn't exist when the REST response completes.

The last method for TwitterSearchResponderFragment:

twitter-search-responder-fragment-2.java
@Override
public void onRESTResult(int code, String result) {
    // Here is where we handle our REST response. This is similar to the 
    // LoaderCallbacks<D>.onLoadFinished() call from the previous tutorial.

    // Check to see if we got an HTTP 200 code and have some data.
    if (code == 200 && result != null) {

        // For really complicated JSON decoding I usually do my heavy lifting
        // with Gson and proper model classes, but for now let's keep it simple
        // and use a utility method that relies on some of the built in
        // JSON utilities on Android.
        mTweets = getTweetsFromJson(result);
        setTweets();
    }
    else {
        Activity activity = getActivity();
        if (activity != null) {
            Toast.makeText(activity, "Failed to load Twitter data. Check your internet settings.", Toast.LENGTH_SHORT).show();
        }
    }
}

This method gets called once the RESTService call has completed. It is very much like the onLoadFinished() method from the last tutorial. Once the JSON has been parsed, it uses the setTweets() utility method to finish processing the data.

RESTServiceActivity.java

rest-service-activity.java
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_rest_service);

    mAdapter = new ArrayAdapter<String>(this, R.layout.item_label_list);

    FragmentManager     fm = getSupportFragmentManager();
    FragmentTransaction ft = fm.beginTransaction();

    // Since we are using the Android Compatibility library
    // we have to use FragmentActivity. So, we use ListFragment
    // to get the same functionality as ListActivity.
    ListFragment list = new ListFragment();
    ft.add(R.id.fragment_content, list);

    // Let's set our list adapter to a simple ArrayAdapter.
    list.setListAdapter(mAdapter);

    // RESTResponderFragments call setRetainedInstance(true) in their onCreate() method. So that means
    // we need to check if our FragmentManager is already storing an instance of the responder.
    TwitterSearchResponderFragment responder = (TwitterSearchResponderFragment) fm.findFragmentByTag("RESTResponder");
    if (responder == null) {
        responder = new TwitterSearchResponderFragment();

        // We add the fragment using a Tag since it has no views. It will make the Twitter REST call
        // for us each time this Activity is created.
        ft.add(responder, "RESTResponder");
    }

    // Make sure you commit the FragmentTransaction or your fragments
    // won't get added to your FragmentManager. Forgetting to call ft.commit()
    // is a really common mistake when starting out with Fragments.
    ft.commit();
}

The Activity code is pretty simple. It creates the two Fragments it needs, ListFragment and TwitterSearchResponderFragment, and then commits those to the FragmentManager using a FragmentTransaction. Before the Activity makes an instance of TwitterSearchResponderFragment, it checks to see if it exists because the FragmentManager could already be storing an instance of the Fragment.

That's it. Once the TwitterSearchResponderFragment has been committed, it will automatically make the REST call using RESTService and then process the results once they come it. Note that using a Fragment only really makes sense here in the Activity. If I needed to make a REST call from another Android component, like a Service or a BroadcastReceiver, I would just make the Intent for RESTService directly and pass in a ResultReceiver. The Fragment is only needed when the results from the REST call are going to be inserted in a View on an Activity.

If you find any bugs or have any questions, let me know in the comments.


comments powered by