Привет, Хабр! Представляю вашему вниманию перевод статьи ViewModels and LiveData: Patterns + AntiPatterns автора Jose Alcerreca.
Типичное взаимодействие объектов приложения, построенное с помощью Архитектурных Компонентов:
В идеале ViewModel не должна ничего знать про Android. Это улучшает тестируемость и модульность, снижает кол-во утечек памяти. Основное правило — в Вашей ViewModel не должно быть импортов android.* (за исключением вроде android.arch.*). Это относится и к Presenter.
Условные операторы, циклы и общие решения должны проводиться в ViewModel или прочих слоях приложения, не в активити или фрагментах. View обычно не подвергается unit-тестированию (кроме случаев, когда используется Robolectric), поэтому чем меньше строчек кода — тем лучше. View должны только отображать данные и посылать действия пользователя в ViewModel (или Presenter). Этот паттерн называется Passive View.
Скоуп ViewModels отличается от скоупа активити или фрагмента. Тем временем, как ViewModel инициализирована и работает, активити может пройти через несколько состояний жизненного цикла. ViewModel может ничего не знать о том, что активити и фрагменты были уничтожены и созданы.
ViewModels сохраняется при изменениях конфигурации:
Передача ссылки на View (активити или фрагмент) в ViewModel является серьезным риском. Предположим, ViewModel запросила данные из сети, и они придут чуть позже. В этот момент ссылка на View может быть уничтожена или активити может больше не отображаться — это приведет к утечке памяти, а возможно и к крэшу приложения.
Удобным способом дизайна презентационного слоя в Android является, когда View (активити или фрагмент) наблюдает (подписана на изменения в) ViewModel. Т.к. ViewModel не знает ничего про Android, она также не знает о том, как часто Android убивает View. У этого подхода есть несколько преимуществ:
Типичные подписки от активити или фрагмента:
Разделение ответственностей — всегда хорошая идея. Если Ваш ViewModel содержит в себе слишком много кода или имеет слишком много ответственностей, подумайте о том, что возможно следует:
Как видно в руководствах по архитектуре приложений, большинство приложений имеют несколько источников данных. Таких как:
Создать в прилжении слой данных, который ничего не знает о слое presentation — хорошая идея. Алгоритмы синхронизации кэша и базы данных не тривиальны. Поэтому рекомендуется создать класс репозитория, который послужит единственной точкой входа для борьбы с этой сложностью.
Если у вас несколько моделей данных и они достаточно сложны — имеет смысл добавления нескольких репозиториев.
Представьте следующее: Вы подписаны на обновления LiveData, предоставленной ViewModel, которая содержит список для отображения. Как View сможет отличить загруженные данные от сетевой ошибки или пустого списка?
Вы можете предоставить доступ к LiveData через ViewModel. К примеру, MyDataState может содержать информацию о том, корректно ли загружаются данные, загрузились ли окончательно или загрузка была прервана.
Вы можете обернуть данные в клас, который содержит в себе состояния и другие метаданные (например, сообщение о ошибке). См. класс Resource в предоставленных примерах.
Состояние активити это информация, которую потребуется пересоздать на экране если активити была уничтожена, или был убит содержащий ее процесс. Поворот экрана это самый очевидный случай и этот вопрос закрыт ViewModel. Состояние хранится в ViewModel в полной безопасности.
Однако восстановление состояния может понадобиться в других сценариях, в которых ViewModel была также уничтожена: когда ресурсы операционной системы заканчиваются, и она убивает процесс приложения.
Для эффективного сохранения и восстановления состояния UI используйте комбинацию сохранения данных, onSaveInstanceState() и ViewModel.
Для примера см.: ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders Events
Событие — это что-то, что произошло единожды. ViewModel предоставляет доступ к данным, но что насчет событий? К примеру, события навигации или показ сообщений в Snackbar являются действиями, которые выполняются лишь один раз.
Концепция События не вписывается в то, как LiveData хранит и воссоздает данные. Представим себе ViewModel с таким полем:
Активити подписывается на изменения этой ViewModel, и когда ViewModel заканчивает операцию должно появиться сообщение:
Активити получает значение и показывает Snackbar. Вроде всё работает.
Не смотря на это, если пользователь повернет телефон — будет создана новая активити, которая также подпишется на изменения ViewModel. Когда произойдет подписка на изменения в LiveData, активити немедленно получит старое значение и сообщение отобразится снова!
Для того, чтобы решить эту проблему в одном из наших примеров мы создали класс SingleLiveEvent (класс наследуется от LiveData). Он посылает только те обновления, которые произошли после подписки на изменения. Также обратите внимание, что класс поддерживает только одного подписчика.
Парадигма реактивного программирования отлично работает в Android, т.к. предоставляет удобную связь между UI и прочими слоями архитектуры приложения. LiveData является ключевым компонентом этой структуры, поэтому обычно Ваши активити и фрагменты будут подписаны на изменения инстансов LiveData.
То, как ViewModels будет сообщаться с прочими компонентами приложения — решать Вам, но остерегайтесь утечек памяти и пограничных состояний. В нижеприведенной диаграмме слой Presentation использует observer pattern и слой Data, который получает колбэки.
Observer pattern в UI и колбэки в слой Data:
Если пользователь выходит из прилжения, ViewModel перестает кем бы то ни было наблюдаться. Если репозиторий реализован как синглтон или каким-то другим образом привязан к скоупу приложения, он не будет уничтожен пока процесс приложения не будет убит. Это произойдет только когда системе потребуются ресурсы или пользователь вручную убьет прилжение. Если репозиторий ссылается на колбэк в ViewModel — будет создана утечка памяти.
Активити закончила работу, но ViewModel продолжает существовать:
Эта утечка не так важна, если ViewModel легкая или операция гарантированно завершится в течение короткого отрезка времени. Идеально ViewModel'и должны освобождаться во всех случаях, когда отсутствуют View, которые за ними наблюдают:
Этого можно достигнуть многими способами:
Чтобы избежать утечек в ViewModel и «ада колбэков», репозитории могут наблюдаться следующим образом:
Когда ViewModel очищена или когда жизненный цикл view прекратился, подписка очищается:
В этом способе есть одна тонкость: как подписаться к репозиторию от ViewModel, если нет доступа к LifecycleOwner? С помощью Трансформаций. Transformations.switchMap позволяет создавать LiveData, которая реагирует на изменения в других инстансах LiveData. За счет этого информация передается через observer жидненного цикла по цепочке:
Пример трансформации [ссылка]
В этом примере, когда триггер получает обновление, выполняется функция и результат каскадируется. Активити наблюдает за репозиторием и тот же LifecycleOwner будет использован для вызова repository.loadRepo(id).
Самый частый вид использования LiveData это использование MutableLiveData в ViewModel представление их как LiveData для того, чтобы сделать их неизменяемыми для наблюдателей.
Если Вам нужно больше функционала, наследование от LiveData позволит узнать, присутствуют ли активные наблюдатели. К примеру, это может быть полезно, если вы хотите начать прослушивать сервис локации или сеносоры устройства.
Вы также можете использовать onActive() для начала какого-то сервиса, который загружает данные, но если у Вас для этого нет хорошей причины, Вам не надо ждать, пока LiveData начнет наблюдаться. Самые распространненные паттерны:
Добавьте метод start() в ViewModel и вызовите его как можно скорее [см. пример с Blueprints]
Назначьте поле, которое прекращает загрузку [см. пример GithubBrowserExample].
View и ViewModel
Распределение ответственностей
Типичное взаимодействие объектов приложения, построенное с помощью Архитектурных Компонентов:
В идеале ViewModel не должна ничего знать про Android. Это улучшает тестируемость и модульность, снижает кол-во утечек памяти. Основное правило — в Вашей ViewModel не должно быть импортов android.* (за исключением вроде android.arch.*). Это относится и к Presenter.
ViewModel (и Presenter) не должны знать о классах фреймворка Android
Условные операторы, циклы и общие решения должны проводиться в ViewModel или прочих слоях приложения, не в активити или фрагментах. View обычно не подвергается unit-тестированию (кроме случаев, когда используется Robolectric), поэтому чем меньше строчек кода — тем лучше. View должны только отображать данные и посылать действия пользователя в ViewModel (или Presenter). Этот паттерн называется Passive View.
Активити и фрагменты должны содержать минимум логики
Ссылки на View в ViewModel
Скоуп ViewModels отличается от скоупа активити или фрагмента. Тем временем, как ViewModel инициализирована и работает, активити может пройти через несколько состояний жизненного цикла. ViewModel может ничего не знать о том, что активити и фрагменты были уничтожены и созданы.
ViewModels сохраняется при изменениях конфигурации:
Передача ссылки на View (активити или фрагмент) в ViewModel является серьезным риском. Предположим, ViewModel запросила данные из сети, и они придут чуть позже. В этот момент ссылка на View может быть уничтожена или активити может больше не отображаться — это приведет к утечке памяти, а возможно и к крэшу приложения.
Избегайте ссылок на View в ViewModels.Рекомендуемый способ коммуникации между ViewModel и View — использование observer pattern, при помощи LiveData или observable из других библиотек.
Observer Pattern (шаблон проектирования «Наблюдатель»)
Удобным способом дизайна презентационного слоя в Android является, когда View (активити или фрагмент) наблюдает (подписана на изменения в) ViewModel. Т.к. ViewModel не знает ничего про Android, она также не знает о том, как часто Android убивает View. У этого подхода есть несколько преимуществ:
- ViewModel сохраняются при изменении конфигурации, поэтому нет нужды в повторном запросе внешних источников данных (к примеру, базы данных или сетевого хранилища) при произведенном повороте устройства.
- Когда заканчивается какая-либо длительная операция, observable в ViewModel обновляются. Не важно, велось ли наблюдение за данными или нет. NPE не произойдет даже при попытке обновления несуществующего View.
- ViewModel не содержат ссылки на View, что снижает риск возникновения утечек памяти.
Типичные подписки от активити или фрагмента:
private void subscribeToModel() {
// Observe product data
viewModel.getObservableProduct().observe(this, new Observer<Product>() {
@Override
public void onChanged(@Nullable Product product) {
mTitle.setText(product.title);
}
});
}
Вместо того, чтобы отсылать данные в UI, пусть UI наблюдает за изменениями в данных.
«Жирные» ViewModel
Разделение ответственностей — всегда хорошая идея. Если Ваш ViewModel содержит в себе слишком много кода или имеет слишком много ответственностей, подумайте о том, что возможно следует:
- Переместить часть логики в Presenter, с тем же scope, что у ViewModel. Именно он будет связываться с другими частями приложения и обновлять LiveData в ViewModel.
- Добавить слой Domain и адаптировать приложение под Чистую Архитектуру. Это облегчит проведение тестов и поддержку кода. Также это обычно приводит к тому, что большая часть логики не выполняется в главном треде. С примером Чистой Архитектуры можно ознакомиться в Architecture Blueprints.
Распределяйте ответственности, добавьте слой domain если требуется.
Использовать репозиторий для данных
Как видно в руководствах по архитектуре приложений, большинство приложений имеют несколько источников данных. Таких как:
- Удаленный: сеть или облако
- Локальный: база данных или файл
- Кэш
Создать в прилжении слой данных, который ничего не знает о слое presentation — хорошая идея. Алгоритмы синхронизации кэша и базы данных не тривиальны. Поэтому рекомендуется создать класс репозитория, который послужит единственной точкой входа для борьбы с этой сложностью.
Если у вас несколько моделей данных и они достаточно сложны — имеет смысл добавления нескольких репозиториев.
Добавьте репозиторий данных в качестве едсинственной точки входа для данных
Обработка состояний данных
Представьте следующее: Вы подписаны на обновления LiveData, предоставленной ViewModel, которая содержит список для отображения. Как View сможет отличить загруженные данные от сетевой ошибки или пустого списка?
Вы можете предоставить доступ к LiveData через ViewModel. К примеру, MyDataState может содержать информацию о том, корректно ли загружаются данные, загрузились ли окончательно или загрузка была прервана.
Вы можете обернуть данные в клас, который содержит в себе состояния и другие метаданные (например, сообщение о ошибке). См. класс Resource в предоставленных примерах.
Expose информацию о состоянии данных, используя обертку или другой LiveData.
Сохраняя состояние активити
Состояние активити это информация, которую потребуется пересоздать на экране если активити была уничтожена, или был убит содержащий ее процесс. Поворот экрана это самый очевидный случай и этот вопрос закрыт ViewModel. Состояние хранится в ViewModel в полной безопасности.
Однако восстановление состояния может понадобиться в других сценариях, в которых ViewModel была также уничтожена: когда ресурсы операционной системы заканчиваются, и она убивает процесс приложения.
Для эффективного сохранения и восстановления состояния UI используйте комбинацию сохранения данных, onSaveInstanceState() и ViewModel.
Для примера см.: ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders Events
События
Событие — это что-то, что произошло единожды. ViewModel предоставляет доступ к данным, но что насчет событий? К примеру, события навигации или показ сообщений в Snackbar являются действиями, которые выполняются лишь один раз.
Концепция События не вписывается в то, как LiveData хранит и воссоздает данные. Представим себе ViewModel с таким полем:
LiveData<String> snackbarMessage = new MutableLiveData<>();
Активити подписывается на изменения этой ViewModel, и когда ViewModel заканчивает операцию должно появиться сообщение:
snackbarMessage.setValue("Объект сохранен!");
Активити получает значение и показывает Snackbar. Вроде всё работает.
Не смотря на это, если пользователь повернет телефон — будет создана новая активити, которая также подпишется на изменения ViewModel. Когда произойдет подписка на изменения в LiveData, активити немедленно получит старое значение и сообщение отобразится снова!
Для того, чтобы решить эту проблему в одном из наших примеров мы создали класс SingleLiveEvent (класс наследуется от LiveData). Он посылает только те обновления, которые произошли после подписки на изменения. Также обратите внимание, что класс поддерживает только одного подписчика.
Для таких событий, как показ сообщений в Snackbar или навигации используйте observable вроде SingleLiveEvent.
Утечки памяти в ViewModels
Парадигма реактивного программирования отлично работает в Android, т.к. предоставляет удобную связь между UI и прочими слоями архитектуры приложения. LiveData является ключевым компонентом этой структуры, поэтому обычно Ваши активити и фрагменты будут подписаны на изменения инстансов LiveData.
То, как ViewModels будет сообщаться с прочими компонентами приложения — решать Вам, но остерегайтесь утечек памяти и пограничных состояний. В нижеприведенной диаграмме слой Presentation использует observer pattern и слой Data, который получает колбэки.
Observer pattern в UI и колбэки в слой Data:
Если пользователь выходит из прилжения, ViewModel перестает кем бы то ни было наблюдаться. Если репозиторий реализован как синглтон или каким-то другим образом привязан к скоупу приложения, он не будет уничтожен пока процесс приложения не будет убит. Это произойдет только когда системе потребуются ресурсы или пользователь вручную убьет прилжение. Если репозиторий ссылается на колбэк в ViewModel — будет создана утечка памяти.
Активити закончила работу, но ViewModel продолжает существовать:
Эта утечка не так важна, если ViewModel легкая или операция гарантированно завершится в течение короткого отрезка времени. Идеально ViewModel'и должны освобождаться во всех случаях, когда отсутствуют View, которые за ними наблюдают:
Этого можно достигнуть многими способами:
- С помощью ViewModel.onCleared() Вы можете сообщить репозиторию о том, что он должен сбросить колбэк к ViewModel.
- В репозитории Вы можете использовать WeakReference или Event Bus (использование обеих может проводиться некорректно и даже считается вредным).
- Используйте LiveData для коммуникации между репозиторием и ViewModel также, как используете ее для коммуникации между View и ViewModel.
Учитывайте пограничные случаи, утечки памяти и то, как долгие операции может повлиять на инстансы в Вашей архитектуре.
Не помещайте в ViewModel логику, которая является критически важной для сохранения чистоты архитектуры или если она связана с данными. Каждый вызов, сделанный от ViewModel может быть последним.
LiveData в репозиториях
Чтобы избежать утечек в ViewModel и «ада колбэков», репозитории могут наблюдаться следующим образом:
Когда ViewModel очищена или когда жизненный цикл view прекратился, подписка очищается:
В этом способе есть одна тонкость: как подписаться к репозиторию от ViewModel, если нет доступа к LifecycleOwner? С помощью Трансформаций. Transformations.switchMap позволяет создавать LiveData, которая реагирует на изменения в других инстансах LiveData. За счет этого информация передается через observer жидненного цикла по цепочке:
LiveData<Repo> repo = Transformations.switchMap(repoIdLiveData, repoId -> {
if (repoId.isEmpty()) {
return AbsentLiveData.create();
}
return repository.loadRepo(repoId);
}
);
Пример трансформации [ссылка]
В этом примере, когда триггер получает обновление, выполняется функция и результат каскадируется. Активити наблюдает за репозиторием и тот же LifecycleOwner будет использован для вызова repository.loadRepo(id).
Каждый раз, когда Вы думаете, что Вам нужен объект Lifecycle внутри ViewModel, использование Transformation скорее всего поможет этого избежать и решит проблему.
Наследуясь от LiveData
Самый частый вид использования LiveData это использование MutableLiveData в ViewModel представление их как LiveData для того, чтобы сделать их неизменяемыми для наблюдателей.
Если Вам нужно больше функционала, наследование от LiveData позволит узнать, присутствуют ли активные наблюдатели. К примеру, это может быть полезно, если вы хотите начать прослушивать сервис локации или сеносоры устройства.
public class MyLiveData extends LiveData<MyData> {
public MyLiveData(Context context) {
// Initialize service
}
@Override
protected void onActive() {
// Start listening
}
@Override
protected void onInactive() {
// Stop listening
}
}
Когда не надо наследоваться от LiveData
Вы также можете использовать onActive() для начала какого-то сервиса, который загружает данные, но если у Вас для этого нет хорошей причины, Вам не надо ждать, пока LiveData начнет наблюдаться. Самые распространненные паттерны:
Добавьте метод start() в ViewModel и вызовите его как можно скорее [см. пример с Blueprints]
Назначьте поле, которое прекращает загрузку [см. пример GithubBrowserExample].
Обычно наследование от LiveData не происходит. Пусть активити или фрагмент подскажет ViewModel когда начать загружать данные.Оригинал статьи
Комментарии (4)
agent10
24.09.2017 22:48Ну, на мой взгляд вот эта «Концепция событий» в данном случае выглядит как костыль. Архитектурно не заложено и просто решили притянуть за уши то, что есть. Так же костыльным решением выглядит обработка ошибок, почему сразу не добавили некий onError по аналогии с Rx Observable не очень ясно.
Было бы также интересно посмотреть на случай разной вёрстки для планшетов/телефонов. Что лучше делать в этом случае? Разные ViewModel или может одна? Если одна, то где располагать логику поведения для разных типов девайсов?
flatscode
Подскажите, а в iOS так же приходится плясать с бубном, как и в Android вокруг onSaveInstanceState(), Fragment.setRetainInstance() и т.п., или там проще?