Приветствую!
Сегодня я решил поведать Вам мой способ организации 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: Добавил пример реализации сервиса, работающего на пуле потоков