Приветствую!
Сегодня я решил поведать Вам мой способ организации activity-service interaction в Android приложениях. Мотивирован топик тем, что достаточно часто можно встретить приложения, в которых, скажем, поход на сервер организовывается внутри активити в AsyncTask. При этом часто встречается верная мысль, что это надо делать в сервисах, но нигде в оф. документации нет ни слова об организации правильной архитектуры двустороннего взаимодействия между ними.
Поэтому я методом проб и ошибок пришел к архитектуре, лично для меня покрывающей все необходимые вопросы.
Об этом методе я буду рассказывать далее.
Давайте сначала рассмотрим высокоуровневую картину предлагаемой архитектуры.

Далее в статье я буду использовать два термина — управляемая и неуправляемая обратная связь. Это неофициальные термины, но я их буду использовать, т. к. они мне нравятся. Управляемые — это уведомления, осуществляемые платформой Android для нас (ContentProvider + ContentObserver система). Для того, чтобы UI получал управляемые уведомления нам ничего не нужно, кроме корректно реализованного провайдера.
Гораздо интересней — как реализованы неуправляемые уведомления, т. е. те, которые осуществляются при помощи нашей системы событий. Ведь не всегда выполнение какой-то операции в сервисе сопряжено с записью в провайдер, поэтому нам нужен свой механизм уведомления клиента о том, что сервис завершил работу.
Итак, данная архитектура подразумевает наличие четырех основных к��мпонентов системы:
Наш сервис выполняет роль command processor'а.
Каждый входящий интент несет в extras:
Сервис смотрит на переданный action, сопоставляет ему команду, которую нужно выполнить, и передает аргументы и ResultReceiver команде.
Самый простой вариант реализации сервиса:
Здесь в большом блоке if просто ищется нужная команда. Понятное дело, здесь можно как угодно загнаться, чтобы избежать ифа: держать Map action-handler, сделать фабрику, использовать IoC и т. п., но это выходит за рамки статьи.
Обработчики инкапсулируют в себе выполняемую процедуру. У меня они образуют определенную иерархию, где базовый класс выглядит как:
следующим уровнем иерархии я реализовал базовую команду, выполняющую подготовку http запроса, но это, опять же выходит за рамки статьи. В целом, Вы наследуетесь от базовой команды и реализуете doExecute, в котором при необходимости вызываете sendUpdate метод, передаете код (успех/ошибка) и Bundle с данными.
ServiceHelper — это промежуточный слой между UI и сервисом, упрощающий вызовы к сервису для UI, и выполняющий рутинные операции по упаковке интентов. Также он координирует координирует ответы от сервиса и содержит информацию о командах, выполняющихся в данный момент.
Итак, как это работает:
Давайте посмотрим на код:
Сервис хелпер держит список подписчиков в массиве, именно на этот список будут рассылаться уведомления по работе команд.
это — общий метод по созданию нашего интента, который мы зашлем сервису.
Более интересным местом является pendingActivities — это регистр всех выполняющихся на данный момент задач на сервисе. Поскольку при вызове метода ServiceHelper мы получаем id, мы всегда можем узнать, выполняется команда или нет. Подробней об этом — чуть далее в статье.
Теперь пример public метода, который будет выполнять какое-то действие на нашем сервисе:
и вот таких методов, торчащих наружу будет ровно столько, сколько команд поддерживает ваш сервис.
Итак, как я уже сказал, у нас есть интерфейс:
Я считаю, что удобно в базовой абстрактной активити реализовывать сей интерфейс. Тогда в конкретных активити Вам надо будет всего лишь переопределить метод onServiceCallback для получения уведомлений, что очень похоже на стандартные callback методы в activity, т. е. грациозно вписывается в Ваш клиентский код.
Обратите внимание, как активити подписывается и отписывается от ServiceHelper в своих методах onResume/onPause. Это позволяет избегать проблем при пересоздании активити, например, при повороте экрана, или сворачивании приложения.
Давайте рассмотрим, что нам приходит в метод onServiceCallback:
Теперь, у нас есть все необходимое, чтобы в нашей activity мы всегда могли получить уведомление от нашего сервиса без кучи boilerplate кода.
Более того, что мне кажется очень полезным — мы можем идентифицировать как конкретный запрос по ID, так и все запросы одного типа по action, что дает нам огромную гибкость.
Также, имея ID запроса, мы можем выполнять отложенную проверку. Представим последовательность:
т. е., просто вызываем getServiceHelper().isPending(requestId), если нам это нужно.
Вот, пожалуй, и все.
Сразу скажу, что я не претендую на универсальность данной архитектуры или на какую-то ее уникальность. Она была медленно выведена мной путем проб и ошибок, просмотров различных материалов и т. п. Но, пока, все мои нужды в настоящих коммерческих проектах она покрывает на 100%.
Более того, если чего-от не хватает — ее можно легко расширить. Из очевидного:
Еще одна деталь, у меня код ServiceHelper не синхронизирован, т. к. подразумевается, что его методы будут вызываться в UI thread всегда. Если у Вас это не так, то необходимо добавить синхронизацию при любом изменении состояния ServiceHelper.
В общем, спасибо за внимание, надеюсь, кому-то поможет. Готов отвечать на Ваши вопросы и замечания в комментах.
UPD: Выложил маленький sandbox примерчик, иллюстрирующий архитектуру на GitHub: https://github.com/TheHiddenDuck/android-service-arch
UPD 2: Добавил пример реализации сервиса, работающего на пуле потоков
Сегодня я решил поведать Вам мой способ организации activity-service interaction в Android приложениях. Мотивирован топик тем, что достаточно часто можно встретить приложения, в которых, скажем, поход на сервер организовывается внутри активити в AsyncTask. При этом часто встречается верная мысль, что это надо делать в сервисах, но нигде в оф. документации нет ни слова об организации правильной архитектуры двустороннего взаимодействия между ними.
Поэтому я методом проб и ошибок пришел к архитектуре, лично для меня покрывающей все необходимые вопросы.
Об этом методе я буду рассказывать далее.
С высоты птичьего полета
Давайте сначала рассмотрим высокоуровневую картину предлагаемой архитектуры.

Далее в статье я буду использовать два термина — управляемая и неуправляемая обратная связь. Это неофициальные термины, но я их буду использовать, т. к. они мне нравятся. Управляемые — это уведомления, осуществляемые платформой Android для нас (ContentProvider + ContentObserver система). Для того, чтобы UI получал управляемые уведомления нам ничего не нужно, кроме корректно реализованного провайдера.
Гораздо интересней — как реализованы неуправляемые уведомления, т. е. те, которые осуществляются при помощи нашей системы событий. Ведь не всегда выполнение какой-то операции в сервисе сопряжено с записью в провайдер, поэтому нам нужен свой механизм уведомления клиента о том, что сервис завершил работу.
Итак, данная архитектура подразумевает наличие четырех основных к��мпонентов системы:
- Activity, выполняющую стандартную роль отображения интерфейса
- Service — сервис, выполняющий тяжелую работу в background потоке
- ServiceHelper — наш компонент, который будет склеивать нашу активити и сервис и предоставлять неуправляемые уведомления
- ContentProvider — необязательный, в зависимости от вашего UI компонент, который будет помогать осуществлять управляемые уведомления.
Сервис
Наш сервис выполняет роль command processor'а.
Каждый входящий интент несет в extras:
- Действие, которое необходимо выполнить
- Аргументы, определяемые командой
- ResultReceiver
Сервис смотрит на переданный action, сопоставляет ему команду, которую нужно выполнить, и передает аргументы и ResultReceiver команде.
Самый простой вариант реализации сервиса:
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); } // ..... } }
Здесь в большом блоке if просто ищется нужная команда. Понятное дело, здесь можно как угодно загнаться, чтобы избежать ифа: держать Map action-handler, сделать фабрику, использовать IoC и т. п., но это выходит за рамки статьи.
Handler
Обработчики инкапсулируют в себе выполняемую процедуру. У меня они образуют определенную иерархию, где базовый класс выглядит как:
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); } } }
следующим уровнем иерархии я реализовал базовую команду, выполняющую подготовку http запроса, но это, опять же выходит за рамки статьи. В целом, Вы наследуетесь от базовой команды и реализуете doExecute, в котором при необходимости вызываете sendUpdate метод, передаете код (успех/ошибка) и Bundle с данными.
ServiceHelper
ServiceHelper — это промежуточный слой между UI и сервисом, упрощающий вызовы к сервису для UI, и выполняющий рутинные операции по упаковке интентов. Также он координирует координирует ответы от сервиса и содержит информацию о командах, выполняющихся в данный момент.
Итак, как это работает:
- UI вызывает метод хелпера, хелпер возвращает ID запроса
- Хелпер запоминает ID запроса
- Собирает Intent, в который вкладывет ResultReceiver и отправляет сервису
- когда сервис завершает операцию, в onReceiveResult оповещаются все слушающие UI компоненты
Давайте посмотрим на код:
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); } // .....
Сервис хелпер держит список подписчиков в массиве, именно на этот список будут рассылаться уведомления по работе команд.
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; }
это — общий метод по созданию нашего интента, который мы зашлем сервису.
Более интересным местом является pendingActivities — это регистр всех выполняющихся на данный момент задач на сервисе. Поскольку при вызове метода ServiceHelper мы получаем id, мы всегда можем узнать, выполняется команда или нет. Подробней об этом — чуть далее в статье.
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; }
Теперь пример public метода, который будет выполнять какое-то действие на нашем сервисе:
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); }
и вот таких методов, торчащих наружу будет ровно столько, сколько команд поддерживает ваш сервис.
Activity
Итак, как я уже сказал, у нас есть интерфейс:
public interface ServiceCallbackListener { void onServiceCallback(int requestId, Intent requestIntent, int resultCode, Bundle data); }
Я считаю, что удобно в базовой абстрактной активити реализовывать сей интерфейс. Тогда в конкретных активити Вам надо будет всего лишь переопределить метод onServiceCallback для получения уведомлений, что очень похоже на стандартные callback методы в activity, т. е. грациозно вписывается в Ваш клиентский код.
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) { } }
Обратите внимание, как активити подписывается и отписывается от ServiceHelper в своих методах onResume/onPause. Это позволяет избегать проблем при пересоздании активити, например, при повороте экрана, или сворачивании приложения.
Давайте рассмотрим, что нам приходит в метод onServiceCallback:
- requestId — уникальный идентификатор, сгенерированный при отправке запроса
- requestIntent — оригинальный интент, который мы послали
- resultCode — код результата выполнения
- resultData — данные
Теперь, у нас есть все необходимое, чтобы в нашей activity мы всегда могли получить уведомление от нашего сервиса без кучи boilerplate кода.
Более того, что мне кажется очень полезным — мы можем идентифицировать как конкретный запрос по ID, так и все запросы одного типа по action, что дает нам огромную гибкость.
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()) { //обработка по типу } if (requestId == superRequestId) { //обработка конкретного запроса } } }
Также, имея ID запроса, мы можем выполнять отложенную проверку. Представим последовательность:
- пользователь запустил действие, запустилась крутилка
- закрыл приложение на 2 минуты
- действие уже выполнилось
- пользователь открыл снова
- тут мы проверяем в onResume, выполнилась ли операция, и убираем крутилку
т. е., просто вызываем getServiceHelper().isPending(requestId), если нам это нужно.
Заключение
Вот, пожалуй, и все.
Сразу скажу, что я не претендую на универсальность данной архитектуры или на какую-то ее уникальность. Она была медленно выведена мной путем проб и ошибок, просмотров различных материалов и т. п. Но, пока, все мои нужды в настоящих коммерческих проектах она покрывает на 100%.
Более того, если чего-от не хватает — ее можно легко расширить. Из очевидного:
- добавить помимо success и failure код progress, тогда с сервиса можно будет передавать информацию о прогрессе задачи и отображать ее в, скажем ProgressBar
- прикрутить код по прерыванию выполняемой задачи
- и т. п.
Еще одна деталь, у меня код ServiceHelper не синхронизирован, т. к. подразумевается, что его методы будут вызываться в UI thread всегда. Если у Вас это не так, то необходимо добавить синхронизацию при любом изменении состояния ServiceHelper.
В общем, спасибо за внимание, надеюсь, кому-то поможет. Готов отвечать на Ваши вопросы и замечания в комментах.
UPD: Выложил маленький sandbox примерчик, иллюстрирующий архитектуру на GitHub: https://github.com/TheHiddenDuck/android-service-arch
UPD 2: Добавил пример реализации сервиса, работающего на пуле потоков
