В blueBill Mobile для Android (кстати,
я опубликовал первую версию на Android Market ) мне пришлось столкнуться с проблемой внутреннего повторного использования. Рассмотрим следующую диаграмму, которая изображает пару возможных «экранных» потоков, по которым пользователь может перемещаться:
В Android каждый «экран» называется Activity. Приложение запускается с ObservationActivity, которое показывает список записанных наблюдений. Нажав кнопку, пользователь может вставить новую. Эта задача выполняется с помощью волшебного потока, описанного верхней последовательностью на диаграмме:
- В конечном итоге выберите вид птицы из списка истории (содержащего самые последние выборки) (PickTaxonActivity).
- Кроме того, пользователь может нажать кнопку и перейти на новый экран, где можно просмотреть таксономию и перейти к одному виду (TaxonBrowserActivity).
- Затем вводится количество наблюдаемых птиц и их пол (CountAndGenderActivity). Нажав кнопку «Добавить еще», пользователь может перейти к циклу и вернуться к вводу нового вида.
- Вместо этого, нажав кнопку «ОК», конечная активность PickLocationActivity позволяет ввести местоположение наблюдения.
- В конце пользователь возвращается к ObservationActivity.
Во второй функции пользователь может захотеть вспомнить информационный бюллетень о видах птиц с информацией, мультимедиа (фотографии и звуки) и т. Д. Имеет смысл повторно использовать те же два действия (PickTaxonActivity и TaxonBrowserActivity) и после выбора было сделано, чтобы добраться до FactSheetActivity:
- В конечном итоге выберите вид птицы из списка истории (содержащего самые последние выборки) (PickTaxonActivity).
- Кроме того, пользователь может нажать кнопку и перейти на новый экран, где можно просмотреть таксономию и перейти к одному виду (TaxonBrowserActivity).
- Затем отображается FactSheetActivity.
Как можно перемещаться по действиям в Android? С помощью асинхронного средства передачи сообщений, называемого «намерение». По сути, одно действие создает и отправляет сообщение среде выполнения, которая реагирует, выводя на экран получатель действия сообщения, которое активируется.
Сообщения могут быть основаны на своего рода шаблоне «публиковать и подписываться» (они несут «тему», и соответствующая активность найдена, поскольку она объявлена компетентной в данной теме в манифесте приложения):
Intent intent = new Intent("topic");
// eventually put extra information into the intent
startActivity(intent);
В качестве альтернативы можно явно указать получателя, ссылаясь на его класс (в этом случае у нас есть система обмена сообщениями «точка-точка»):
Intent intent = new Intent();
intent.setClass(..., NextActivity.class);
// eventually put extra information into the intent
startActivity(intent);
Сообщение особого вида генерируется путем вызова метода finish (), который уведомляет, что Activity завершает свою задачу, поэтому элемент управления возвращается отправителю сообщения.
Кроме того, Android отслеживает поток и реализует стек недавно посещенных операций, поэтому кнопка «назад» обычно работает автоматически без необходимости дополнительного кода; сообщение «финиш» сбрасывает стек до точки вызова.
Я большой поклонник паттерна «публикуй и подписывайся», потому что он отлично справляется с разделением различных частей дизайна. В Android механизм очень мощный, потому что различные действия не обязательно должны быть частью одного и того же приложения: очень легко объединить несколько приложений. Например, отправка Намерения с заранее определенной темой может вызвать клиент электронной почты или список контактов телефона и т. Д.
К сожалению, часто вам нужен вариант обмена сообщениями «точка-точка», когда вы работаете внутри приложения, так как вам требуется определенная последовательность действий, как в моем вводном примере.
Теперь шаблон «точка-точка» очень взаимосвязан, потому что отправитель должен знать получателя. Другими словами, PickTaxonActivity и TaxonBrowserActivity должны знать, какой будет следующий запуск действия: CountAndGenderActivity (поток № 1) или FactShetActivity (поток № 2). Если бы я хотел использовать их в дальнейших сценариях, у меня был бы более сложный и сложный код с PickTaxonActivity и TaxonBrowserActivity в зависимости от ряда других операций, хотя в действительности все должно быть наоборот. Это признак плохого распределения ролей и обязанностей: действия должны быть сосредоточены только на своей задаче и ничего не знать о том, что происходит после.
Кто должен отвечать за определение последовательности? Возможным кандидатом может быть начальная ObservationsActivity, так как он знает, какая команда была вызвана пользователем, таким образом, он знает всю последовательность шагов, через которую нужно пройти. Итак, в первый раз я подумал о подходе, при котором каждое действие, когда оно завершается, всегда отправляет сообщение «конец», поэтому оно немедленно возвращается к ObservationsActivity; этот класс будет реализовывать логику для поиска следующего шага и запуска соответствующей операции.
К сожалению, таким образом вы полностью потеряете реализацию кнопки возврата на основе стека, предоставляемую Android, что является большой ошибкой. Чтобы сохранить его, каждое действие, когда оно завершается, должно продолжать отправлять сообщения вперед.
Самостоятельная маршрутизация сообщений
Элегантное решение, которое я нашел для работы, основано на сообщениях с автоматической маршрутизацией . Идея состоит в том, чтобы сделать сообщения достаточно умными, чтобы знать путь, по которому они должны идти (это может быть фиксированный путь или путь, основанный на правилах). Это означает, что когда действие завершается, оно делегирует следующее, что нужно сделать самому сообщению. Пути могут быть описаны путем расширения класса ControlFlow, как показано в этом простом примере:
public class TaxonFactSheetControlFlow extends ControlFlow
{
public TaxonFactSheetControlFlow()
{
startFrom(PickTaxonActivity.class);
when(PickTaxonActivity.class).completes().thenForwardTo(TaxonFactSheetActivity.class);
when(TaxonBrowserActivity.class).completes().thenForwardTo(TaxonFactSheetActivity.class);
}
}
Он должен быть достаточно читабельным, чтобы понять, что он реализует поток № 2 на нашей диаграмме. Чтобы запустить его, вызывающая активность вызывает:
ControlFlow.start(this, TaxonFactSheetControlFlow.class);
Указанный ControlFlow не только вызывает первое действие последовательности, но также связывает себя с намерением; таким образом, будет возможно получить его на следующих шагах.
Whenever an Activity completes, it calls:
ControlFlow.from(this).toNextStep(intent); // an intent can be used to carry extra information
The originally instantiated ControlFlow is retrieved and invoked to find the next Activity to activate.
In case of rule-based paths, it is possible to pass arguments to be evaluated by the ControlFlow; for instance, CountAndGenderActivity in flow#1 calls one of the two following code fragments reacting to the pressure of the «Ok» and «Add more» buttons:
ControlFlow.from(this).toNextStep();
ControlFlow.from(this).toNextStep(ADD_MORE);
The flow#2 is described by this class:
public class AddObservationControlFlow extends ControlFlow
{
private final Condition addMore = new Condition()
{
public boolean compute (final @Nonnull Object... args)
{
return Arrays.asList(args).contains(CountAndGenderActivity.ADD_MORE);
}
};
public AddObservationControlFlow()
{
startFrom(PickTaxonActivity.class);
when(PickTaxonActivity.class).completes().thenForwardTo(CountAndGenderActivity.class);
when(TaxonBrowserActivity.class).completes().thenForwardTo(CountAndGenderActivity.class);
when(CountAndGenderActivity.class).completesWith(addMore).thenForwardTo(PickTaxonActivity.class);
when(CountAndGenderActivity.class).completesWithout(addMore).thenForwardTo(PickLocationActivity.class);
}
}
As you can see, each transition can be associated with a condition which is function, for instance, of the arguments passed to toNextStep().
This solution is very scalable, as I can reuse existing Activities as many times as I want, by just creating new subclasses of ControlFlow which describe the new sequences to implement.
The code can be checked out from the Mercurial repository at https://kenai.com/hg/bluebill-mobile~android-src, tag dzone20100517.