- PVSM.RU - https://www.pvsm.ru -
В одной статье на хабре (274635 [1]) было продемонстрировано любопытное решение для передачи объекта из onSaveInstanceState в onRestoreInstanceState без сериализации. Там используется метод writeStrongBinder(IBInder) класса android.os.Parcel.
Такое решение корректно функционирует до тех пор, пока Android не выгрузит ваше приложение. А он вправе это сделать.
…system may safely kill its process to reclaim memory for other foreground or visible processes…
(http://developer.android.com/intl/ru/reference/android/app/Activity.html [2])
Однако это не главное. (Если приложению не нужно восстанавливать свое состояние после такого рестарта, то подойдет и это решение).
А вот цель, для чего там используются такие «несериализуемые» объекты мне показалась странной. Там через них передаются вызовы из асинхронных операций в Activity, чтобы обновить отображаемое состояние приложения.
Я всегда думал, что со времен Smalltalk, любой разработчик распознает эту типовую задачу проектирования. Но кажется я оказался не прав.
onClick()) запустить асинхронную операциюActivityОсобенности
Activity, отображаемая в момент завершения операции, может оказаться
Activty из приложения не отображается
В последнем случае результаты должны отображаться при следующем открытии Activity.
MVC (с активной моделью) и Layers.
Вся остальная часть статьи — это объяснение что такое MVC и Layers.
Поясню на конкретном примере. Пусть нам необходимо построить приложение «Электронный билет в электронную очередь».
Получение билета от сервера я сделаю с помощью асинхронной операции. Также асинхронными операциями будут считывание билета из файла (после перезапуска) и удаление файла.
Построить такое приложение можно из несложных компонентов. Например:
TicketSubsystem)TicketActivity где будет отображаться билет и кнопка «Взять билет»Самое интересное то, как эти компоненты взаимодействуют.
Приложение вовсе не обязано содержать компонент TicketSubsystem. Билет мог бы находиться
в статическом поле Ticket.currentTicket, или в поле в классе-наследнике android.app.Application.
Однако очень важно, чтобы состояние есть/нет билета исходило из объекта способного выполнять роль
Модель из MVC — т. е. генерировать уведомления при появлении (или замене) билета.
Если сделать TicketSubsystem моделью в терминах MVC, то Activity сможет подписаться на события и обновить отображение билета когда тот будет загружен. В этом случае Activity будет выполнять роль View (Представление) в терминах MVC.
Тогда асинхронная операция «Получение нового билета» сможет просто записать полученный билет в TicketSubsystem и больше ни о чем не заботиться.
Очевидно, что моделью должен являться билет. Однако в приложении билет не может «висеть» в воздухе. Кроме того, билет изначально не существует, он появляется только по завершению асинхронной операции. Из этого следует, что в приложении должно быть еще что-то где будет находиться билет. Пусть это будет TicketSubsystem. Сам билет также должен быть как-то представлен, пусть это будет класс Ticket. Оба этих класса должны быть способны выполнять роль активной модели.
Активная модель — модель оповещает представление о том, что в ней произошли изменения. wikipedia [3]
В java есть несколько вспомогательных классов для создания активной модели. Вот например:
PropertyChangeSupport и PropertyChangeListener из пакета java.beansObservable и Observer из пакета java.utilBaseObservable и Observable.OnPropertyChangedCallback из android.databinding
Мне лично нравится третий способ. Он поддерживает строгое именование наблюдаемых полей, благодаря аннотации android.databinding.Bindable. Но есть и другие способы, и все они подходят.
А в Groovy есть замечательная аннотация groovy.beans.Bindable [4]. Вместе с возможностью краткого объявления свойств объекта получается очень лаконичный код (который опирается на PropertyChangeSupport из java.beans).
@groovy.beans.Bindable
class TicketSubsystem {
Ticket ticket
}
@groovy.beans.Bindable
class Ticket {
String number
int positionInQueue
String tellerNumber
}
TicketActivity (как практически все объекты относящиеся к представлению) появляется и исчезает по воле пользователя. Приложение всего лишь должно корректно отображать данные в момент появления Activity и при изменении данных пока отображается Activity.
Итак в TicketActivity нужно:
ticketTicketSubsytem (чтобы обновить вид, когда появится ticket)
В примерах в статье я буду использовать PropertyChangeListener из java.beans ради демонстрации
подробностей. А в исходном коде по ссылке внизу статьи будет использоваться библиотека android.databinding,
как обеспечивающая самый лаконичный код.
PropertyChangeListener ticketListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
updateTicketView();
}
};
void updateTicketView() {
TextView queuePositionView = (TextView) findViewById(R.id.textQueuePosition);
queuePositionView.setText(ticket != null ? "" + ticket.getQueuePosition() : "");
...
}
PropertyChangeListener ticketSubsystemListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
setTicket(ticketSubsystem.getTicket());
}
};
void setTicket(Ticket newTicket) {
if(ticket != null) {
ticket.removePropertyChangeListener(ticketListener);
}
ticket = newTicket;
if(ticket != null) {
ticket.addPropertyChangeListener(ticketListener);
}
updateTicketView();
}
Метод setTicket при замене билета удаляет подписку на события от старого билета и подписывается на события от нового билета. Если вызывать setTicket(null), то TicketActivity отпишется от событий ticket.
void setTicketSubsystem(TicketSubsystem newTicketSubsystem) {
if(ticketSubsystem != null) {
ticketSubsystem.removePropertyChangeListener(ticketSubsystemListener);
setTicket(null);
}
ticketSubsystem = newTicketSubsystem;
if(ticketSubsystem != null) {
ticketSubsystem.addPropertyChangeListener(ticketSubsystemListener);
setTicket(ticketSubsystem.getTicket());
}
}
@Override
protected void onPostCreate(@Nullable Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
setTicketSubsystem(globalTicketSubsystem);
}
@Override
protected void onStop() {
super.onStop();
setTicketSubsystem(null);
}
Код получается довольно простым и прямолинейным. Но без использования специальных инструментов приходится писать довольно много однотипных операций. Для каждого элемента в иерархии модели приходится заводить поле и создавать отдельный слушатель.
Код асинхронной операции тоже довольно простой. Основная идея в том, чтобы по завершению асинхронной операции записывать результаты в Модель. А Представление обновится по уведомлению из Модели.
public class GetNewTicket extends AsyncTask<Void, Void, Void> {
private int queuePosition;
private String ticketNumber;
@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(TimeUnit.SECONDS.toMillis(2));
Random random = new Random();
queuePosition = random.nextInt(100);
ticketNumber = "A" + queuePosition;
// TODO записать данные билета в файл, чтобы можно было
// его загрузить после перезапуска приложения.
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
Ticket ticket = new Ticket();
ticket.setNumber(ticketNumber);
ticket.setQueuePosition(queuePosition);
globalTicketSubsystem.setTicket(ticket);
}
}
Здесь ссылка globalTicketSubsystem (она также упоминалась в TicketActivity) зависит от способа компоновки подсистем в вашем приложении.
Допустим, что пользователь нажал кнопку «Взять билет», приложение послало запрос на сервер, а в это время случился входящий звонок. Пока пользователь отвечал на звонок, пришел ответ от сервера, но пользователь об этом не знает. Мало того, пользователь нажал «Home» и запустил какое-нибудь приложение, которое сожрало всю память и системе пришлось выгрузить наше приложение.
И вот наше приложение должно отобразить билет полученный до рестарта.
Чтобы обеспечить эту функциональность я буду записывать билет в файл и считывать его после старта приложения.
public class ReadTicketFromFileextends AsyncTask<File, Void, Void> {
...
@Override
protected Void doInBackground(File... files) {
// Считываем из файла в number, positionInQueue, tellerNumber
}
@Override
protected void onPostExecute(Void aVoid) {
Ticket ticket = new Ticket();
ticket.setNumber(number);
ticket.setPositionInQueue(positionInQueue);
ticket.setTellerNumber(tellerNumber);
globalTicketSubsystem.setTicket(ticket);
}
}
Этот шаблон определяет правила по которым одним классам позволяется зависеть от других классов, так чтобы не возникало чрезмерной запутанности кода. Вообще это семейство шаблонов, а я ориентируюсь на вариант Крейга Лармана из книги «Применение UML и шаблонов проектирования». Вот здесь есть диаграмма [5].
Основная идея в том, что классам с нижних уровней нельзя зависеть от классов с верхних уровней. Если разместить наши классы по уровням Layers, то получится примерно такая диаграмма:

Обратите внимание, что все стрелочки, что пересекают границы уровней, направлены строго вниз! TicketActivity создает GetNewTicket — стрелка вниз. GetNewTicket создает Ticket — стрелка вниз. Анонимный ticketListener реализует интерфейс PropertyChangeListener — стрелка вниз. Ticket оповещает слушателей PropertyChangeListener — стрелка вниз. И т. д.
То есть любые зависимости (наследование, использование в качестве типа члена класса, использование в качестве типа параметра или типа возвращаемого значения, использование в качестве типа локальной переменной) допустимы только к классам на том же уровне или на уровнях ниже.
Еще капельку теории, и перейдем к коду.
Объекты на уровне Domains отражают бизнес-сущности с которыми работает приложение. Они должны быть независимы от того как устроено наше приложение. Например наличие поля positionInQueue у Ticket обусловлено бизнес требованиями (а не тем, как мы написали наше приложение).
Уровень Application — это граница того, где может располагаться логика приложения (кроме формирования внешнего вида). Если нужно сделать какую-то полезную работу, то код должен оказаться здесь (или ниже).
Если нужно сделать что-то обладающее внешним видом, то это класс для уровня Presentation. А значит этот класс может содержать только код отображения, и никакой логики. За логикой ему придется обращаться к классам с уровня Application.
Принадлежность класса к определенному уровню Layers — условна. Класс находится на заданном уровне до тех пор пока выполняет его требования. То есть в результате правки класс может перейти на другой уровень, или стать непригодным ни для одного уровня.
Как определить на каком уровне должен находиться заданный класс? Я поделюсь скромной эвристикой, а вообще рекомендую изучить доступную теорию. Начинайте хоть здесь [6].
Эвристика
В репозитории https://github.com/SamSoldatenko/habr3 [7] находится описанное здесь приложение, построенное с применением android.databinding и roboguice. Посмотрите код, а здесь я кратко объясню какой выбор я делал и по каким причинам.
com.android.support:appcompat-v7 добавлена потому что коммерческие разработки опираются на эту библиотеку для поддержки старых версий android.
com.android.support:support-annotations добавлена для использования аннотации @UiThread (там много других полезных аннотаций).
org.roboguice:roboguice — библиотека для внедрения зависимостей. Используется чтобы компоновать приложение из частей с помощью аннотаций Inject [8]. Также эта библиотека позволяет внедрять ресурсы, ссылки на виджеты и содержит механизм пересылки сообщений похожий на CDI Events из JSR-299.
TicketActivity c помощью аннотации @Inject получает ссылку на TicketSubsystem.ReadTicketFromFile с помощью аннотации @InjectResource получает имя файла из ресурсов, из которого нужно загрузить билет.TicketSubsystem с помощью @Inject получает Provider который использует чтобы создать ReadTicketFromFile.
org.roboguice:roboblender создает базу данных всех аннотаций для org.roboguice:roboguice во время компиляции, которая затем используется во время выполнения.
app/lint.xml с настройками для подавления предупреждений от библиотеки roboguice.
dataBinding в app/build.gradle разрешает специальный синтаксис в layout файлах похожий на Expression Language (EL) и подключает пакет android.databinding, который используется чтобы сделать Ticket и TicketSubsystem активной моделью. В результате код представлений сильно упрощается и заменяется на декларации в layout файле. Например:
<TextView
...
android:text="@{ts.ticket.number}"
/>
.idea внесена в .gitignore чтобы использовать любые версии Android Studio или IDEA. Проект отлично импортируется и синхронизируется через файлы build.gradle.
gradlew, gradlew.bat и папка gradle). Это очень эффективный и удобный механизм.
unitTests.returnDefaultValues = true в app/build.gradle. Это компромисс между защищенностью от случайных ошибок в модульных тестах и краткостью модульных тестов. Здесь я отдал предпочтение краткости модульных тестов.
org.mockito:mockito-core используется для создания заглушек в модульных тестах. Кроме того эта библиотека позволяет описать «System Under Test» с помощью аннотаций @Mock и @InjectMocks. При использовании Dependency Injection компоненты «ожидают» что перед их использованием им будут внедрены зависимости. Перед тестами также требуется внедрить все зависимости. Mockito умеет создавать и внедрять заглушки в тестируемый класс. Это очень упрощает код тестов, особенно если внедряемые поля имеют ограниченную видимость. См. GetNewTicketTest.
Mockito, а не Robolectric?
org.powermock:powermock-module-junit и org.powermock:powermock-api-mockito. Некоторые вещи не удается заменить заглушками. Например подменить статический метод или подавить вызов метода базового класса. Для этих целей PowerMock подменяет загрузчик классов и правит байт-код. В TicketActivityTest с помощью PowerMock подавляется вызов RoboActionBarActivity.onCreate(Bundle) и задается возвращаемое значение из вызова статического метода DataBindingUtil.setContentView
LogInterface log не статическая?
LogInterface и LogImpl которые всего лишь потомки похожих классов из RoboGuice?@ImplementedBy(LogImpl.class).
@UiThread у классов Ticket и TicketSubsystem?onPropertyChanged которые используются в UI компонентах чтобы обновить отображение. Необходимо гарантировать что вызовы будут производиться в UI потоке.
TicketSubsystem?TicketSubsystem (создается всего одна копия, т. к. он помечен аннотацией @Singleton). Однако в конструкторе TicketSubsystem нельзя создать ReadTicketFromFile, так как ему нужна ссылка на еще не созданный TicketSubsystem. Поэтому создание ReadTicketFromFile откладывается на следующий цикл UI потока.
adb shell am kill ru.soldatenko.habr3
Спасибо
Автор: SamSol
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/117531
Ссылки в тексте:
[1] 274635: https://habrahabr.ru/post/274635/
[2] http://developer.android.com/intl/ru/reference/android/app/Activity.html: http://developer.android.com/intl/ru/reference/android/app/Activity.html
[3] wikipedia: https://ru.wikipedia.org/wiki/Model-View-Controller
[4] groovy.beans.Bindable: http://docs.groovy-lang.org/2.3.2/html/api/index.html?groovy/beans/Bindable.html
[5] Вот здесь есть диаграмма: http://csis.pace.edu/~marchese/CS616/Lec11/se_l11.htm
[6] здесь: https://en.wikipedia.org/wiki/Multilayered_architecture
[7] https://github.com/SamSoldatenko/habr3: https://github.com/SamSoldatenko/habr3
[8] Inject: https://habrahabr.ru/users/inject/
[9] Источник: https://habrahabr.ru/post/281290/
Нажмите здесь для печати.