- 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.beans
Observable
и Observer
из пакета java.util
BaseObservable
и 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
нужно:
ticket
TicketSubsytem
(чтобы обновить вид, когда появится 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/
Нажмите здесь для печати.