ViewModel и LiveData: паттерны и антипаттерны

в 12:48, , рубрики: android, antipatterns, livedata, перевод, Разработка под android

Привет! Представляю вашему вниманию перевод статьи ViewModels and LiveData: Patterns + AntiPatterns автора Jose Alcérreca.

View и ViewModel

Распределение ответственностей

Типичное взаимодействие объектов приложения, построенное с помощью Архитектурных Компонентов:

image

В идеале 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 сохраняется при изменениях конфигурации:

image

Передача ссылки на View (активити или фрагмент) в ViewModel является серьезным риском. Предположим, ViewModel запросила данные из сети, и они придут чуть позже. В этот момент ссылка на View может быть уничтожена или активити может больше не отображаться — это приведет к утечке памяти, а возможно и к крэшу приложения.

Избегайте ссылок на View в ViewModels.

Рекомендуемый способ коммуникации между ViewModel и View — использование observer pattern, при помощи LiveData или observable из других библиотек.

Observer Pattern (шаблон проектирования «Наблюдатель»)

image

Удобным способом дизайна презентационного слоя в Android является, когда View (активити или фрагмент) наблюдает (подписана на изменения в) ViewModel. Т.к. ViewModel не знает ничего про Android, она также не знает о том, как часто Android убивает View. У этого подхода есть несколько преимуществ:

  1. ViewModel сохраняются при изменении конфигурации, поэтому нет нужды в повторном запросе внешних источников данных (к примеру, базы данных или сетевого хранилища) при произведенном повороте устройства.
  2. Когда заканчивается какая-либо длительная операция, observable в ViewModel обновляются. Не важно, велось ли наблюдение за данными или нет. NPE не произойдет даже при попытке обновления несуществующего View.
  3. 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 если требуется.

Использовать репозиторий для данных

Как видно в руководствах по архитектуре приложений, большинство приложений имеют несколько источников данных. Таких как:

  1. Удаленный: сеть или облако
  2. Локальный: база данных или файл
  3. Кэш

Создать в прилжении слой данных, который ничего не знает о слое presentation — хорошая идея. Алгоритмы синхронизации кэша и базы данных не тривиальны. Поэтому рекомендуется создать класс репозитория, который послужит единственной точкой входа для борьбы с этой сложностью.

Если у вас несколько моделей данных и они достаточно сложны — имеет смысл добавления нескольких репозиториев.

Добавьте репозиторий данных в качестве едсинственной точки входа для данных

Обработка состояний данных

Представьте следующее: Вы подписаны на обновления LiveData, предоставленной ViewModel, которая содержит список для отображения. Как View сможет отличить загруженные данные от сетевой ошибки или пустого списка?

Вы можете предоставить доступ к LiveData через ViewModel. К примеру, MyDataState может содержать информацию о том, корректно ли загружаются данные, загрузились ли окончательно или загрузка была прервана.

image

Вы можете обернуть данные в клас, который содержит в себе состояния и другие метаданные (например, сообщение о ошибке). См. класс Resource в предоставленных примерах (TODO: add link)

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:

image

Если пользователь выходит из прилжения, ViewModel перестает кем бы то ни было наблюдаться. Если репозиторий реализован как синглтон или каким-то другим образом привязан к скоупу приложения, он не будет уничтожен пока процесс приложения не будет убит. Это произойдет только когда системе потребуются ресурсы или пользователь вручную убьет прилжение. Если репозиторий ссылается на колбэк в ViewModel — будет создана утечка памяти.
Активити закончила работу, но ViewModel продолжает существовать:

image

Эта утечка не так важна, если ViewModel легкая или операция гарантированно завершится в течение короткого отрезка времени. Идеально ViewModel'и должны освобождаться во всех случаях, когда отсутствуют View, которые за ними наблюдают:

image

Этого можно достигнуть многими способами:

  • С помощью ViewModel.onCleared() Вы можете сообщить репозиторию о том, что он должен сбросить колбэк к ViewModel.
  • В репозитории Вы можете использовать WeakReference или Event Bus (использование обеих может проводиться некорректно и даже считается вредным).
  • Используйте LiveData для коммуникации между репозиторием и ViewModel также, как используете ее для коммуникации между View и ViewModel.

Учитывайте пограничные случаи, утечки памяти и то, как долгие операции может повлиять на инстансы в Вашей архитектуре.

Не помещайте в ViewModel логику, которая является критически важной для сохранения чистоты архитектуры или если она связана с данными. Каждый вызов, сделанный от ViewModel может быть последним.

LiveData в репозиториях

Чтобы избежать утечек в ViewModel и «ада колбэков», репозитории могут наблюдаться следующим образом:

image

Когда ViewModel очищена или когда жизненный цикл view прекратился, подписка очищается:

image

В этом способе есть одна тонкость: как подписаться к репозиторию от 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 когда начать загружать данные.

Оригинал статьи

Автор: artem_dobrovinskiy

Источник

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


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