Организация архитектуры взаимодействия Activity и Service

    Приветствую!

    Сегодня я решил поведать Вам мой способ организации 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: Добавил пример реализации сервиса, работающего на пуле потоков
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 37

      +1
      Спасибо, интересно. А как вообще подобная проблема решается другими? Есть же open source приложения где тоже надо на сервер ходить.
        +1
        Спасибо за вопрос. Я сильно не вдавался во многие open source приложения, но в них все по-разному, в зависимости от качества. Где-то люди используют AsyncTask, где-то броадкаст ресиверы, где-то — что-то похожее на описанное в статье. Тут, поймите, проблема не в самом походе в сервер, а именно в организации связанной архитектуры в своем приложении, где надо ходить на сервер.
          +3
          На Google IO был доклад на эту тему, www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.html, кстати там использовалась похожая структура.
            +1
            Да, этот доклад мне в свое время очень помог, всем рекомендую. Проблема этого доклада, что там совершенно не описывались детали обратного взаимодействия сервиса с хелпером, т. е. он сказал «binder callback», а что это такое, как его писать — нет.
            0
            У меня, например, есть класс для запроса API, в котором реализованы методы для выполнения запроса с вызовом callback на главном потоке/на фоновом/и.т.д. (в общем, аналог AsyncTask, но самописный), на каждый возможный запрос — свой наследующийся от него класс, который задает параметры, парсит ответ от сервера и выдает в callback уже готовые объекты с данными.
              0
              Я так раньше делал, пытался выделять каллбэки для подобного рода библиотек. Сейчас только три пакета на подобную библиотеку: запросы, просто возвращают прямо, синхронно, String — ответ, буть то json, xml или что-то другое, потом другой пакет:Objects и Responses, который берут эту String и выдают java Objects с полями, ну и главный пакет, который просто имееть какие константы обще доступные и *MyLIB*Exception, который используется везде в библиотеке, а так же для обработки ответа с ошибкой. Думаю так лучше, а callback для приложения оставить, в каждом приложении может иметь смысл по разному вызывать синхронные, долговременные операции, буть то поток, буть то очередь или как то иначе, да и callback может быть разный, listener или intent и др.
            0
            А так ли необходимо использовать Intent'ы если и Activity и сервис находятся в одном приложении (т.е. исполняются в одном процессе)?
            Activity ведь может получить ссылку на инстанс сервиса через биндинг. Также активити может быть уведомлена об остановке сервиса, что даст возможность корректно подчищать ссылки. Таким образом мы получим связь активити->сервис.
            Ну а имея такую связь уже можно создать листенер для сервиса, и подписать активити на уведомления сервиса, получив таким образом обратную связь сервис->активити.
            В результате мы получим быстрый, простой и прозрачный механизм взаимодействия (я не упомянул об механизме обработки сообщений сервисом — при такой схеме можно делать как заблагорассудиться — как вариант создать специальный рабочий поток и в нем через Handler уже обрабатывать команды).
              0
              По поводу биндинга сервисов: stackoverflow.com/questions/4908267/communicate-with-activity-from-service-localservice-android-best-practices

              Крутой спец по андроиду CommonsWare считает их неоправданными для этой цели и советует их не использовать:
              I wouldn't. Use the loosest possible coupling you can stand. Hence, on average, aim for the command pattern with startService() instead of the binding pattern with bindService().


              Для себя решил использовать для связи:
              Activity -> Service: Intent
              Service -> Activity: LocalBroadcastManager
                0
                А вообще, как тут писал он же, есть три способа взаимодействия с сервисами:
                1. с помощью интентов;
                2. с помощью AIDL;
                3. используя непосредственно объект сервиса (как сингтон).


                AIDL сложен и имеет смысл его использовать только если сервис находится в другом процессе (Remote). Для локальных сервисов остаются варианты 1 и 3.
                  0
                  Мне кажется остается вариант 1, потому что 3й это уж слишком конкретно. Я имею ввиду, что вы конкретно говорите, вот имя класса, вот метод, и он должен что-то вернуть (instance). Тут сразу минусы видны и не кто не гарантирует что сервис был создан, что он сейчас запущен, что ссылка через singleton будет актуальна. В свою очередь Intent, это не привязанность к какому либо классу, просто описание правил как читать поля и что с ними делать, тут только плюсы, можно даже поменять или удалить сервис, часть кода с Intent не пострадает.
                    0
                    Посмотрите мой ответ ниже — все хорошо там получается.
                  0
                  Не очень хорошо этот спец советует использовать синглтон для сервиса и метод ожидания запуска сервиса.
                  Вместо синглтона нужно использовать класс Binder, возвращающий ссылку на инстанс сервиса, а вместо ожидания пока MyService.getInstance() возвращает null, использовать ServiceConnection.
                  Пример можно посмотреть например здесь: http://stackoverflow.com/questions/5731387/bindind-to-the-same-service-instance-in-android
                    0
                    Да, это один из нормальных путей общения с сервисом. Так же этот вариант гарантирует интересующимуся когда сервис доступен для отсылки Intent, ну или же вызова метода через Binder. Согласен полностью, зависит от задачи.
                  +1
                  Во-первых, как я уже сказал, я не претендую, что это — единственный верный способ. Я использую интенты, т. к., во-первых, это самый рекомендованный и стандартный способ посылки сообщений сервису, во-вторых, это самый простой, не требующий boilerplate кода способ. Локальный биндинг — вещь в целом неплохая, но он привязывает сервис к жизненному циклу активити, напрямую связывает их, требует отслеживания событий биндинга/анбиндинга и т. п. Я вообще восхищаюсь системой сообщений в андроид, и считаю, что по возможности лучше придерживаться ее. Кроме того, используя способ в статье, вы сможете очень легко перенести сервис в другой процесс, если надо (одной строчкой в манифесте). ResultReceiver бьет через процессы, все работает, а вот локальный биндинг тут уже придется менять на AIDL или Messenger, что не так тривиально.

                  По-поводу делать как хотим в сервисе — а сейчас разве не так? В данном примере у меня IntentService, он работает через Handler. Если надо, можно заменить на свой сервис, который будет работать, скажем на ExecutorService, все точно так же.
                    0
                    Я также ни на что не претендую. Но считаю приведенную мной выше информацию желательной для ознакомления при рассмотрении методов взаимодействия активити и сервиса.

                    По поводу Вашего кода — я не вникал в детали и не заметил, что там используется Handler. Все таки готовый пример был бы более наглядным.

                    [offtopic]У Вас новый Handler создается в потоке вызывающего кода (т.е. UI поток активити скорее всего) — я правильно понимаю? Если так, то обрабатываться Handler будет в этом-же потоке… Поправьте если я не прав[/offtopic]
                      +1
                      В детали не вникали, а минусовать, смотрю, горазды. В моем коде используется IntentService, который основан внутри на хендлере. Этот хендлер создается на лупере WorkerThread'a. Ознакомьтесь с исходниками IntentService.
                        +1
                        Мне кажется r_ii про хэндлер, который передается в ResultReceiver в конструктор, он там действительно для того, чтобы на нем вызывать onReceiveResult, поэтому onReceiveResult будет выполнен в том же потоке, из которого вызвался createIntent. А вообще каждый раз создавать новый Handler там тоже не нужно :)
                          +2
                          Если про этот, то да, ответ выполняется в UI потоке, и так и должно быть, т. к. уже непосредственно будет модифицироваться UI. По-поводу хендлера, да, Вы правы, каждый раз создавать новый инстанс бессмысленно, мой недочет. Ничего смертельного, конечно, но ненужные объекты, в общем-то не нужны :)
                            0
                            Ну в таком случае оффтопик можно считать закрытым. Спасибо за прояснение ситуации.
                            P.S. пример с исходниками сильно помог бы вникнуть в суть.
                      0
                      ResultReceiver бьет через процессы

                      Только если свое приложение работает в нескольких процессах. Иначе будет падать эксепшн в intent.getParcelableExtra, т.к. класс такой не найдет.
                        +2
                        Да, но когда я говорил, о том, что нужно в манифесте своего приложения добавить одну строчку, я именно об одном приложении и говорил, очевидно :) О взаимодействии разных package-ей — другая история, и у меня как раз назревает статья о том, как я делал плагин, играющий аудио семплы к своему MIDI приложению. Там как раз отдельный APK, другой пакет процесс, и т. п.
                    0
                    В книге «Рето Майер — Android 2. Программирование приложений» неплохо описаны состояния activity и взаимодействие с сервисами. Советую почитать, как дополнение к статье.
                      0
                      спасибо за статью. планировал написать похожую, т.к. часто использую аналогичную архитектуру. и вот вы меня избавили от такой необходимости :)

                      вопрос по поводу расширения
                      «прикрутить код по прерыванию выполняемой задачи»

                      Какого либо элегантного способа прерывания IntentService я не нашел. stopService() приводит к вызову onDestroy() сервиса, однако working tread продолжает исполнение до полного завершения обработки интента.
                      Не поделитесь опытoм как вы реализовали или планировали реализовать прерывание? спасибо.
                        +1
                        С интент сервисом особо элегантного способа и нет. Однако, простейшее прерывание можно организовать и на нем. Как я себе вижу:

                        Во-первых, командам добавляем флаг isCancelled и проверяем его во время выполнения команды, если он == true, немедленно прекращаем все действия. Т. к. в интент сервисе мы всегда знаем, какая команда выполняется, мы можем просто выставить ей этот флаг. Для этого просто оверрайдим onStartCommand(...) и, если приходит ACTION_CANCEL, тормозим текущую команду, иначе super.onStartCommand(...). Разумеется, если команда уже выполняется и, скажем, в середине HTTP запроса на 10 секунд (что тоже не фонтан, конечно, но бывает), то дождаться ее завершения в любом случае придется, ничего не поделаешь.

                        Если нужны более продвинутые прерывания, лучше всего сделать свой сервис и использовать пул потоков (ExecutorService). Там у нас всегда будет future выполняемых тасков и мы можем интерраптнуть выполнение. Организовать очередь по-прежнему можно при помощи newFixedThreadPool, а, если надо — всегда можем держать несколько потоков. Единственное — в этой ситуации (параллельное выполнение) лучше избегать stopService, а использовать именно stopSelf, когда завершает выполнение последняя задача завершится. Лично я предпочитаю всегда добавлять cooldown на минутку прежде, чем окончательно убивать сервис. Если за минутку приходит новая таска, кулдаун сбрасывается, и мы избегаем лишних завершений и стартов сервиса. Это легко делается Handler'ом.
                          0
                          спасибо. пойду читать про ExecutorService
                        0
                        Спасибо!
                        Давно пытаюсь унифицировать способ работы с вебсервисами.
                        GoogleIO про работу с веб сервисами + ioshed дало стандарт, но все-равно сложно до конца все понять в их app.
                        Было бы очень круто, если бы вы выложили или дали ссылочку на код программы, где реализован ваш подход.
                        Может у вас есть некий макет, которым вам не жалко поделиться.
                          +2
                          Код всей программы выложить не могу, т. к. это коммерческий продукт. В общем, код приведенный в статье содержит все основные элементы фреймворка, используемого в программе (с измененными именами переменных, но смыслом тем же), осталось собрать в кучу. Я постараюсь, как время будет, слепить простенький примерчик, основанный на этом подходе.
                            0
                            Да, я понимаю.
                            Примерчик было бы очень суперово)
                              +1
                              Готов примерчик, ссылка в конце статьи.
                                0
                                Спасибо. Смотрю
                            0
                            в ioschede вроде все даже проще. Там есть один SyncService, который выполняет последовательно синхронизацию данных из разных источников. То есть берет из источника и пишет в ContentProvider.
                            Там последовательное выполнение операций, а в данной архитектуре я так понимаю можно запустить параллельно несколько действий, что бывает удобно.
                            0
                            А может можно проще? Наследовать и раширить объект Application. Вставить в него видимые ВСЕМ поля — и сервисам, и активностям. И использовать эти поля в синхронизированном режиме и через них обмениваться? Чем плох такой подход?
                              0
                              В синхронности?
                              0
                              Как говорит документация All requests are handled on a single worker thread — they may take as long as necessary (and will not block the application's main loop), but only one request will be processed at a time. То есть предложенный подход не позволяет выполнять 2 и более запросов одновременно. Верно?
                                0
                                Если включить мозг и понять, что предложенный подход нигде ничем не обязывает использовать IntentService, то можно прийти к выводу, что нет, позволяет, только нужно реализовать свой сервис, скажем, с ExecutorService.
                                0
                                только нужно реализовать свой сервис, скажем, с ExecutorService
                                Именно так я и сделал.

                                Спасибо за совет, даже не смотря на форму, в которой он был дан.
                                  0
                                  Рад, что смог помочь. Сорри за форму, так получилось, что писал коммент, находясь в не самом лучшем настроении. Как извинение держите плюсик.

                                Only users with full accounts can post comments. Log in, please.