Основы Android Kotlin 07.2: DiffUtil и привязка данных с RecyclerView

Эта практическая работа входит в курс «Основы Android Kotlin». Вы получите максимальную пользу от этого курса, если будете выполнять практические работы последовательно. Все практические работы курса перечислены на целевой странице практической работы «Основы Android Kotlin» .

Введение

В предыдущей лабораторной работе вы обновили приложение TrackMySleepQuality для отображения данных о качестве сна в RecyclerView . Методы, которые вы освоили при создании своего первого RecyclerView , достаточны для большинства RecyclerViews , отображающих простые и не слишком большие списки. Однако существует ряд методов, которые повышают эффективность RecyclerView для больших списков и упрощают поддержку и расширение кода для сложных списков и сеток.

В этой практической работе вы доработаете приложение для отслеживания сна из предыдущей. Вы узнаете более эффективный способ обновления списка данных о сне и научитесь использовать привязку данных с RecyclerView . (Если у вас нет приложения из предыдущей практической работы, вы можете скачать начальный код для этой практической работы.)

Что вам уже следует знать

  • Создание базового пользовательского интерфейса с использованием активности, фрагментов и представлений.
  • Навигация между фрагментами и использование safeArgs для передачи данных между фрагментами.
  • Просмотр моделей, просмотр фабрик моделей, преобразований, а также LiveData и их наблюдателей.
  • Как создать базу данных Room , создать DAO и определить сущности.
  • Как использовать сопрограммы для баз данных и других длительных задач.
  • Как реализовать базовый RecyclerView с Adapter , ViewHolder и макетом элемента.

Чему вы научитесь

  • Как использовать DiffUtil для эффективного обновления списка, отображаемого RecyclerView .
  • Как использовать привязку данных с RecyclerView .
  • Как использовать адаптеры привязки для преобразования данных.

Что ты будешь делать?

  • Используйте приложение TrackMySleepQuality из предыдущей лабораторной работы этой серии.
  • Обновите SleepNightAdapter для эффективного обновления списка с помощью DiffUtil .
  • Реализуйте привязку данных для RecyclerView , используя адаптеры привязки для преобразования данных.

Приложение для отслеживания сна имеет два экрана, представленных фрагментами, как показано на рисунке ниже.

На первом экране, показанном слева, расположены кнопки для запуска и остановки отслеживания. На экране отображаются некоторые данные о сне пользователя. Кнопка «Очистить» безвозвратно удаляет все данные, собранные приложением. Второй экран, показанный справа, предназначен для выбора оценки качества сна.

Это приложение спроектировано для использования контроллера пользовательского интерфейса, ViewModel и LiveData , а также базы данных Room для сохранения данных о сне.

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

Вы можете продолжить использование приложения SleepTracker из предыдущей лабораторной работы или загрузить приложение RecyclerViewDiffUtilDataBinding-Starter с GitHub.

  1. При необходимости загрузите приложение RecyclerViewDiffUtilDataBinding-Starter с GitHub и откройте проект в Android Studio.
  2. Запустите приложение.
  3. Откройте файл SleepNightAdapter.kt .
  4. Изучите код, чтобы ознакомиться со структурой приложения. На схеме ниже представлен обзор использования RecyclerView с шаблоном адаптера для отображения данных о сне пользователю.

  • На основе данных, введенных пользователем, приложение создает список объектов SleepNight . Каждый объект SleepNight представляет одну ночь сна, ее продолжительность и качество.
  • SleepNightAdapter адаптирует список объектов SleepNight во что-то, что RecyclerView может использовать и отображать.
  • Адаптер SleepNightAdapter создает ViewHolders , которые содержат представления, данные и метаинформацию для представления recycler для отображения данных.
  • RecyclerView использует SleepNightAdapter для определения количества отображаемых элементов ( getItemCount() ). RecyclerView использует onCreateViewHolder() и onBindViewHolder() для привязки держателей представлений к отображаемым данным.

Метод notifyDataSetChanged() неэффективен

Чтобы сообщить RecyclerView , что элемент в списке изменился и его необходимо обновить, текущий код вызывает notifyDataSetChanged() в SleepNightAdapter , как показано ниже.

var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

Однако notifyDataSetChanged() сообщает RecyclerView , что весь список потенциально недействителен. В результате RecyclerView перепривязывает и перерисовывает каждый элемент списка, включая те, которые не видны на экране. Это лишнее. Для больших или сложных списков этот процесс может занять достаточно много времени, из-за чего изображение на экране будет мерцать или зависать при прокрутке списка пользователем.

Чтобы решить эту проблему, вы можете сообщить RecyclerView , что именно изменилось. После этого RecyclerView сможет обновлять только те представления на экране, которые изменились.

RecyclerView имеет богатый API для обновления одного элемента. Вы можете использовать notifyItemChanged() чтобы сообщить RecyclerView об изменении элемента, а также использовать аналогичные функции для добавления, удаления или перемещения элементов. Всё это можно сделать вручную, но эта задача будет нетривиальной и потребует написания значительного объёма кода.

К счастью, есть способ лучше.

DiffUtil эффективен и выполняет сложную работу за вас.

RecyclerView есть класс DiffUtil , предназначенный для вычисления разницы между двумя списками. DiffUtil берёт старый и новый списки и определяет разницу. Он находит элементы, которые были добавлены, удалены или изменены. Затем он использует алгоритм, называемый алгоритмом разностей Юджина У. Майерса, чтобы определить минимальное количество изменений, которые нужно внести в старый список для создания нового списка.

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

В этой задаче вы обновите SleepNightAdapter , чтобы использовать DiffUtil для оптимизации RecyclerView с учетом изменений данных.

Шаг 1: Реализация SleepNightDiffCallback

Чтобы использовать функциональность класса DiffUtil , расширьте DiffUtil.ItemCallback .

  1. Откройте SleepNightAdapter.kt .
  2. Ниже полного определения класса SleepNightAdapter создайте новый класс верхнего уровня SleepNightDiffCallback , расширяющий DiffUtil.ItemCallback . Передайте SleepNight как универсальный параметр.
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
  1. Установите курсор на имя класса SleepNightDiffCallback .
  2. Нажмите Alt+Enter ( Option+Enter на Mac) и выберите Реализовать элементы .
  3. В открывшемся диалоговом окне щелкните левой кнопкой мыши, удерживая клавишу Shift, чтобы выбрать методы areItemsTheSame() и areContentsTheSame() , затем нажмите кнопку OK .

    Это создаёт заглушки внутри SleepNightDiffCallback для двух методов, как показано ниже. DiffUtil использует эти два метода, чтобы определить, как изменились список и его элементы.
    override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
  1. Внутри areItemsTheSame() замените TODO кодом, который проверяет, одинаковы ли два переданных элемента SleepNight , oldItem и newItem . Если у элементов одинаковый nightId , это один и тот же элемент, поэтому возвращаем true . В противном случае возвращаем false . DiffUtil использует эту проверку, чтобы определить, был ли элемент добавлен, удалён или перемещён.
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem.nightId == newItem.nightId
}
  1. Внутри areContentsTheSame() проверьте, содержат ли oldItem и newItem одни и те же данные, то есть равны ли они. Эта проверка равенства проверит все поля, поскольку SleepNight — это класс данных. Классы Data автоматически определяют equals и несколько других методов. Если между oldItem и newItem есть различия, этот код сообщит DiffUtil , что элемент был обновлён.
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem == newItem
}

RecyclerView часто используется для отображения изменяющегося списка. RecyclerView предоставляет класс адаптера ListAdapter , который помогает создать адаптер RecyclerView на основе списка.

ListAdapter отслеживает список и уведомляет адаптер об его обновлении.

Шаг 1: Измените свой адаптер, чтобы расширить ListAdapter

  1. В файле SleepNightAdapter.kt измените сигнатуру класса SleepNightAdapter , чтобы расширить ListAdapter .
  2. При появлении запроса импортируйте androidx.recyclerview.widget.ListAdapter .
  3. Добавьте SleepNight в качестве первого аргумента ListAdapter перед SleepNightAdapter.ViewHolder .
  4. Добавьте SleepNightDiffCallback() в качестве параметра конструктора. ListAdapter будет использовать его для определения изменений в списке. Готовая сигнатура класса SleepNightAdapter должна выглядеть так, как показано ниже.
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. Внутри класса SleepNightAdapter удалите поле data , включая сеттер. Оно вам больше не понадобится, поскольку ListAdapter отслеживает список автоматически.
  2. Удалите переопределение getItemCount() , поскольку ListAdapter реализует этот метод за вас.
  3. Чтобы избавиться от ошибки в onBindViewHolder() , измените переменную item . Вместо использования data для получения item вызовите метод getItem(position) предоставляемый ListAdapter .
val item = getItem(position)

Шаг 2: Используйте метод submitList() для обновления списка.

Ваш код должен сообщать ListAdapter о доступности изменённого списка. ListAdapter предоставляет метод submitList() который сообщает ListAdapter о доступности новой версии списка. При вызове этого метода ListAdapter сравнивает новый список со старым и обнаруживает элементы, которые были добавлены, удалены, перемещены или изменены. Затем ListAdapter обновляет элементы, отображаемые RecyclerView .

  1. Откройте SleepTrackerFragment.kt .
  2. В onCreateView() , в наблюдателе sleepTrackerViewModel , найдите ошибку, в которой ссылается переменная data , которую вы удалили.
  3. Замените adapter.data = it вызовом adapter.submitList(it) . Обновлённый код показан ниже.

sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.submitList(it)
   }
})
  1. Запустите приложение. Оно работает быстрее, возможно, незаметно, если список небольшой.

В этом задании вы используете ту же технику, что и в предыдущих практикумах, для настройки привязки данных и исключаете вызовы findViewById() .

Шаг 1: Добавьте привязку данных к файлу макета

  1. Откройте файл макета list_item_sleep_night.xml на вкладке Текст .
  2. Установите курсор на тег ConstraintLayout и нажмите Alt+Enter ( Option+Enter (на Mac) Откроется меню намерений (меню «быстрого исправления»).
  3. Выберите «Преобразовать в макет привязки данных» . Макет будет упакован в тег <layout> и внутри него будет добавлен тег <data> .
  4. При необходимости прокрутите страницу обратно наверх и внутри тега <data> объявите переменную с именем sleep .
  5. Укажите его type соответствующий полному имени SleepNight , com.example.android.trackmysleepquality.database.SleepNight . Готовый тег <data> должен выглядеть так, как показано ниже.
   <data>
        <variable
            name="sleep"
            type="com.example.android.trackmysleepquality.database.SleepNight"/>
    </data>
  1. Чтобы принудительно создать объект Binding , выберите Build > Clean Project , а затем Build > Rebuild Project . (Если проблемы не исчезли, выберите File > Invalidate Caches / Restart .) Объект привязки ListItemSleepNightBinding вместе с соответствующим кодом добавляется в сгенерированные файлы проекта.

Шаг 2: Расширьте макет элемента, используя привязку данных

  1. Откройте SleepNightAdapter.kt .
  2. В классе ViewHolder найдите метод from() .
  3. Удалить объявление переменной view .

Код для удаления :

val view = layoutInflater
       .inflate(R.layout.list_item_sleep_night, parent, false)
  1. Там, где была переменная view , определите новую переменную с именем binding , которая расширяет объект привязки ListItemSleepNightBinding , как показано ниже. Выполните необходимый импорт объекта привязки.
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
  1. В конце функции вместо возврата view верните binding .
return ViewHolder(binding)
  1. Чтобы устранить ошибку, наведите курсор на слово binding . Нажмите Alt+Enter ( Option+Enter на Mac), чтобы открыть меню намерений.
  1. Выберите «Изменить тип параметра itemView основного конструктора класса ViewHolder» на ListItemSleepNightBinding» . Это обновит тип параметра класса ViewHolder .

  1. Прокрутите страницу вверх до определения класса ViewHolder , чтобы увидеть изменение в сигнатуре. Вы видите ошибку для itemView , поскольку вы изменили itemView на binding в методе from() .

    В определении класса ViewHolder щёлкните правой кнопкой мыши по одному из вхождений itemView и выберите «Рефакторинг» > «Переименовать» . Измените имя на binding .
  2. Добавьте префикс val binding параметра конструктора, чтобы сделать его свойством.
  3. В вызове родительского класса RecyclerView.ViewHolder измените параметр с binding на binding.root . Вам необходимо передать View , а binding.root — это корневой ConstraintLayout в вашем макете элемента.
  4. Готовое объявление класса должно выглядеть так, как показано ниже.
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){

Вы также видите ошибку при вызовах findViewById() , и исправьте ее следующим образом.

Шаг 3: Заменить findViewById()

Теперь вы можете обновить свойства sleepLength , quality и qualityImage , чтобы использовать объект binding вместо findViewById() .

  1. Измените инициализацию sleepLength , qualityString и qualityImage , чтобы использовать представления объекта binding , как показано ниже. После этого ваш код больше не должен выдавать ошибок.
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage

После добавления объекта привязки вам больше не нужно определять свойства sleepLength , quality и qualityImage . DataBinding кэширует результаты поиска, поэтому нет необходимости объявлять эти свойства.

  1. Щёлкните правой кнопкой мыши по именам свойств sleepLength , quality и qualityImage . Выберите «Рефакторинг» > «Встроить » или нажмите Control+Command+N ( Option+Command+N на Mac).
  2. Запустите приложение. (Возможно, вам придется очистить и пересобрать проект, если в нем есть ошибки.)

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

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

Вам предстоит реализовать три адаптера привязки: один для изображения качества и по одному для каждого текстового поля. Подводя итог, чтобы объявить адаптер привязки, вы определяете метод, принимающий элемент и представление, и аннотируете его @BindingAdapter . В теле метода реализуется преобразование. В Kotlin адаптер привязки можно написать как функцию расширения для класса представления, принимающего данные.

Шаг 1: Создание адаптеров привязки

Обратите внимание, что вам придется импортировать несколько классов на этом этапе, и они не будут вызываться по отдельности.

  1. Откройте SleepNightAdapater.kt .
  2. Внутри класса ViewHolder найдите метод bind() и запомните, что он делает. Вместо этого вы возьмёте код, вычисляющий значения binding.sleepLength , binding.quality и binding.qualityImage , и используете его внутри адаптера. (Пока оставьте код как есть; вы перенесёте его позже.)
  3. В пакете sleeptracker создайте и откройте файл BindingUtils.kt .
  4. Объявите функцию расширения для TextView с именем setSleepDurationFormatted и передайте ей SleepNight . Эта функция будет вашим адаптером для расчета и форматирования продолжительности сна.
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
  1. В теле setSleepDurationFormatted привяжите данные к представлению, как это было сделано в ViewHolder.bind() . Вызовите convertDurationToFormatted() , а затем установите форматированный text в качестве значения для TextView . (Поскольку это функция расширения TextView , вы можете напрямую обращаться к свойству text .)
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
  1. Чтобы сообщить привязке данных об этом адаптере привязки, аннотируйте функцию с помощью @BindingAdapter .
  2. Эта функция является адаптером для атрибута sleepDurationFormatted , поэтому передайте sleepDurationFormatted в качестве аргумента @BindingAdapter .
@BindingAdapter("sleepDurationFormatted")
  1. Второй адаптер устанавливает качество сна на основе значения объекта SleepNight . Создайте функцию расширения setSleepQualityString() для TextView и передайте ей SleepNight .
  2. В теле привяжите данные к представлению, как это было сделано в ViewHolder.bind() . Вызовите convertNumericQualityToString и задайте text .
  3. Добавьте к функции аннотацию @BindingAdapter("sleepQualityString") .
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
   text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
  1. Третий адаптер привязки устанавливает изображение в поле представления. Создайте функцию расширения для ImageView , вызовите setSleepImage и используйте код из ViewHolder.bind() , как показано ниже.
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
   setImageResource(when (item.sleepQuality) {
       0 -> R.drawable.ic_sleep_0
       1 -> R.drawable.ic_sleep_1
       2 -> R.drawable.ic_sleep_2
       3 -> R.drawable.ic_sleep_3
       4 -> R.drawable.ic_sleep_4
       5 -> R.drawable.ic_sleep_5
       else -> R.drawable.ic_sleep_active
   })
}

Шаг 2. Обновите SleepNightAdapter

  1. Откройте SleepNightAdapter.kt .
  2. Удалите все в методе bind() , поскольку теперь вы можете использовать привязку данных и новые адаптеры, которые выполнят эту работу за вас.
fun bind(item: SleepNight) {
}
  1. Внутри bind() назначьте sleep item , поскольку вам нужно сообщить объекту привязки о вашем новом SleepNight .
binding.sleep = item
  1. Ниже этой строки добавьте binding.executePendingBindings() . Этот вызов представляет собой оптимизацию, которая требует от привязки данных немедленно выполнить все ожидающие привязки. Всегда рекомендуется вызывать executePendingBindings() при использовании адаптеров привязки в RecyclerView , поскольку это может немного ускорить изменение размера представлений.
 binding.executePendingBindings()

Шаг 3: Добавьте привязки к XML-макету

  1. Откройте list_item_sleep_night.xml .
  2. В ImageView добавьте свойство app с тем же именем, что и у адаптера привязки, который задаёт изображение. Передайте переменную sleep , как показано ниже.

    Это свойство создаёт связь между представлением и объектом привязки через адаптер. При каждом обращении к sleepImage адаптер адаптирует данные из SleepNight .
app:sleepImage="@{sleep}"
  1. Сделайте то же самое для текстовых представлений sleep_length и quality_string . При каждом обращении к sleepDurationFormatted или sleepQualityString адаптеры адаптируют данные из SleepNight .
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
  1. Запустите приложение. Оно работает точно так же, как и раньше. Адаптеры привязки берут на себя всю работу по форматированию и обновлению представлений по мере изменения данных, упрощая ViewHolder и обеспечивая гораздо лучшую структуру кода, чем раньше.

В последних нескольких упражнениях вы видели один и тот же список. Это сделано намеренно, чтобы показать вам, что интерфейс Adapter позволяет проектировать код различными способами. Чем сложнее ваш код, тем важнее его грамотная архитектура. В рабочих приложениях эти и другие шаблоны используются с RecyclerView . Все эти шаблоны работают, и у каждого есть свои преимущества. Выбор зависит от того, что именно вы разрабатываете.

Поздравляем! Вы на верном пути к освоению RecyclerView на Android.

Проект Android Studio: RecyclerViewDiffUtilDataBinding .

DiffUtil :

  • RecyclerView имеет класс DiffUtil , который предназначен для вычисления разницы между двумя списками.
  • У DiffUtil есть класс ItemCallBack , который вы расширяете, чтобы выяснить разницу между двумя списками.
  • В классе ItemCallback необходимо переопределить методы areItemsTheSame() и areContentsTheSame() .

ListAdapter :

  • Чтобы получить возможность управлять списками бесплатно, вы можете использовать класс ListAdapter вместо RecyclerView.Adapter . Однако при использовании ListAdapter вам придётся написать собственный адаптер для других макетов, поэтому в этой лабораторной работе показано, как это сделать.
  • Чтобы открыть меню намерений в Android Studio, наведите курсор на любой элемент кода и нажмите Alt+Enter ( Option+Enter на Mac). Это меню особенно полезно для рефакторинга кода и создания заглушек для реализации методов. Меню контекстно-зависимо, поэтому для получения нужного меню необходимо точно установить курсор.

Привязка данных:

  • Используйте привязку данных в макете элемента для привязки данных к представлениям.

Адаптеры для привязки:

  • Ранее вы использовали Transformations для создания строк из данных. Если вам нужно связать данные разных или сложных типов, предоставьте адаптеры связывания, которые помогут механизму связывания данных использовать их.
  • Чтобы объявить адаптер привязки, определите метод, принимающий элемент и представление, и аннотируйте его @BindingAdapter . В Kotlin адаптер привязки можно написать как функцию расширения для View . Передайте имя свойства, которое адаптирует адаптер. Например:
@BindingAdapter("sleepDurationFormatted")
  • В XML-макете задайте свойство app с тем же именем, что и у адаптера привязки. Передайте переменную с данными. Например:
.app:sleepDurationFormatted="@{sleep}"

Курсы Udacity:

Документация для разработчиков Android:

Другие ресурсы:

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

  • При необходимости задавайте домашнее задание.
  • Объясните учащимся, как следует сдавать домашние задания.
  • Оцените домашние задания.

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

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

Ответьте на эти вопросы

Вопрос 1

Что из перечисленного необходимо для использования DiffUtil ? Выберите все подходящие варианты.

▢ Расширьте класс ItemCallBack .

▢ Переопределить areItemsTheSame() .

▢ Переопределить areContentsTheSame() .

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

Вопрос 2

Какие из следующих утверждений относительно адаптеров привязки верны?

▢ Адаптер привязки — это функция, аннотированная @BindingAdapter .

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

▢ Если вы хотите использовать адаптеры привязки, необходимо использовать RecyclerViewAdapter .

▢ Адаптеры привязки являются хорошим решением, когда вам нужно преобразовать сложные данные.

Вопрос 3

Когда следует использовать Transformations вместо адаптера привязки? Выберите все подходящие варианты.

▢ Ваши данные просты.

▢ Вы форматируете строку.

▢ Ваш список очень длинный.

▢ Ваш ViewHolder содержит только одно представление.

Начните следующий урок: 7.3: GridLayout с RecyclerView