Эта практическая работа входит в курс «Основы 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.
- При необходимости загрузите приложение RecyclerViewDiffUtilDataBinding-Starter с GitHub и откройте проект в Android Studio.
- Запустите приложение.
- Откройте файл
SleepNightAdapter.kt. - Изучите код, чтобы ознакомиться со структурой приложения. На схеме ниже представлен обзор использования
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 .
- Откройте
SleepNightAdapter.kt. - Ниже полного определения класса
SleepNightAdapterсоздайте новый класс верхнего уровняSleepNightDiffCallback, расширяющийDiffUtil.ItemCallback. ПередайтеSleepNightкак универсальный параметр.
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}- Установите курсор на имя класса
SleepNightDiffCallback. - Нажмите
Alt+Enter(Option+Enterна Mac) и выберите Реализовать элементы . - В открывшемся диалоговом окне щелкните левой кнопкой мыши, удерживая клавишу 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.
}- Внутри
areItemsTheSame()заменитеTODOкодом, который проверяет, одинаковы ли два переданных элементаSleepNight,oldItemиnewItem. Если у элементов одинаковыйnightId, это один и тот же элемент, поэтому возвращаемtrue. В противном случае возвращаемfalse.DiffUtilиспользует эту проверку, чтобы определить, был ли элемент добавлен, удалён или перемещён.
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
return oldItem.nightId == newItem.nightId
}- Внутри
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
- В файле
SleepNightAdapter.ktизмените сигнатуру классаSleepNightAdapter, чтобы расширитьListAdapter. - При появлении запроса импортируйте
androidx.recyclerview.widget.ListAdapter. - Добавьте
SleepNightв качестве первого аргументаListAdapterпередSleepNightAdapter.ViewHolder. - Добавьте
SleepNightDiffCallback()в качестве параметра конструктора.ListAdapterбудет использовать его для определения изменений в списке. Готовая сигнатура классаSleepNightAdapterдолжна выглядеть так, как показано ниже.
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {- Внутри класса
SleepNightAdapterудалите полеdata, включая сеттер. Оно вам больше не понадобится, посколькуListAdapterотслеживает список автоматически. - Удалите переопределение
getItemCount(), посколькуListAdapterреализует этот метод за вас. - Чтобы избавиться от ошибки в
onBindViewHolder(), измените переменнуюitem. Вместо использованияdataдля полученияitemвызовите методgetItem(position)предоставляемыйListAdapter.
val item = getItem(position)Шаг 2: Используйте метод submitList() для обновления списка.
Ваш код должен сообщать ListAdapter о доступности изменённого списка. ListAdapter предоставляет метод submitList() который сообщает ListAdapter о доступности новой версии списка. При вызове этого метода ListAdapter сравнивает новый список со старым и обнаруживает элементы, которые были добавлены, удалены, перемещены или изменены. Затем ListAdapter обновляет элементы, отображаемые RecyclerView .
- Откройте
SleepTrackerFragment.kt. - В
onCreateView(), в наблюдателеsleepTrackerViewModel, найдите ошибку, в которой ссылается переменнаяdata, которую вы удалили. - Замените
adapter.data = itвызовомadapter.submitList(it). Обновлённый код показан ниже.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.submitList(it)
}
})- Запустите приложение. Оно работает быстрее, возможно, незаметно, если список небольшой.
В этом задании вы используете ту же технику, что и в предыдущих практикумах, для настройки привязки данных и исключаете вызовы findViewById() .
Шаг 1: Добавьте привязку данных к файлу макета
- Откройте файл макета
list_item_sleep_night.xmlна вкладке Текст . - Установите курсор на тег
ConstraintLayoutи нажмитеAlt+Enter(Option+Enter(на Mac) Откроется меню намерений (меню «быстрого исправления»). - Выберите «Преобразовать в макет привязки данных» . Макет будет упакован в тег
<layout>и внутри него будет добавлен тег<data>. - При необходимости прокрутите страницу обратно наверх и внутри тега
<data>объявите переменную с именемsleep. - Укажите его
typeсоответствующий полному имениSleepNight,com.example.android.trackmysleepquality.database.SleepNight. Готовый тег<data>должен выглядеть так, как показано ниже.
<data>
<variable
name="sleep"
type="com.example.android.trackmysleepquality.database.SleepNight"/>
</data>- Чтобы принудительно создать объект
Binding, выберите Build > Clean Project , а затем Build > Rebuild Project . (Если проблемы не исчезли, выберите File > Invalidate Caches / Restart .) Объект привязкиListItemSleepNightBindingвместе с соответствующим кодом добавляется в сгенерированные файлы проекта.
Шаг 2: Расширьте макет элемента, используя привязку данных
- Откройте
SleepNightAdapter.kt. - В классе
ViewHolderнайдите методfrom(). - Удалить объявление переменной
view.
Код для удаления :
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)- Там, где была переменная
view, определите новую переменную с именемbinding, которая расширяет объект привязкиListItemSleepNightBinding, как показано ниже. Выполните необходимый импорт объекта привязки.
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)- В конце функции вместо возврата
viewвернитеbinding.
return ViewHolder(binding)- Чтобы устранить ошибку, наведите курсор на слово
binding. НажмитеAlt+Enter(Option+Enterна Mac), чтобы открыть меню намерений.
- Выберите «Изменить тип параметра itemView основного конструктора класса ViewHolder» на ListItemSleepNightBinding» . Это обновит тип параметра класса
ViewHolder.

- Прокрутите страницу вверх до определения класса
ViewHolder, чтобы увидеть изменение в сигнатуре. Вы видите ошибку дляitemView, поскольку вы изменилиitemViewнаbindingв методеfrom().
В определении классаViewHolderщёлкните правой кнопкой мыши по одному из вхожденийitemViewи выберите «Рефакторинг» > «Переименовать» . Измените имя наbinding. - Добавьте префикс
valbindingпараметра конструктора, чтобы сделать его свойством. - В вызове родительского класса
RecyclerView.ViewHolderизмените параметр сbindingнаbinding.root. Вам необходимо передатьView, аbinding.root— это корневойConstraintLayoutв вашем макете элемента. - Готовое объявление класса должно выглядеть так, как показано ниже.
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){Вы также видите ошибку при вызовах findViewById() , и исправьте ее следующим образом.
Шаг 3: Заменить findViewById()
Теперь вы можете обновить свойства sleepLength , quality и qualityImage , чтобы использовать объект binding вместо findViewById() .
- Измените инициализацию
sleepLength,qualityStringиqualityImage, чтобы использовать представления объектаbinding, как показано ниже. После этого ваш код больше не должен выдавать ошибок.
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImageПосле добавления объекта привязки вам больше не нужно определять свойства sleepLength , quality и qualityImage . DataBinding кэширует результаты поиска, поэтому нет необходимости объявлять эти свойства.
- Щёлкните правой кнопкой мыши по именам свойств
sleepLength,qualityиqualityImage. Выберите «Рефакторинг» > «Встроить » или нажмитеControl+Command+N(Option+Command+Nна Mac).
- Запустите приложение. (Возможно, вам придется очистить и пересобрать проект, если в нем есть ошибки.)
В этой задаче вы обновите свое приложение, чтобы использовать привязку данных с адаптерами привязки для установки данных в ваших представлениях.
В предыдущей лабораторной работе вы использовали класс Transformations для работы LiveData и генерации форматированных строк для отображения в текстовых представлениях. Однако, если вам нужно привязать различные типы данных или сложные типы, вы можете предоставить адаптеры привязки, которые помогут привязке данных использовать эти типы. Адаптеры привязки — это адаптеры, которые принимают ваши данные и преобразуют их в нечто, что можно использовать при привязке данных для привязки представления, например, текст или изображение.
Вам предстоит реализовать три адаптера привязки: один для изображения качества и по одному для каждого текстового поля. Подводя итог, чтобы объявить адаптер привязки, вы определяете метод, принимающий элемент и представление, и аннотируете его @BindingAdapter . В теле метода реализуется преобразование. В Kotlin адаптер привязки можно написать как функцию расширения для класса представления, принимающего данные.
Шаг 1: Создание адаптеров привязки
Обратите внимание, что вам придется импортировать несколько классов на этом этапе, и они не будут вызываться по отдельности.
- Откройте
SleepNightAdapater.kt. - Внутри класса
ViewHolderнайдите методbind()и запомните, что он делает. Вместо этого вы возьмёте код, вычисляющий значенияbinding.sleepLength,binding.qualityиbinding.qualityImage, и используете его внутри адаптера. (Пока оставьте код как есть; вы перенесёте его позже.) - В пакете
sleeptrackerсоздайте и откройте файлBindingUtils.kt. - Объявите функцию расширения для
TextViewс именемsetSleepDurationFormattedи передайте ейSleepNight. Эта функция будет вашим адаптером для расчета и форматирования продолжительности сна.
fun TextView.setSleepDurationFormatted(item: SleepNight) {}- В теле
setSleepDurationFormattedпривяжите данные к представлению, как это было сделано вViewHolder.bind(). ВызовитеconvertDurationToFormatted(), а затем установите форматированныйtextв качестве значения дляTextView. (Поскольку это функция расширенияTextView, вы можете напрямую обращаться к свойствуtext.)
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)- Чтобы сообщить привязке данных об этом адаптере привязки, аннотируйте функцию с помощью
@BindingAdapter. - Эта функция является адаптером для атрибута
sleepDurationFormatted, поэтому передайтеsleepDurationFormattedв качестве аргумента@BindingAdapter.
@BindingAdapter("sleepDurationFormatted")- Второй адаптер устанавливает качество сна на основе значения объекта
SleepNight. Создайте функцию расширенияsetSleepQualityString()дляTextViewи передайте ейSleepNight. - В теле привяжите данные к представлению, как это было сделано в
ViewHolder.bind(). ВызовитеconvertNumericQualityToStringи задайтеtext. - Добавьте к функции аннотацию
@BindingAdapter("sleepQualityString").
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
text = convertNumericQualityToString(item.sleepQuality, context.resources)
}- Третий адаптер привязки устанавливает изображение в поле представления. Создайте функцию расширения для
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
- Откройте
SleepNightAdapter.kt. - Удалите все в методе
bind(), поскольку теперь вы можете использовать привязку данных и новые адаптеры, которые выполнят эту работу за вас.
fun bind(item: SleepNight) {
}- Внутри
bind()назначьте sleepitem, поскольку вам нужно сообщить объекту привязки о вашем новомSleepNight.
binding.sleep = item- Ниже этой строки добавьте
binding.executePendingBindings(). Этот вызов представляет собой оптимизацию, которая требует от привязки данных немедленно выполнить все ожидающие привязки. Всегда рекомендуется вызыватьexecutePendingBindings()при использовании адаптеров привязки вRecyclerView, поскольку это может немного ускорить изменение размера представлений.
binding.executePendingBindings()Шаг 3: Добавьте привязки к XML-макету
- Откройте
list_item_sleep_night.xml. - В
ImageViewдобавьте свойствоappс тем же именем, что и у адаптера привязки, который задаёт изображение. Передайте переменнуюsleep, как показано ниже.
Это свойство создаёт связь между представлением и объектом привязки через адаптер. При каждом обращении кsleepImageадаптер адаптирует данные изSleepNight.
app:sleepImage="@{sleep}"- Сделайте то же самое для текстовых представлений
sleep_lengthиquality_string. При каждом обращении кsleepDurationFormattedилиsleepQualityStringадаптеры адаптируют данные изSleepNight.
app:sleepDurationFormatted="@{sleep}"app:sleepQualityString="@{sleep}"- Запустите приложение. Оно работает точно так же, как и раньше. Адаптеры привязки берут на себя всю работу по форматированию и обновлению представлений по мере изменения данных, упрощая
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:
- Создайте список с помощью RecyclerView
-
RecyclerView -
DiffUtil - Библиотека привязки данных
- Адаптеры для привязки
-
notifyDataSetChanged() -
Transformations
Другие ресурсы:
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Ответьте на эти вопросы
Вопрос 1
Что из перечисленного необходимо для использования DiffUtil ? Выберите все подходящие варианты.
▢ Расширьте класс ItemCallBack .
▢ Переопределить areItemsTheSame() .
▢ Переопределить areContentsTheSame() .
▢ Используйте привязку данных для отслеживания различий между элементами.
Вопрос 2
Какие из следующих утверждений относительно адаптеров привязки верны?
▢ Адаптер привязки — это функция, аннотированная @BindingAdapter .
▢ Использование адаптера привязки позволяет отделить форматирование данных от держателя представления.
▢ Если вы хотите использовать адаптеры привязки, необходимо использовать RecyclerViewAdapter .
▢ Адаптеры привязки являются хорошим решением, когда вам нужно преобразовать сложные данные.
Вопрос 3
Когда следует использовать Transformations вместо адаптера привязки? Выберите все подходящие варианты.
▢ Ваши данные просты.
▢ Вы форматируете строку.
▢ Ваш список очень длинный.
▢ Ваш ViewHolder содержит только одно представление.
Начните следующий урок:

