When it comes to developing mobile applications, there is really more to it than just monkey coding. Unlike web frameworks like RoR or CakePHP, which force you into a certain paradigm and make all the tough design decisions, Android (and app development in general) puts difficult design choices into the developer’s hands. One such choice is how to effectively communicate data over the network. This may seem like a simple task, but as we will see, there are several ways this task can be accomplished, and choosing the right design methodology can make the app both easier to write, and will present a better user experience.
Choosing HTTP and Parsing Libraries
The first obvious choices are how we will actually be communicating over the network. Using Android, the most common and easiest interface to write is one using a simple HTTP connection to transfer data in JSON or XML format. Android, and the Java platform upon which it is built, provides excellent support for network communication using those specifications, and if at all possible your server solution should match what makes the most sense on your client app.
We at DineEngine have had great success in using the droid-fu library’s excellent HTTP connection handling methods. These methods are synchronous, and build functionality around the existing Android network libraries. The fact that these methods are synchronous provides us with a base for our future design choices. Had the library we chose to use instead threaded itself and use callback runnables or Handlers, our remaining design choices would be quite different.
We also have have success using the inbuilt JSON parser and the inbuilt SAX2 Handlers and parsers available on Android. These classes make it easy to take the String content read from the above HTTP library, and either convert them directly to JSON objects or parse them into value objects using a SAX2 Handler implementation. Once this is complete, and we have populated whatever value objects our application needs, we are ready to move onto the next choice.
How to effectively handle errors, or unexpected API responses, is an important part of presenting the user with a robust experience. There are various ways a network request may fail: the network might be unavailable, the server received unexpected input, there was a server error, etc. Handing these errors, and specifically how you transport these errors from your API classes up to the Activity and possibly up to the user can make the difference between a frustrated user and a well informed bug reporter. In our case, we typically will create a Singleton class that handles all communication with the API, and will return a simple bean object that reports a status code and an object associated with the API request. The API management classes will also catch all relavent exceptions thrown and translate them into a mostly human readable form. If the network was unavailable, we catch a ConnectException, if the server returns an error code, we translate the error code into something that will inform the user, and so on. In this way we can keep our code robust and responsive, while also being very easy to maintain, as this approach lends itself well to encapsulation.
Data Transport Between API Classes and Activities
In order to transport data between the API manager classes and the UI, we abstract all functionality from the Activity to the API management classes. This means the calling Activity will simply call something like ResponseBean result = MyAPIManager.getInstance().getAllPosts(). The resulting bean will include the API response encoded into value objects, as well as a status code that we can use to make sure the API request went as expected. In the activity, we simply use the value objects from the resulting bean to populate or update our UI. This results in very light and clean Activities, that do not need to call boilerplate API methods like logging in the user, or making sure a particular post, for example, is selected before we perform some API operation on it. We rely on these assumptions and the fact that the API manager is stateful to keep our Activities from bloating and from requiring too much knowledge about the system as a whole. Since our HTTP methods are synchronous, we also do our parsing on the same thread to keep things simple. This means we can call BetterHTTP.post(), get a response, convert the response into JSON, and parse the JSON object into one or more value objects, all from within the same thread and even from within the same method. This makes our API manager classes very easy to follow, to write, and to maintain.
However, all this synchronicity, while a blessing, will also curse our UI with occasional stalls and hangs if we run our API methods on the UI thread. This is where the AsyncTask comes in mighty handy. But, instead of using the vanilla AsyncTask class that Android provides us, we typically will use the BetterAsyncTask class provided by the droid-fu library. This extended class provides us with a few simple enhancements, such as automatically continuing a task when the screen orientation changes and automatically popping up a progress dialog, that makes our code much simpler and cleaner. In short, an AsyncTask allows us to very easily start a synchronous operation on another thread. The AsyncTask handles all the cumbersome thread management, and we simply write a few lines of code to start our synchronous operation, and to in turn update the UI when the operation is complete. Since, in our case, we abstract all the API functionality into manager classes, our AsyncTasks are very simple and easy to write and to understand later.
As we can see, there are quite a few challenges and choices that an app developer must overcome in order to not only implement features, but also to provide the user with a clean experience. In short, our design choices center around using synchronous operations at a low level, encapsulating those operations into manager Singletons, populating value objects with the HTTP response content, and using AsyncTasks to free our UI from locking while those operations are executing. This allows a clean and easy to follow path from HTTP, to value objects, all the way up to the UI. In the middle of all this we also provide outlets for letting the user know when an error has occurred, or restarting the operation if we think that might be a better option. As such, these design choices probably do not cover all use cases, but they should at least provide a base upon which to build, and should not require major modifications.