Greetings
Today I decided to tell you my way of organizing the activity-service interaction in Android applications. The topic is motivated by the fact that quite often you can find applications in which, say, a hike to a server is organized inside an AsyncTask activity. In this case, the correct idea is often encountered that it should be done in services, but nowhere in the office. There is not a word about the documentation about the organization of the correct architecture of bilateral interaction between them.
Therefore, by trial and error, I came to an architecture that, for me personally, covers all the necessary questions.
')
I will discuss this method further.
Bird's-eye
Let's first look at the high-level picture of the proposed architecture.

Further in the article I will use two terms - controlled and uncontrolled feedback. These are informal terms, but I will use them, because I like them. Managed - these are notifications implemented by the Android platform for us (ContentProvider + ContentObserver system). In order for the UI to receive managed notifications, we do not need anything, except for a correctly implemented provider.
Much more interesting is how uncontrollable notifications are implemented, that is, those implemented using our system of events. After all, it is not always that some operation in the service is associated with writing to the provider, so we need our own mechanism for notifying the client that the service has completed.
So, this architecture implies the presence of four main components of the system:
- An activity that performs the standard interface mapping role.
- Service - a service that performs heavy work in the background thread.
- ServiceHelper is our component that will stick together our activites and services and provide unmanaged notifications.
- ContentProvider is optional, depending on your UI component, which will help implement managed notifications.
Service
Our service serves as a command processor.
Each incoming intent carries in extras:
- Action to be performed
- Team-defined arguments
- ResultReceiver
The service looks at the passed action, assigns to it the command to be executed, and passes the arguments and the ResultReceiver to the command.
The easiest way to implement the service:
protected void onHandleIntent(Intent intent) { String action = intent.getAction(); if (!TextUtils.isEmpty(action)) { final ResultReceiver receiver = intent.getParcelableExtra(EXTRA_STATUS_RECEIVER); if (AwesomeHandler.ACTION_AWESOME_ACTION.equals(action)) { new AwesomeHandler().execute(intent, getApplicationContext(), receiver); }
Here, in the large if block, the desired command is simply searched. It is clear, here you can be bent as you please to avoid ifa: keep the Map action-handler, make a factory, use IoC, etc., but this is beyond the scope of the article.
Handler
Handlers encapsulate the procedure being executed. In my case, they form a certain hierarchy, where the base class looks like:
public abstract class BaseIntentHandler { public static final int SUCCESS_RESPONSE = 0; public static final int FAILURE_RESPONSE = 1; public final void execute(Intent intent, Context context, ResultReceiver callback) { this.callback = callback; doExecute(intent, context, callback); } public abstract void doExecute(Intent intent, Context context, ResultReceiver callback); private ResultReceiver callback; private int result; public int getResult() { return result; } protected void sendUpdate(int resultCode, Bundle data) { result = resultCode; if (callback != null) { callback.send(resultCode, data); } } }
the next level of hierarchy I implemented a basic command that performs the preparation of the http request, but this, again, is beyond the scope of the article. In general, you inherit from the base command and implement doExecute, in which, if necessary, call the sendUpdate method, pass the code (success / error) and Bundle with the data.
Servicehelper
ServiceHelper is an intermediate layer between the UI and the service, simplifying calls to the service for the UI, and performing routine intent packaging tasks. It also coordinates coordinates from the service and contains information about the commands that are currently running.
So, how it works:
- UI invokes the helper method, the helper returns the request ID
- Helper remembers request ID
- Collects an intent into which the ResultReceiver inserts and sends it to the service.
- when the service completes the operation, all listening UI components are notified in onReceiveResult
Let's look at the code:
public class ServiceHelper { private ArrayList<ServiceCallbackListener> currentListeners = new ArrayList<ServiceCallbackListener>(); private AtomicInteger idCounter = new AtomicInteger(); private SparseArray<Intent> pendingActivities = new SparseArray<Intent>(); private Application application; ServiceHelper(Application app) { this.application = app; } public void addListener(ServiceCallbackListener currentListener) { currentListeners.add(currentListener); } public void removeListener(ServiceCallbackListener currentListener) { currentListeners.remove(currentListener); }
Service helper keeps a list of subscribers in an array, it is on this list will be sent notifications on the work of teams.
private Intent createIntent(final Context context, String actionLogin, final int requestId) { Intent i = new Intent(context, WorkerService.class); i.setAction(actionLogin); i.putExtra(WorkerService.EXTRA_STATUS_RECEIVER, new ResultReceiver(new Handler()) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { Intent originalIntent = pendingActivities.get(requestId); if (isPending(requestId)) { pendingActivities.remove(requestId); for (ServiceCallbackListener currentListener : currentListeners) { if (currentListener != null) { currentListener.onServiceCallback(requestId, originalIntent, resultCode, resultData); } } } } }); return i; }
This is a general method for creating our intent, which we will send to the service.
A more interesting place is pendingActivities - this is the register of all currently running tasks on the service. Because when we call the ServiceHelper method, we get an id, we can always find out if the command is running or not. More on this - later in the article.
public boolean isPending(int requestId) { return pendingActivities.get(requestId) != null; } private int createId() { return idCounter.getAndIncrement(); } private int runRequest(final int requestId, Intent i) { pendingActivities.append(requestId, i); application.startService(i); return requestId; }
Now an example of a public method that will perform some action on our service:
public int doAwesomeAction(long personId) { final int requestId = createId(); Intent i = createIntent(application, AwesomeHandler.ACTION_AWESOME_ACTION, requestId); i.putExtra(AwesomeHandler.EXTRA_PERSON_ID, personId); return runRequest(requestId, i); }
and there will be exactly as many such methods sticking out, as many commands are supported by your service.
Activity
So, as I said, we have an interface:
public interface ServiceCallbackListener { void onServiceCallback(int requestId, Intent requestIntent, int resultCode, Bundle data); }
I believe that it is convenient to implement this interface in the base abstract activit. Then in specific activations you only need to override the onServiceCallback method to receive notifications, which is very similar to the standard callback methods in the activity, i.e., it fits gracefully into your client code.
public abstract class AwesomeBaseActivity extends FragmentActivity implements ServiceCallbackListener { private ServiceHelper serviceHelper; protected AwesomeApplication getApp() { return (AwesomeApplication ) getApplication(); } protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); serviceHelper = getApp().getServiceHelper(); } protected void onResume() { super.onResume(); serviceHelper.addListener(this); } protected void onPause() { super.onPause(); serviceHelper.removeListener(this); } public ServiceHelper getServiceHelper() { return serviceHelper; } public void onServiceCallback(int requestId, Intent requestIntent, int resultCode, Bundle resultData) { } }
Notice how the activation subscribes and unsubscribes from ServiceHelper in its onResume / onPause methods. This allows you to avoid problems when recreating the activation, for example, when you rotate the screen, or minimize the application.
Let's consider what comes to the onServiceCallback method:
- requestId - a unique identifier generated when sending a request
- requestIntent - the original intent we sent
- resultCode - the result code of the execution
- resultData - data
Now, we have everything we need so that in our activity we can always receive a notification from our service without a heap of boilerplate code.
Moreover, it seems to me very useful - we can identify both a specific request by ID and all requests of the same type by action, which gives us tremendous flexibility.
class AwesomeActivity extends AwesomeBaseActivity { private int superRequestId; ... private void myMethod() { superRequestId = getServiceHelper().doAwesomeAction(); } public void onServiceCallback(int requestId, Intent requestIntent, int resultCode, Bundle resultData) { if (AwesomeHandler.ACTION_AWESOME_ACTION.equals(requestIntent.getAction()) {
Also, having a request ID, we can perform a pending check. Imagine the sequence:
- user started the action, started the spinner
- closed the application for 2 minutes
- the action has already been completed
- user opened again
- here we check in onResume whether the operation was completed, and remove the twist
that is, just call getServiceHelper (). isPending (requestId) if we need it.
Conclusion
Here, perhaps, that's all.
I must say at once that I do not claim for the universality of this architecture or for some of its uniqueness. She was slowly derived by me through trial and error, views of various materials, etc. But for now, she covers 100% of all my needs in real commercial projects.
Moreover, if something is missing, it can be easily expanded. From the obvious:
- In addition to the success and failure code, add progress code, then from the service it will be possible to transfer information about the progress of the task and display it in, say, ProgressBar
- fasten code to interrupt the task in progress
- etc.
One more detail, my ServiceHelper code is not synchronized, since it is assumed that its methods will always be called in UI thread. If this is not the case for you, then
it is necessary to add synchronization in case of any change in the state of ServiceHelper .
In general, thank you for your attention, I hope someone will help. Ready to answer your questions and comments in the comments.
UPD : Laid out a small sandbox example illustrating the architecture on GitHub:
https://github.com/TheHiddenDuck/android-service-archUPD 2 : Added an example implementation of a service running on a thread pool