Android Architecture Components. Часть 4. ViewModel
Компонент ViewModel — предназначен для хранения и управления данными, связанными с представлением, а заодно, избавить нас от проблемы, связанной с пересозданием активити во время таких операций, как переворот экрана и т.д. Не стоит его воспринимать, как замену onSaveInstanceState, поскольку, после того как система уничтожит нашу активити, к примеру, когда мы перейдем в другое приложение, ViewModel будет также уничтожен и не сохранит свое состояние. В целом же, компонент ViewModel можно охарактеризовать как синглтон с колекцией экземпляров классов ViewModel, который гарантирует, что не будет уничтожен пока есть активный экземпляр нашей активити и освободит ресурсы после ухода с нее (все немного сложнее, но выглядит как-то так). Стоит также отметить, что мы можем привязать любое количество ViewModel к нашей Activity(Fragment).
Компонент состоит из таких классов: ViewModel, AndroidViewModel, ViewModelProvider, ViewModelProviders, ViewModelStore, ViewModelStores. Разработчик будет работать только с ViewModel, AndroidViewModel и для получения истанца с ViewModelProviders, но для лучшего понимания компонента, мы поверхностно рассмотрим все классы.
Класс ViewModel, сам по себе представляет абстрактный класс, без абстрактных методов и с одним protected методом onCleared(). Для реализации собственного ViewModel, нам всего лишь необходимо унаследовать свой класс от ViewModel с конструктором без параметров и это все. Если же нам нужно очистить ресурсы, то необходимо переопределить метод onCleared(), который будет вызван когда ViewModel долго не доступна и должна быть уничтожена. Как пример, можно вспомнить предыдущую статью про LiveData, а конкретно о методе observeForever(Observer), который требует явной отписки, и как раз в методе onCleared() уместно ее реализовать. Стоит еще добавить, что во избежания утечки памяти, не нужно ссылаться напрямую на View или Context Activity из ViewModel. В целом, ViewModel должна быть абсолютно изолированная от представления данных. В таком случае появляется вопрос: А каким же образом нам уведомить представление (Activity/Fragment) об изменениях в наших данных? В этом случае на помощь нам приходит LiveData, все изменяемые данные мы должны хранить с помощью LiveData, если же нам необходимо, к примеру, показать и скрыть ProgressBar, мы можем создать MutableLiveData и хранить логику показать\скрыть в компоненте ViewModel. В общем это будет выглядеть так:
Для получения ссылки на наш экземпляр ViewModel мы должны воспользоваться ViewModelProviders:
Класс AndroidViewModel, являет собой расширение ViewModel, с единственным отличием — в конструкторе должен быть один параметр Application. Является довольно полезным расширением в случаях, когда нам нужно использовать Location Service или другой компонент, требующий Application Context. В работе с ним единственное отличие, это то что мы наследуем наш ViewModel от ApplicationViewModel. В Activity/Fragment инициализируем его точно также, как и обычный ViewModel.
Класс ViewModelProviders, являет собой четыре метода утилиты, которые, называются of и возвращают ViewModelProvider. Адаптированные для работы с Activity и Fragment, а также, с возможностью подставить свою реализацию ViewModelProvider.Factory, по умолчанию используется DefaultFactory, которая является вложенным классом в ViewModelProviders. Пока что других реализаций приведенных в пакете android.arch нет.
Класс ViewModelProvider, собственно говоря класс, который возвращает наш инстанс ViewModel. Не будем особо углубляться здесь, в общих чертах он являет роль посредника с ViewModelStore, который, хранит и поднимает наш интанс ViewModel и возвращает его с помощью метода get, который имеет две сигнатуры get(Class) и get(String key, Class modelClass). Смысл заключается в том, что мы можем привязать несколько ViewModel к нашему Activity/Fragment даже одного типа. Метод get возвращает их по String key, который по умолчанию формируется как: «android.arch.lifecycle.ViewModelProvider.DefaultKey:» + canonicalName
Класс ViewModelStores, являет собой фабричный метод, напомню: Фабричный метод — паттерн, который определяет интерфейс для создания объекта, но оставляет подклассам решение о том, какой класс инстанцировать, по факту, позволяет классу делегировать инстанцирование подклассам. На данный момент, в пакете android.arch присутствует как один интерфейс, так и один подкласс ViewModelStore.
Класс ViewModelStore, класс в котором и находится вся магия, состоит из методов put, get и clear. Про них не стоит беспокоится, поскольку работать напрямую мы с ними не должны, а с get и put и физически не можем, так как они объявлены как default (package-private), соответственно видны только внутри пакета. Но, для общего образования, рассмотрим устройство этого класса. Сам класс хранит в себе HashMap , методы get и put, соответственно, возвращают по ключу (по тому самому, который мы формируем во ViewModelProvider) или добавляют ViewModel. Метод clear(), вызовет метод onCleared() у всех наших ViewModel которые мы добавляли.
Для примера работы с ViewModel давайте реализуем небольшое приложение, позволяющее выбрать пользователю точку на карте, установить радиус и показывающее, находится человек в этом поле или нет. А также дающее возможность указать WiFi network, если пользователь подключен к нему, будем считать что он в радиусе, вне зависимости от физических координат.
Для начала создадим две LiveData для отслеживания локации и имени WiFi сети:
Теперь перейдем к ViewModel, поскольку у нас есть условие, которое зависит от полученных данных с двух LifeData, нам идеально подойдет MediatorLiveData как холдер самого значения, но поскольку перезапускать сервисы нам невыгодно, поэтому подпишемся к MediatorLiveData без привязки к жизненному циклу с помощью observeForever. В методе onCleared() реализуем отписку от него с помощью removeObserver. В свою же очередь LiveData будет уведомлять об изменении MutableLiveData, на которую и будет подписано наше представление.
И наше представление:
В общих чертах мы подписываемся на MutableLiveData, с помощью меnода getStatus() из нашего ViewModel. А также работаем с ним для инициализации и сохранения наших данных.
Здесь также добавлено несколько проверок, таких как RuntimePermission и проверка на состояние GPS. Как можно заметить, код в Activity получился довольно обширный, в случае сложного UI, гугл рекомендует посмотреть в сторону создания презентера(но это может быть излишество).
В примере также использовались такие библиотеки как:
Источник
Урок 4. ViewModel
В этом уроке рассмотрим, как использовать ViewModel. Как сохранять данные при повороте экрана. Как передать Context в ViewModel. Как передать свои данные в конструктор модели с помощью фабрики. Как передать данные между фрагментами. Что использовать: ViewModel или onSavedInstanceState.
Полный список уроков курса:
ViewModel — класс, позволяющий Activity и фрагментам сохранять необходимые им объекты живыми при повороте экрана.
Давайте сразу посмотрим пример и по ходу дела разберем нюансы и прочую теорию. Как подключить библиотеку к проекту, вы можете посмотреть в начале Урока 2.
Создаем свой класс, наследующий ViewModel
Пока оставим его пустым.
Чтобы добраться до него в Activity, нужен следующий код:
В метод ViewModelProviders.of передаем Activity. Тем самым мы получим доступ к провайдеру, который хранит все ViewModel для этого Activity.
Методом )» target=»_blank» rel=»noopener noreferrer»>get запрашиваем у этого провайдера конкретную модель по имени класса — MyViewModel. Если провайдер еще не создавал такой объект ранее, то он его создает и возвращает нам. И пока Activity окончательно не будет закрыто, при всех последующих вызовах метода get мы будем получать этот же самый объект MyViewModel.
Соответственно, при поворотах экрана, Activity будет пересоздаваться, а объект MyViewModel будет спокойно себе жить в провайдере. И Activity после пересоздания сможет получить этот объект обратно и продолжить работу, как будто ничего не произошло.
Отсюда следует важный вывод. Не храните в ViewModel ссылки на Activity, фрагменты, View и пр. Это может привести к утечкам памяти.
На картинке время жизни (оно же scope) модели это выглядит так:
Модель жива, пока Activity не закроется окончательно.
У метода get, который возвращает нам модель из провайдера, есть еще такой вариант вызова:
Т.е. вы можете создавать несколько моделей одного и того же класса, но использовать разные текстовые ключи для их хранения в провайдере.
LiveData
LiveData очень удобно использовать с ViewModel. В прошлых уроках я для работы с LiveData использовал синглтон, но теперь мы перейдем на ViewModel.
Рассмотрим несложный пример асинхронной однократной загрузки каких-либо данных:
Основной метод здесь — это getData. Когда Activity захочет получить данные, оно вызовет именно этот метод. Мы проверяем, создан ли уже MutableLiveData. Если нет, значит этот метод вызывается первый раз. В этом случае создаем MutableLiveData и стартуем асинхронный процесс получения данных методом loadData. Далее возвращаем LiveData.
В методе loadData происходит асинхронное получение данных из какого-нибудь репозитория. Как только данные будут получены (в методе onLoad), мы передаем их в MutableLiveData.
Метод loadData должен быть асинхронным, потому что он вызывается из метода getData, а getData в свою очередь вызывается из Activity и все это происходит в UI потоке. Если loadData начнет грузить данные синхронно, то он заблокирует UI поток.
Код в Activity выглядит так:
Получаем от провайдера модель. От модели получаем LiveData, на который подписываемся и ждем данные.
В этом примере ViewModel нужен, чтобы сохранить процесс получения данных при повороте экрана. А LiveData — для удобного асинхронного получения данных.
Т.е. это будет выглядеть так:
— Activity вызывает метод модели getData
— модель создает MutableLiveData и стартует асинхронный процесс получения данных от репозитория
— Activity подписывается на полученный от модели LiveData и ждет данные
— происходит поворот экрана
— на модели этот поворот никак не сказывается, она спокойно сидит в провайдере и ждет ответ от репозитория
— Activity пересоздается, получает ту же самую модель от провайдера, получает тот же самый LiveData от модели и подписывается на него и ждет данные
— репозиторий возвращает данные, модель передает их в MutableLiveData
— Activity получает данные данные от LiveData
Если репозиторий вдруг пришлет ответ в тот момент, когда Activity будет пересоздаваться, то Activity получит этот ответ, как только подпишется на LiveData.
Если ваш репозиторий сам умеет возвращать LiveData, то все значительно упрощается. Вы просто отдаете этот LiveData в Activity и оно подписывается.
Очистка ресурсов
Когда Activity окончательно закрывается, провайдер удаляет ViewModel, предварительно вызвав его метод onCleared
В этом методе вы сможете выполнить все необходимые операции по освобождению ресурсов, закрытию соединений/потоков и т.п.
Context
Не стоит передавать Activity в модель в качестве Context. Это может привести к утечкам памяти.
Если вам в модели понадобился объект Context, то вы можете наследовать не ViewModel, а AndroidViewModel.
При создании этой модели, провайдер передаст ей в конструктор класс Application, который является Context. Вы сможете до него добраться методом getApplication.
Код получения этой модели в Activity останется тем же самым.
Передача объектов в конструктор модели
Бывает необходимость передать модели какие-либо данные при создании. Модель создается провайдером и у нас есть возможность вмешаться в этот процесс. Для этого используется фабрика. Мы учим эту фабрику создавать модель так, как нам нужно. И провайдер воспользуется этой фабрикой, когда ему понадобится создать объект.
Рассмотрим пример. У нас есть такая модель
Ей нужен long при создании.
В конструктор передаем long, который нам необходимо будет передать в модель.
В методе )» target=»_blank» rel=»noopener noreferrer»>create фабрика получит от провайдера на вход класс модели, которую необходимо создать. Проверяем, что это класс MyViewModel, сами создаем модель и передаем туда long.
В Activity код получения модели будет выглядеть так:
Мы создаем новую фабрику с нужными нам данными и передаем ее в метод of. При вызове метода get провайдер использует фабрику для создания модели, т.е. выполнится наш код создания модели и передачи в нее данных.
Передача данных между фрагментами
ViewModel может быть использована для передачи данных между фрагментами, которые находятся в одном Activity. В документации есть отличный пример кода:
SharedViewModel — модель с двумя методами: один позволяет поместить данные в LiveData, другой — позволяет получить этот LiveData. Соответственно, если два фрагмента будут иметь доступ к этой модели, то один сможет помещать данные в его LiveData, а другой — подпишется и будет получать эти данные. Таким образом два фрагмента будут обмениваться данными ничего не зная друг о друге.
Чтобы два фрагмента могли работать с одной и той же моделью, они могут использовать общее Activity. Код получения модели в фрагментах выглядит так:
Для обоих фрагментов getActivity вернет одно и то же Activity. Метод ViewModelProviders.of вернет провайдера этого Activity. Далее методом get получаем модель.
Фрагмент MasterFragment помещает данные в LiveData. А DetailFragment — подписывается и получает данные.
onSavedInstanceState
Чем ViewModel отличается от onSavedInstanceState. Для каких данных какой из них лучше использовать. Кажется, что если есть ViewModel, который жив все время, пока не закрыто Activity, то можно забыть про onSavedInstanceState. Но это не так.
Давайте в качестве примера рассмотрим Activity, которое отображает список каких-то данных и может выполнять поиск по ним. Пользователь открывает Activity и выполняет поиск. Activity отображает результаты этого поиска. Пользователь сворачивает приложение. Когда он его снова откроет, он ожидает, что там все останется в этом же состоянии.
Но тут внезапно системе не хватает памяти и она убивает это свернутое приложение. Когда пользователь снова запустит его, Activity ничего не будет знать о поиске, и просто покажет все данные. В этом случае ViewModel нам никак не поможет, потому что модель будет убита вместе с приложением. А вот onSavedInstanceState будет выполнен. В нем мы сможем сохранить поисковый запрос, и при последующем запуске получить его из объекта savedInstanceState и выполнить поиск. В результате пользователь увидит тот же экран, который был, когда приложение было свернуто.
ViewModel — здесь удобно держать все данные, которые нужны вам для формирования экрана. Они будут жить при поворотах экрана, но умрут, когда приложение будет убито системой.
onSavedInstanceState — здесь нужно хранить тот минимум данных, который понадобится вам для восстановления состояния экрана и данных в ViewModel после экстренного закрытия Activity системой. Это может быть поисковый запрос, ID и т.п.
Соответственно, когда вы достаете данные из savedInstanceState и предлагаете их модели, это может быть в двух случаях:
1) Был обычный поворот экрана. В этом случае ваша модель должна понять, что ей эти данные не нужны, потому что при повороте экрана модель ничего не потеряла. И уж точно модель не должна заново делать запросы в БД, на сервер и т.п.
2) Приложение было убито, и теперь запущено заново. В этом случае модель берет данные из savedInstanceState и использует их, чтобы восстановить свои данные. Например, берет ID и идет в БД за полными данными.
RxJava
В начале этого урока мы рассмотрели пример работы ViewModel и LiveData. Возникает вопрос, можно ли заменить LiveData на Flowable?
У LiveData есть одно большое преимущество — он учитывает состояние Activity. Т.е. он не будет слать данные, если Activity свернуто. И он отпишет от себя Activity, которое закрывается.
А вот Flowable этого не умеет. Если в модели есть Flowable, и Activity подпишется на него, то этот Flowable будет держать Activity, пока оно само явно не отпишется (или пока Flowable не завершится).
Давайте рассмотрим пример. ViewModel обычно работает с репозиториями, которые могут быть синглтонами. В репозитории есть какой-то объект для подписки (типа LiveData или Flowable). Репозиторий периодически обновляет в нем данные. Модель берет этот объект из репозитория и отдает его в Activity, и Activity подписывается на этот объект. Объект теперь хранит ссылку на Activity.
Таким образом получилось, что репозиторий держит ссылку на Activity через объект подписки. И если мы закроем Activity, но не отпишем его от объекта подписки, то возникнет утечка памяти, т.к. репозиторий может жить все время работы приложения. И все это время Activity будет висеть в памяти.
Давайте рассмотрим, как это решается в случае с LiveData или Flowable. Важно понимать, что будет происходить с подпиской при закрытии Activity. ViewModel будем рассматривать только как инструмент передачи объекта из репозитория в Activity.
1) ViewModel готов из репозитория предоставить LiveData. И мы в Activiy хотели бы работать с LiveData.
Цепочка ссылок:
Repository -> LiveData -> Activity
Тут получается полная идилия. Activity берет из модели LiveData, подписывается на него и все ок. При закрытии Activity не будет никаких утечек памяти и прочих проблем с подпиской, т.к. LiveData сам отпишет Activity и тем самым разорвет цепочку ссылок.
2) ViewModel готов вернуть нам Flowable, а мы в Activity хотели бы работать с LiveData.
В этом случае конвертируем Flowable в LiveData внутри модели и отдаем LiveData в Activity.
Цепочка ссылок:
Repository -> Flowable -> LiveData -> Activity
Activity снова будет подписано на LiveData. А это значит, что нам, как и в первом варианте, не надо заботиться об этой подписке. LiveData отпишет от себя Activity, и сам отпишется от Flowable. Цепочка ссылок разорвется в двух местах.
3) ViewModel готов вернуть нам LiveData, а мы в Activity хотели бы работать с Flowable.
В этом случае передаем LiveData в Activity и преобразуем его в Flowable.
Цепочка ссылок:
Repository -> LiveData -> Flowable -> Activity
Activity будет подписано на Flowable. А Flowable будет подписан на LiveData. При этом подписка Flowable на LiveData будет работать с учетом Activity LifeCycle. И когда Activity будет закрыто, LiveData сам отпишет от себя Flowable.
Цепочка ссылок разорвется, но, в любом случае, хорошей практикой является отписка от Flowable вручную при закрытии Activity.
4) ViewModel готов вернуть нам Flowable, и мы в Activity хотели бы работать с Flowable.
В этом случае Activity будет подписано на Flowable.
Цепочка ссылок:
Repository -> Flowable -> Activity
При закрытии Activity нам самим необходимо отписать Activity от Flowable.
Как конвертировать LiveData во Flowable и наоборот, мы рассматривали в Уроке 3.
Присоединяйтесь к нам в Telegram:
— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование
— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня
— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме
Источник