Click here to Skip to main content
15,867,568 members
Articles / Mobile Apps / Android

Sample Implementation of Virgil Dobjanschi's Rest pattern

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
29 Jul 2012CPOL4 min read 47.6K   8   8
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: 

Java
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. 

Java
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: 

Java
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.  

Java
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: 

Java
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:

Java
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:

Java
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: 

Java
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:  

Java
registerReceiver(mBroadcastReceiver, mFilter); 

mServiceHelper = new RowsServiceHelper(mActivity, RETURN_ACTION); 

In onStop: 

Java
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



Comments and Discussions

 
QuestionМогли бы вы выложить ссылку на пример проект с данным паттерном? Pin
Member 130387474-Mar-17 21:05
Member 130387474-Mar-17 21:05 
QuestionDownload Dobjanschi's presentation as PDF Pin
KishanAhir26-Dec-13 18:23
KishanAhir26-Dec-13 18:23 
QuestionRoboSpice is a library based on this pattern Pin
Adam Mackler27-Dec-12 11:08
Adam Mackler27-Dec-12 11:08 
QuestionDataDroid is a library based on this pattern Pin
Adam Mackler27-Dec-12 10:51
Adam Mackler27-Dec-12 10:51 
QuestionHere's another implementation of the same pattern Pin
Adam Mackler27-Dec-12 10:43
Adam Mackler27-Dec-12 10:43 
QuestionCould you please share your code. Pin
Mateen Dar29-Nov-12 19:14
Mateen Dar29-Nov-12 19:14 
Questionhi Pin
Ronnie23_414-Oct-12 6:01
Ronnie23_414-Oct-12 6:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.