Sample Implementation of Virgil Dobjanschi's Rest pattern
This is a sample implementation of Pattern A from Virgil Dobjanschi's talk at Google IO 2010.
Introduction
After watching the great talk by Virgil Dobjanschi from Google IO 2010 on Restful Android applications I searched the net for implementations of his patterns ending up with very little results.
This is my attempt at implementing Pattern A from his presentation.
If you use the code I would be interested in how you are using it, i.e. are you using it for a personal project or a commercial one, is the app going on the market? I would be keen to see any apps which use the code.
I welcome any comments including any suggested improvements.Background
I recommend that if you have not already watched Dobjanschi's presentation, that you do so, it is found here:
http://www.youtube.com/watch?v=xHXn3Kg2IQE
The main reason I chose Pattern A from the talk is that you can give the REST methods whatever interface you want, you are not restricted to the ContentProvider API alone.
Implementation
I will explain the code starting from the database and the REST calls and then move up to the UI.
Rest
I have an abstract class which exposes Post, Put, Get, Delete methods to the sub classes.
I have a number of sub classes which will call these methods on the base and parse their results to data objects.
For each Rest method I wish to use I have a method something like this:
public RowDTO[] getRows()
{
// Make rest call by calling PUT/POST etc on the base class
// From result of HTTP call create an array of RowDTO's and return it
}
I perform HTTP calls synchronously at this level as this is running on it's own thread, as we will see when we get to the ProcessorService
Processor
I have basically implemented a processor for each table in the database.
The processor's job is to make a call to Rest and to update the SQL as unnecessary.
I pass a reference to the Context from the service so that the Processor can access the database using:
Context.getContentResolver()
I will not provide any code here as I think the implementation will change substantially for each application and, in his video, Dobjanschi gives a good description of how to implement this.
ServiceProvider
I have an IServiceProvider
interface which provides a common interface to the processor from the
ProcessorService
.
The main purpose for this class is to translate an integer constant to a specific method on the processor and to parse the arguments from a Bundle
to typed arguments for the processor method.
import android.os.Bundle;
/**
* Implementations of this interface should allow methods on their respective Processor to be called through the RunTask method.
*/
public interface IServiceProvider
{
/**
* A common interface for all Processors.
* This method should make a call to the processor and return the result.
* @param methodId The method to call on the processor.
* @param extras Parameters to pass to the processor.
* @return The result of the method
*/
boolean RunTask(int methodId, Bundle extras);
}
An example implementation of this interface is:
import android.content.Context;
import android.os.Bundle;
public class RowsServiceProvider implements IServiceProvider
{
private final Context mContext;
public RowsServiceProvider(Context context)
{
mContext = context;
}
/**
* Identifier for each provided method.
* Cannot use 0 as Bundle.getInt(key) returns 0 when the key does not exist.
*/
public static class Methods
{
public static final int REFRESH_ROWS_METHOD = 1;
public static final int DELETE_ROW_METHOD = 2;
public static final String DELETE_ROW_PARAMETER_ID = "id";
}
@Override
public boolean RunTask(int methodId, Bundle extras)
{
switch(methodId)
{
case Methods.REFRESH_ROWS_METHOD:
return refreshRows();
case Methods.DELETE_ROW_METHOD:
return deleteRow(extras);
}
return false;
}
private boolean refreshRows()
{
return new RowsProcessor(mContext).resfreshRows();
}
private boolean deleteRow(Bundle extras)
{
int id = extras.getInt(Methods.DELETE_ROW_PARAMETER_ID);
return new RowsProcessor(mContext).deleteRow(id);
}
}
ProcessorService
This seems to me to be the most complex part of the pattern.
This service takes care of running each call to a Processor on its own thread.
It also ensures that if a method is currently running and it is called again with the same parameters, then instead of running the method multiple times in parallel, a single call will just notify both callers when it is complete.
To start a method call an Intent should be sent to the onStart
method of this service, this will start the service if the service is not already running.
The intent will contain the following details:
- Which processor does the intended method exist on.
- The method to call.
- The parameters for the method.
- A tag to be used for the result Intent.
The result tag is used by the caller to identify a result intent to send when the method call completes. In a case where two calls are made to the same method, the method is only called once, however each caller may specify it's own result tag so that it can individually be notified of completion and whether the call completed successfully.
The result intents contains all the extras passed to start the service (including the processor called, method called, any parameters passed) and also a boolean result indicating if the call was successful.
When all methods complete this service will shut itself down.
import java.util.ArrayList;
import java.util.HashMap;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
/**
* This service is for making asynchronous method calls on providers.
* The fact that this is a service means the method calls
* will continue to run even when the calling activity is killed.
*/
public class ProcessorService extends Service
{
private Integer lastStartId;
private final Context mContext = this;
/**
* The keys to be used for the required actions to start this service.
*/
public static class Extras
{
/**
* The provider which the called method is on.
*/
public static final String PROVIDER_EXTRA = "PROVIDER_EXTRA";
/**
* The method to call.
*/
public static final String METHOD_EXTRA = "METHOD_EXTRA";
/**
* The action to used for the result intent.
*/
public static final String RESULT_ACTION_EXTRA = "RESULT_ACTION_EXTRA";
/**
* The extra used in the result intent to return the result.
*/
public static final String RESULT_EXTRA = "RESULT_EXTRA";
}
private final HashMap<String, AsyncServiceTask> mTasks = new HashMap<String, AsyncServiceTask>();
/**
* Identifier for each supported provider.
* Cannot use 0 as Bundle.getInt(key) returns 0 when the key does not exist.
*/
public static class Providers
{
public static final int ROWS_PROVIDER = 1;
}
private IServiceProvider GetProvider(int providerId)
{
switch(providerId)
{
case Providers.ROWS_PROVIDER:
return new RowsServiceProvider(this);
}
return null;
}
/**
* Builds a string identifier for this method call.
* The identifier will contain data about:
* What processor was the method called on
* What method was called
* What parameters were passed
* This should be enough data to identify a task to detect if a similar task is already running.
*/
private String getTaskIdentifier(Bundle extras)
{
String[] keys = extras.keySet().toArray(new String[0]);
java.util.Arrays.sort(keys);
StringBuilder identifier = new StringBuilder();
for (int keyIndex = 0; keyIndex < keys.length; keyIndex++)
{
String key = keys[keyIndex];
// The result action may be different for each call.
if (key.equals(Extras.RESULT_ACTION_EXTRA))
{
continue;
}
identifier.append("{");
identifier.append(key);
identifier.append(":");
identifier.append(extras.get(key).toString());
identifier.append("}");
}
return identifier.toString();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
// This must be synchronised so that service is not stopped while a new task is being added.
synchronized (mTasks)
{
// stopSelf will be called later and if a new task is being added we do not want to stop the service.
lastStartId = startId;
Bundle extras = intent.getExtras();
String taskIdentifier = getTaskIdentifier(extras);
Log.i("ProcessorService", "starting " + taskIdentifier);
// If a similar task is already running then lets use that task.
AsyncServiceTask task = mTasks.get(taskIdentifier);
if (task == null)
{
task = new AsyncServiceTask(taskIdentifier, extras);
mTasks.put(taskIdentifier, task);
// AsyncTasks are by default only run in serial (depending on the android version)
// see android documentation for AsyncTask.execute()
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
}
// Add this Result Action to the task so that the calling activity can be notified when the task is complete.
String resultAction = extras.getString(Extras.RESULT_ACTION_EXTRA);
if (resultAction != "")
{
task.addResultAction(extras.getString(Extras.RESULT_ACTION_EXTRA));
}
}
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent)
{
return null;
}
public class AsyncServiceTask extends AsyncTask<Void, Void, Boolean>
{
private final Bundle mExtras;
private final ArrayList<String> mResultActions = new ArrayList<String>();
private final String mTaskIdentifier;
/**
* Constructor for AsyncServiceTask
*
* @param taskIdentifier A string which describes the method being called.
* @param extras The Extras from the Intent which was used to start this method call.
*/
public AsyncServiceTask(String taskIdentifier, Bundle extras)
{
mTaskIdentifier = taskIdentifier;
mExtras = extras;
}
public void addResultAction(String resultAction)
{
if (!mResultActions.contains(resultAction))
{
mResultActions.add(resultAction);
}
}
@Override
protected Boolean doInBackground(Void... params)
{
Log.i("ProcessorService", "working " + mTaskIdentifier);
Boolean result = false;
final int providerId = mExtras.getInt(Extras.PROVIDER_EXTRA);
final int methodId = mExtras.getInt(Extras.METHOD_EXTRA);
if (providerId != 0 && methodId != 0)
{
final IServiceProvider provider = GetProvider(providerId);
if (provider != null)
{
try
{
result = provider.RunTask(methodId, mExtras);
} catch (Exception e)
{
result = false;
}
}
}
return result;
}
@Override
protected void onPostExecute(Boolean result)
{
// This must be synchronised so that service is not stopped while a new task is being added.
synchronized (mTasks)
{
Log.i("ProcessorService", "finishing " + mTaskIdentifier);
// Notify the caller(s) that the method has finished executing
for (int i = 0; i < mResultActions.size(); i++)
{
Intent resultIntent = new Intent(mResultActions.get(i));
resultIntent.putExtra(Extras.RESULT_EXTRA, result.booleanValue());
resultIntent.putExtras(mExtras);
resultIntent.setPackage(mContext.getPackageName());
mContext.sendBroadcast(resultIntent);
}
// The task is complete so remove it from the running tasks list
mTasks.remove(mTaskIdentifier);
// If there are no other executing methods then stop the service
if (mTasks.size() < 1)
{
stopSelf(lastStartId);
}
}
}
}
}
ServiceHelper
The service helper is simply provides a nice interface for upper layers as well as 'helping' with creating intents and starting the ProcessService.
The abstract class looks like this:
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
/**
* The service helpers are a facade for starting a task on the ProcessorService.
* The purpose of the helpers is to give a simple interface to the upper layers to make asynchronous method calls in the service.
*/
public abstract class ServiceHelperBase
{
private final Context mcontext;
private final int mProviderId;
private final String mResultAction;
public ServiceHelperBase(Context context, int providerId, String resultAction)
{
mcontext = context;
mProviderId = providerId;
mResultAction = resultAction;
}
/**
* Starts the specified methodId with no parameters
* @param methodId The method to start
*/
protected void RunMethod(int methodId)
{
RunMethod(methodId, null);
}
/**
* Starts the specified methodId with the parameters given in Bundle
* @param methodId The method to start
* @param bundle The parameters to pass to the method
*/
protected void RunMethod(int methodId, Bundle bundle)
{
Intent service = new Intent(mcontext, ProcessorService.class);
service.putExtra(ProcessorService.Extras.PROVIDER_EXTRA, mProviderId);
service.putExtra(ProcessorService.Extras.METHOD_EXTRA, methodId);
service.putExtra(ProcessorService.Extras.RESULT_ACTION_EXTRA, mResultAction);
if (bundle != null)
{
service.putExtras(bundle);
}
mcontext.startService(service);
}
}
An example sub class:
import android.content.Context;
public class RowsServiceHelper extends ServiceHelperBase
{
public RowsServiceHelper(Context context, String resultAction)
{
super(context, ProcessorService.Providers.ROWS_PROVIDER, resultAction);
}
public void refreshRows()
{
RunMethod(RowsServiceProvider.Methods.REFRESH_ROWS_METHOD);
}
public void deleteRow(int id)
{
Bundle extras = new Bundle();
extras.putInt(RowsServiceProviderMethods.DELETE_ROW_PARAMETER_ID, id);
RunMethod(RowsServiceProvider.Methods.DELETE_ROW_METHOD, extras);
}
}
Using The RowsProcessor
Now for the upper layer, usually this will be in an activity.
To receive result intents use the following code:
Create an Intent filter in you code for the return intents:
private final static String RETURN_ACTION = "com.MyApp.RowsActivity.ActionResult";
private final IntentFilter mFilter = new IntentFilter(RETURN_ACTION);
Create a Broadcast receiver to handle return intents:
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver()
{
@Override
public void onReceive(Context context, Intent intent)
{
Bundle extras = intent.getExtras();
boolean success = extras.getBoolean(ProcessorService.Extras.RESULT_EXTRA);
// Which method is the result for
int method = extras.getInt(ProcessorService.Extras.METHOD_EXTRA);
String text;
if (success)
{
text = "Method " + method + " passed!";
}
else
{
text = "Method " + method + " failed!";
}
int duration = Toast.LENGTH_SHORT;
Toast toast = Toast.makeText(context, text, duration);
toast.show();
}
};
In your activities onStart method:
registerReceiver(mBroadcastReceiver, mFilter);
mServiceHelper = new RowsServiceHelper(mActivity, RETURN_ACTION);
In onStop:
unregisterReceiver(mBroadcastReceiver);
Now you can simply call any method on mServiceHelper
in your activity to make REST calls on their own thread and update the database, and you will be notified via mBroadcastReceiver
.
History
28 July 2012 - Initial post