Урок 7. ViewModel и LiveData. Сохранение и передача состояния активити при повороте устройства

Продолжаем курс по обучению основам разработки мобильных приложений в Android Studio на языке Kotlin.

Это урок 7, в котором разберемся, зачем сохранять состояние активити при изменениях конфигурации и какие инструменты для этого лучше использовать: savedInstanceState или ViewModel и LiveData.  
Предыдущий урок, на котором мы разбирали жизненный цикл активити, здесь.

 

Зачем сохранять состояние активити?

Создадим приложение. В макет экрана добавим поле для ввода текста и кнопку. Также оставим здесь текстовое поле по умолчанию, только изменим размер текста. Убедимся, что каждый элемент макета экрана имеет идентификатор. Код макета экрана:

В теле функции onCreate присвоим слушатель кнопке. По ее нажатию будем отправлять набранный текст из поля editText в поле textView.  

Зачем нам нужно сохранять состояние? Для ответа на этот вопрос запустим приложение сейчас. Напишем какой-то текст в editText и нажмем кнопку. Текст передался в текстовое поле, все ок. Но стоит нам повернуть устройство, как текст исчезает. Это происходит потому, что активити пересоздается, соответственно, пересоздаются все экранные компоненты. Но в editText текст сохраняется, если поле имеет идентификатор, а в textView ничего не сохраняется.

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

Функция onSaveInstanceState()

Для сохранения текста в textView можно воспользоваться объектом savedInstanceState, который приходит в качестве параметра в функцию onCreate(). Объект savedInstanceState имеет тип Bundle, который представляет собой набор пар “ключ — значение” и может быть использован для сохранения предыдущего состояния активити. Для сохранения данных в объект savedInstanceState используется функция onSaveInstanceState().

Переопределим функцию onSaveInstanceState() с набором данных Bundle. В теле функции run объекта бандла вызываем функцию putString для создания элемента коллекции с ключом “KEY” и текстом из editText в качестве значения.   

Также переопределим функцию onRestoreInstanceState, которая вызывается при старте активити в том случае, если имеется бандл, ранее сохраненный функцией onSaveInstanceState. В теле функции onRestoreInstanceState мы можем получить из бандла и передать в текстовое поле ранее сохраненный текст.

Теперь запустите приложение и убедитесь, что при смене конфигурации активити текст, переданный в TextView, сохраняется.

ViewModel

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

Архитектурные компоненты из набора Android Jetpack предоставляют вспомогательный класс  ViewModel для контроллера, который отвечает за подготовку данных для пользовательского интерфейса. Объекты ViewModel автоматически сохраняются во время изменений конфигурации, так что содержащиеся в них данные сразу же становятся доступны для следующего экземпляра активити или фрагмента.

Чтобы более тесно на практике познакомиться с чистой архитектурой и архитектурными компонентами, записывайтесь на продвинутый курс по разработке приложения «Чат-мессенжер»

На схеме ниже видно, как ViewModel взаимодействует с жизненным циклом активити:

При пересоздании активити ViewModel остается живым и используется во вновь созданном активити.

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

Приложение с ViewModel

Рассмотрим простой пример приложения, которое использует ViewModel.

За основу был взят этот пример на Github.

В файле сборки build.gradle модуля app добавьте такие зависимости для работы со списком и  ViewModel:

Поскольку приложение будет работать со списком пользователей, нам понадобится модель, сущность:

Здесь два поля — имя и описание.

Далее создадим файл object, выполняющий роль поставщика данных:

В реальном приложении данные поставляются из сети или БД, здесь же для простоты просто создаются два статичных списка пользователей. Первый список, который поменьше, будем отображать при старте приложения. Второй список будем отображать по нажатию кнопки в меню главного экрана.

Для отображения списка нам нужно создать файл макета элемента списка user_item.xml в папке ресурсов res/layout:

Изменим файл макета activity_main.xml для размещения списка на главном экране, добавив виджет списка RecyclerView:

Также нужно создать меню, для этого в папке res создадим папку menu и в ней файл main_menu.xml:

Это меню с одним пунктом Refresh, по нажатию которого будем обновлять список.

Теперь адаптер, который будет создавать список и наполнять его данными:

 

Унаследуем наш адаптер от RecyclerView.Adapter и указываем наш собственный ViewHolder, который предоставит доступ к View-компонентам. Далее инициализируем список. Функция onCreateViewHolder создает ViewHolder и инициализирует View-компоненты для списка. Функция onBindViewHolder связывает View-компоненты с содержимым.  В функции refreshUsers передаем данные и оповещаем адаптер о необходимости обновления списка вызовом notifyDataSetChanged(). Внутренний класс ViewHolder описывает View-компоненты списка и привязку их к RecyclerView.

Теперь мы подходим к самому главному — получению данных для списка.

Этим будет заниматься класс UserViewModel, унаследованный от ViewModel:

Для списка пользователей используется объект класса MutableLiveData — это подкласс LiveData, который является частью Архитектурных компонентов, и следует паттерну Observer (наблюдатель). Если вы знакомы с RxJava, класс LiveData похож на Observable. Но если с Observable вы должны удалять связи вручную, то класс LiveData зависит от жизненного цикла и выполняет всю очистку самостоятельно. Подписчиками LiveData являются активити и фрагменты. LiveData принимает подписчика и уведомляет его об изменениях данных, только когда он находится в состоянии STARTED или RESUMED. Состояние подписчиков определяется их объектом LifeCycle. Более подробно LifeCycle и состояния жизненного цикла мы рассматривали на прошлом уроке.

Класс MutableLiveData предоставляет методы setValue и postValue (второй — поточно-безопасный), посредством которых можно получить и отправить данные любым активным подписчикам.

В классе  UserViewModel мы инициализируем список и заполняем его данными пользователей. Функция getListUsers() возвращает список, а функция updateListUsers() обновляет список, сохраняя в него второй список пользователей из класса UserData.

Теперь код MainActivity:

Инициализируем объект класса  UserViewModel так называемым ленивым способом с помощью функции lazy(). Это функция, которая принимает лямбду и возвращает экземпляр класса Lazy<T>, который служит делегатом для реализации ленивого свойства: первый вызов get() запускает лямбда-выражение, переданное lazy() в качестве аргумента, и запоминает полученное значение, а последующие вызовы просто возвращают вычисленное значение. Таким образом, объект UserViewModel  инициализируется только при первом вызове, а далее используется уже инициализированный объект.

В теле onCreate() инициализируем адаптер и присваиваем его списку. Далее подписываем адаптер на изменения списка с помощью функции observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer), которой на вход передается объект LifecycleOwner (текущее активити) и интерфейс Observer — колбек, уведомляющий об успешном получении данных. При этом вызывается метод обновления списка адаптера и ему передается обновленный список.

Ниже создаем меню и обрабатываем нажатие пункта меню Refresh, по которому обновляем список.

Запуск приложения

Теперь запустим приложение на эмуляторе и проверим его работу.

После запуска открывается экран со списком пользователей. Обновим список из меню. Теперь отображается другой, расширенный список. Но если мы перезапустим приложение, то снова увидим первоначальный список, который открывается по умолчанию.

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

Дополнительно о LiveData можно почитать здесь.

Исходный код приложения можно посмотреть здесь.

Kotlin Android Extensions

Если вы заметили, мы обращаемся к экранным компонентам без вызова метода findViewById, прямо по идентификатору. Это происходит благодаря использованию плагина Kotlin Android Extensions — это плагин для Kotlin, который включён в стандартный пакет. Он позволяет восстанавливать view из Activities, Fragments, и Views таким вот простым способом.

Плагин генерирует дополнительный код, который позволяет получить доступ к view в виде XML, так же, как если бы вы имели дело с properties с именем id, который вы использовали при определении структуры.

Также он создаёт локальный кэш view. При первом использовании свойства, плагин выполнит стандартный findViewById. В последующем, view будет восстановлен из кэша, поэтому доступ к нему будет быстрее.

По умолчанию плагин уже интегрирован в модуль благодаря вот такой строчке в файле сборки модуля:

При первом обращении к любому экранному компоненту в MainActivity автоматически добавляется такой импорт:

Больше о Kotlin Android Extensions рекомендую почитать в переводе статьи Antonio Leiva на Медиуме.

На этом наш урок подошел к концу. Вопросы задавайте в комментариях. Всем добра!

Комментарии: 9
  1. genbachae

    Виталий, что означает запись «UserViewModel::class.java» в строчке: «private val userViewModel by lazy {ViewModelProviders.of(this).get(UserViewModel::class.java)}» ? Почему нельзя написать проще: «private val userViewModel by lazy {UserViewModel}» ? Почему приходиться действовать через класс «ViewModelProviders»?

    1. admin (автор)

      особенности синтаксиса

  2. genbachae

    Виталий, за что отвечает параметр app:showAsAction=»withText» в файле main_menu.xml ? Если этот параметр удалить, то приложение всё равно запускается, может он не нужен?

    1. admin (автор)

      Он отвечает за способ отображения пункта меню. Подробности ищите в документации и в уроках на нашем канале

  3. genbachae

    Виталий, во фрагменте кода:

    //инициализируем адаптер и присваиваем его списку
    val adapter = UserAdapter() // получаем экземпляр класса «UserAdapter»
    userList.layoutManager = LinearLayoutManager(this) // откуда взялся объект «userList» ?
    userList.adapter = adapter

    Где создаётся объект «userList» и каково его назначение?

    1. admin (автор)

      userList — идентификатор виджета списка RecyclerView в макете activity_main.xml. Мы можем обращаться к нему напрямую, благодаря библиотеке Kotlin Android Extensions, о ней написано в уроке.

  4. genbachae

    Виталий, можете подробно объяснить что происходит в строчке:
    class UserAdapter : RecyclerView.Adapter() {

    не проще бы было написать так:
    class UserAdapter : RecyclerView.Adapter{

    Непонятно для чего нужна конструкция: (), можете объяснить её назначение?

    1. admin (автор)

      Без «()» не скомпилируется, так как RecyclerView.Adapter класс, а не интерфейс

  5. Freakytools

    Для тех, кто как и я страдает после июля 2019-го с неработающим кодом.

    В первом участке кода замените зависимость RecyclerView на «androidx.recyclerview:recyclerview:1.1.0-beta02».
    Так же проследите, чтобы библиотека appcompat была версии 1.1.0 и выше, у меня сейчас «androidx.appcompat:appcompat:1.1.0-rc01»
    Открывающий тэг RecyclerView в activity_main.xml меняем на новый androidx.recyclerview.widget.RecyclerView, то же касается соответствующих импортов в адаптере и MainActivity, в котором дополнительно нужно поменять импорт LinearLayoutManager на androidx.recyclerview.widget.LinearLayoutManager

Добавить комментарий