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

в 14:37, , рубрики: android, android development, Разработка под android, метки: ,

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

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

Поэтому я методом проб и ошибок пришел к архитектуре, лично для меня покрывающей все необходимые вопросы.

Об этом методе я буду рассказывать далее.

С высоты птичьего полета

Давайте сначала рассмотрим высокоуровневую картину предлагаемой архитектуры.
Организация архитектуры взаимодействия Activity и Service
Далее в статье я буду использовать два термина — управляемая и неуправляемая обратная связь. Это неофициальные термины, но я их буду использовать, т. к. они мне нравятся. Управляемые — это уведомления, осуществляемые платформой 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()) {
	    <hh user=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.

В общем, спасибо за внимание, надеюсь, кому-то поможет. Готов отвечать на Ваши вопросы и замечания в комментах.

Автор: evilduck

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js