Эта кодовая лаборатория является частью курса Android Kotlin Fundamentals. Вы получите максимальную отдачу от этого курса, если будете последовательно работать с лабораториями кода. Все кодовые лаборатории курса перечислены на целевой странице кодовых лабораторий Android Kotlin Fundamentals .
Введение
В этой лаборатории кода вы узнаете, как добавить заголовок, который охватывает ширину списка, отображаемого в RecyclerView
. Вы строите приложение для отслеживания сна из предыдущих лабораторий кода.
Что вы уже должны знать
- Как создать базовый пользовательский интерфейс, используя активность, фрагменты и представления.
- Как перемещаться между фрагментами и как использовать
safeArgs
для передачи данных между фрагментами. - Просматривайте модели, просматривайте фабрики моделей, преобразования и
LiveData
и их наблюдателей. - Как создать базу данных
Room
, создать DAO и определить сущности. - Как использовать сопрограммы для взаимодействия с базой данных и других длительных задач.
- Как реализовать базовый
RecyclerView
сAdapter
,ViewHolder
и макетом элемента. - Как реализовать привязку данных для
RecyclerView
. - Как создавать и использовать адаптеры привязки для преобразования данных.
- Как использовать
GridLayoutManager
. - Как захватывать и обрабатывать клики по элементам в
RecyclerView.
Что вы узнаете
- Как использовать более одного
ViewHolder
сRecyclerView
для добавления элементов с другим макетом. В частности, как использовать второйViewHolder
для добавления заголовка над элементами, отображаемыми вRecyclerView
.
Что ты будешь делать
- Создайте приложение TrackMySleepQuality из предыдущей кодовой лаборатории в этой серии.
- Добавьте заголовок, который занимает всю ширину экрана над количеством ночей сна, отображаемых в
RecyclerView
.
Приложение для отслеживания сна, с которым вы начинаете, имеет три экрана, представленных фрагментами, как показано на рисунке ниже.
Первый экран, показанный слева, имеет кнопки для запуска и остановки отслеживания. На экране показаны некоторые данные о сне пользователя. Кнопка « Очистить » безвозвратно удаляет все данные, которые приложение собрало для пользователя. Второй экран, показанный посередине, предназначен для выбора оценки качества сна. Третий экран представляет собой подробное представление, которое открывается, когда пользователь касается элемента в сетке.
Это приложение использует упрощенную архитектуру с контроллером пользовательского интерфейса, моделью представления и LiveData
, а также базу данных Room
для сохранения данных о сне.
В этой кодовой лаборатории вы добавляете заголовок к сетке отображаемых элементов. Ваш окончательный главный экран будет выглядеть так:
Эта лаборатория кода учит общему принципу включения элементов, использующих разные макеты, в RecyclerView
. Одним из распространенных примеров являются заголовки в вашем списке или сетке. Список может иметь один заголовок для описания содержимого элемента. Список также может иметь несколько заголовков для группировки и разделения элементов в одном списке.
RecyclerView
ничего не знает о ваших данных или о том, какой тип макета имеет каждый элемент. LayoutManager
упорядочивает элементы на экране, но адаптер адаптирует отображаемые данные и передает держатели представления в RecyclerView
. Таким образом, вы добавите код для создания заголовков в адаптере.
Два способа добавления заголовков
В RecyclerView
каждому элементу в списке соответствует порядковый номер, начинающийся с 0. Например:
[Фактические данные] -> [Виды адаптера]
[0: Ночь сна] -> [0: Ночь сна]
[1: Ночь сна] -> [1: Ночь сна]
[2: Ночь сна] -> [2: Ночь сна]
Один из способов добавить заголовки в список — изменить адаптер для использования другого ViewHolder
, проверив индексы, в которых должен отображаться ваш заголовок. Adapter
будет отвечать за отслеживание заголовка. Например, чтобы отобразить заголовок в верхней части таблицы, вам нужно вернуть другой ViewHolder
для заголовка при размещении элемента с нулевым индексом. Затем все остальные элементы будут сопоставлены со смещением заголовка, как показано ниже.
[Фактические данные] -> [Виды адаптера]
[0: Заголовок]
[0: Ночь сна] -> [1: Ночь сна]
[1: Ночь сна] -> [2: Ночь сна]
[2: Ночь сна] -> [3: Ночь сна.
Другой способ добавить заголовки — изменить резервный набор данных для вашей сетки данных. Поскольку все данные, которые необходимо отобразить, хранятся в списке, вы можете изменить список, включив в него элементы, представляющие заголовок. Это немного проще для понимания, но вам нужно подумать о том, как вы проектируете свои объекты, чтобы вы могли объединять различные типы элементов в один список. Реализованный таким образом, адаптер будет отображать переданные ему элементы. Таким образом, элемент в позиции 0 — это заголовок, а элемент в позиции 1 — это SleepNight
, который напрямую сопоставляется с тем, что находится на экране.
[Фактические данные] -> [Виды адаптера]
[0: Заголовок] -> [0: Заголовок]
[1: Ночь сна] -> [1: Ночь сна]
[2: Ночь сна] -> [2: Ночь сна]
[3: Ночь сна] -> [3: Ночь сна]
Каждая методология имеет преимущества и недостатки. Изменение набора данных не приводит к значительным изменениям в остальной части кода адаптера, и вы можете добавить логику заголовка, манипулируя списком данных. С другой стороны, использование другого ViewHolder
путем проверки индексов на наличие заголовков дает больше свободы при компоновке заголовка. Это также позволяет адаптеру управлять тем, как данные адаптируются к представлению, не изменяя резервные данные.
В этой кодовой лаборатории вы обновляете свой RecyclerView
, чтобы отображать заголовок в начале списка. В этом случае ваше приложение будет использовать другой ViewHolder
для заголовка, чем для элементов данных. Приложение проверит индекс списка, чтобы определить, какой ViewHolder
использовать.
Шаг 1: Создайте класс DataItem
Чтобы абстрагироваться от типа элемента и позволить адаптеру работать только с «элементами», вы можете создать класс держателя данных, представляющий либо SleepNight
, либо Header
. Ваш набор данных будет представлять собой список элементов держателя данных.
Вы можете получить начальное приложение с GitHub или продолжить использовать приложение SleepTracker, созданное в предыдущей лаборатории кода.
- Загрузите код RecyclerViewHeaders-Starter с GitHub. Каталог RecyclerViewHeaders-Starter содержит начальную версию приложения SleepTracker, необходимую для этой лаборатории кода. Вы также можете продолжить работу с готовым приложением из предыдущей кодовой лаборатории, если хотите.
- Откройте SleepNightAdapter.kt .
- Под классом
SleepNightListener
на верхнем уровне определитеsealed
класс с именемDataItem
, представляющий элемент данных.
sealed
класс определяет закрытый тип, что означает, что все подклассыDataItem
должны быть определены в этом файле. В результате количество подклассов известно компилятору. Другая часть вашего кода не может определить новый типDataItem
, который может сломать ваш адаптер.
sealed class DataItem {
}
- Внутри тела класса
DataItem
определите два класса, которые представляют разные типы элементов данных. Первый — этоSleepNightItem
, являющийся оболочкойSleepNight
, поэтому он принимает единственное значение, называемоеsleepNight
. Чтобы сделать его частью закрытого класса, добавьте к нему расширениеDataItem
.
data class SleepNightItem(val sleepNight: SleepNight): DataItem()
- Второй класс —
Header
для представления заголовка. Поскольку в заголовке нет фактических данных, вы можете объявить его какobject
. Это означает, что всегда будет только один экземплярHeader
. Опять же, пусть он расширяетDataItem
.
object Header: DataItem()
- Внутри
DataItem
на уровне класса определитеabstract
свойствоLong
с именемid
. Когда адаптер используетDiffUtil
для определения того, изменился ли элемент и каким образом,DiffItemCallback
должен знать идентификатор каждого элемента. Вы увидите ошибку, потому чтоSleepNightItem
иHeader
должны переопределитьid
абстрактного свойства.
abstract val id: Long
- В
SleepNightItem
переопределитеid
, чтобы вернутьnightId
.
override val id = sleepNight.nightId
- В
Header
переопределитеid
, чтобы он возвращалLong.MIN_VALUE
, что является очень-очень маленьким числом (буквально, -2 в степени 63). Таким образом, это никогда не будет конфликтовать ни с однимnightId
.
override val id = Long.MIN_VALUE
- Ваш готовый код должен выглядеть так, и ваше приложение должно собираться без ошибок.
sealed class DataItem {
abstract val id: Long
data class SleepNightItem(val sleepNight: SleepNight): DataItem() {
override val id = sleepNight.nightId
}
object Header: DataItem() {
override val id = Long.MIN_VALUE
}
}
Шаг 2: Создайте ViewHolder для заголовка
- Создайте макет для заголовка в новом файле ресурсов макета с именем header.xml , который отображает
TextView
. В этом нет ничего захватывающего, так что вот код.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Sleep Results"
android:padding="8dp" />
- Извлеките
"Sleep Results"
в строковый ресурс и назовите егоheader_text
.
<string name="header_text">Sleep Results</string>
- В SleepNightAdapter.kt внутри
SleepNightAdapter
над классомViewHolder
создайте новый классTextViewHolder
. Этот класс расширяет макет textview.xml и возвращает экземплярTextViewHolder
. Поскольку вы делали это раньше, вот код, и вам нужно будет импортироватьView
иR
:
class TextViewHolder(view: View): RecyclerView.ViewHolder(view) {
companion object {
fun from(parent: ViewGroup): TextViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater.inflate(R.layout.header, parent, false)
return TextViewHolder(view)
}
}
}
Шаг 3. Обновите SleepNightAdapter
Далее вам нужно обновить объявление SleepNightAdapter
. Вместо того, чтобы поддерживать только один тип ViewHolder
, он должен иметь возможность использовать любой тип держателя представления.
Определить типы предметов
- В
SleepNightAdapter.kt
на верхнем уровне, ниже операторовimport
и вышеSleepNightAdapter
, определите две константы для типов представлений.
RecyclerView
должен будет различать тип представления каждого элемента, чтобы он мог правильно назначить ему владельца представления.
private val ITEM_VIEW_TYPE_HEADER = 0
private val ITEM_VIEW_TYPE_ITEM = 1
- Внутри
SleepNightAdapter
создайте функцию для переопределенияgetItemViewType()
, чтобы вернуть правильный заголовок или константу элемента в зависимости от типа текущего элемента.
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is DataItem.Header -> ITEM_VIEW_TYPE_HEADER
is DataItem.SleepNightItem -> ITEM_VIEW_TYPE_ITEM
}
}
Обновите определение SleepNightAdapter.
- В определении
SleepNightAdapter
измените первый аргумент дляListAdapter
сSleepNight
наDataItem
. - В определении
SleepNightAdapter
измените второй универсальный аргумент дляListAdapter
сSleepNightAdapter.ViewHolder
наRecyclerView.ViewHolder
. Вы увидите некоторые ошибки для необходимых обновлений, а заголовок вашего класса должен выглядеть так, как показано ниже.
class SleepNightAdapter(val clickListener: SleepNightListener):
ListAdapter<DataItem, RecyclerView.ViewHolder>(SleepNightDiffCallback()) {
Обновление onCreateViewHolder()
- Измените подпись
onCreateViewHolder()
, чтобы она возвращалаRecyclerView.ViewHolder
.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
- Разверните реализацию метода
onCreateViewHolder()
, чтобы проверить и вернуть соответствующий держатель представления для каждого типа элемента. Ваш обновленный метод должен выглядеть так, как показано ниже.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ITEM_VIEW_TYPE_HEADER -> TextViewHolder.from(parent)
ITEM_VIEW_TYPE_ITEM -> ViewHolder.from(parent)
else -> throw ClassCastException("Unknown viewType ${viewType}")
}
}
Обновление onBindViewHolder()
- Измените тип параметра
onBindViewHolder()
сViewHolder
наRecyclerView.ViewHolder
.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int)
- Добавьте условие, чтобы назначать данные владельцу представления только в том случае, если держатель является
ViewHolder
.
when (holder) {
is ViewHolder -> {...}
- Приведите тип объекта, возвращенный
getItem()
, кDataItem.SleepNightItem
. Ваша готоваяonBindViewHolder()
должна выглядеть так.
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val nightItem = getItem(position) as DataItem.SleepNightItem
holder.bind(nightItem.sleepNight, clickListener)
}
}
}
Обновите обратные вызовы diffUtil
- Измените методы в
SleepNightDiffCallback
, чтобы использовать новый классDataItem
вместоSleepNight
. Подавите предупреждение о ворсе, как показано в приведенном ниже коде.
class SleepNightDiffCallback : DiffUtil.ItemCallback<DataItem>() {
override fun areItemsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: DataItem, newItem: DataItem): Boolean {
return oldItem == newItem
}
}
Добавьте и отправьте заголовок
- Внутри
SleepNightAdapter
нижеonCreateViewHolder()
определите функциюaddHeaderAndSubmitList()
, как показано ниже. Эта функция принимает списокSleepNight
. Вместо использованияsubmitList()
, предоставленногоListAdapter
, для отправки вашего списка, вы будете использовать эту функцию, чтобы добавить заголовок, а затем отправить список.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {}
- Внутри
addHeaderAndSubmitList()
, если переданный список имеет значениеnull
, верните только заголовок, в противном случае прикрепите заголовок к началу списка, а затем отправьте список.
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
submitList(items)
- Откройте SleepTrackerFragment.kt и измените вызов
submitList()
наaddHeaderAndSubmitList()
.
- Запустите приложение и посмотрите, как ваш заголовок отображается как первый элемент в списке элементов сна.
Есть две вещи, которые необходимо исправить для этого приложения. Один виден, а другой нет.
- Заголовок отображается в верхнем левом углу, и его нелегко различить.
- Это не имеет большого значения для короткого списка с одним заголовком, но вы не должны манипулировать списком в
addHeaderAndSubmitList()
в потоке пользовательского интерфейса. Представьте себе список с сотнями элементов, несколькими заголовками и логикой, позволяющей решить, куда нужно вставить элементы. Эта работа принадлежит сопрограмме.
Измените addHeaderAndSubmitList()
, чтобы использовать сопрограммы:
- На верхнем уровне внутри класса
SleepNightAdapter
определитеCoroutineScope
сDispatchers.Default
.
private val adapterScope = CoroutineScope(Dispatchers.Default)
- В
addHeaderAndSubmitList()
запустите сопрограмму вadapterScope
для управления списком. Затем переключитесь в контекстDispatchers.Main
, чтобы отправить список, как показано в приведенном ниже коде.
fun addHeaderAndSubmitList(list: List<SleepNight>?) {
adapterScope.launch {
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
- Ваш код должен собираться и работать, и вы не увидите никакой разницы.
В настоящее время заголовок имеет ту же ширину, что и другие элементы в сетке, занимая один интервал по горизонтали и вертикали. Вся сетка умещает три элемента шириной в один пролет по горизонтали, поэтому заголовок должен использовать три отрезка по горизонтали.
Чтобы исправить ширину заголовка, вам нужно сообщить GridLayoutManager
, когда нужно распределить данные по всем столбцам. Это можно сделать, настроив SpanSizeLookup
в GridLayoutManager
. Это объект конфигурации, который GridLayoutManager
использует, чтобы определить, сколько диапазонов использовать для каждого элемента в списке.
- Откройте SleepTrackerFragment.kt .
- Найдите код, в котором вы определяете
manager
, ближе к концуonCreateView()
.
val manager = GridLayoutManager(activity, 3)
- Под
manager
определитеmanager.spanSizeLookup
, как показано. Вам нужно создатьobject
, потому чтоsetSpanSizeLookup
не принимает лямбду. Чтобы создатьobject
в Kotlin, введитеobject : classname
, в данном случаеGridLayoutManager.SpanSizeLookup
.
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
}
- Вы можете получить ошибку компилятора при вызове конструктора. Если вы это сделаете, откройте меню намерений с помощью
Option+Enter
(Mac) илиAlt+Enter
(Windows), чтобы применить вызов конструктора.
- Затем вы получите сообщение об ошибке
object
, в котором говорится, что вам нужно переопределить методы. Поместите курсор наobject
, нажмитеOption+Enter
(Mac) илиAlt+Enter
(Windows), чтобы открыть меню намерений, затем переопределите методgetSpanSize()
.
- В теле
getSpanSize()
верните правильный размер диапазона для каждой позиции. Позиция 0 имеет размер пролета 3, а остальные позиции имеют размер пролета 1. Ваш завершенный код должен выглядеть так, как показано ниже:
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = when (position) {
0 -> 3
else -> 1
}
}
- Чтобы улучшить внешний вид заголовка, откройте файл header.xml и добавьте этот код в файл макета header.xml .
android:textColor="@color/white_text_color"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:background="@color/colorAccent"
- Запустите свое приложение. Это должно выглядеть как на скриншоте ниже.
Поздравляем! Вы сделали.
Проект Android Studio: RecyclerViewHeaders
- Заголовок обычно представляет собой элемент, занимающий всю ширину списка и выступающий в качестве заголовка или разделителя. Список может иметь один заголовок для описания содержимого элемента или несколько заголовков для группировки элементов и их отделения друг от друга.
-
RecyclerView
может использовать несколько держателей представлений для размещения разнородного набора элементов; например, заголовки и элементы списка. - Один из способов добавить заголовки — изменить адаптер для использования другого
ViewHolder
, проверив индексы, в которых должен отображаться ваш заголовок.Adapter
отвечает за отслеживание заголовка. - Другой способ добавить заголовки — изменить базовый набор данных (список) для вашей сетки данных, что вы и сделали в этой кодовой лаборатории.
Вот основные шаги для добавления заголовка:
- Абстрагируйте данные в своем списке, создав
DataItem
, который может содержать заголовок или данные. - Создайте держатель представления с макетом для заголовка в адаптере.
- Обновите адаптер и его методы, чтобы использовать
RecyclerView.ViewHolder
любого типа. - В
onCreateViewHolder()
верните правильный тип держателя представления для элемента данных. - Обновите
SleepNightDiffCallback
для работы с классомDataItem
. - Создайте функцию
addHeaderAndSubmitList()
, которая использует сопрограммы для добавления заголовка в набор данных, а затем вызываетsubmitList()
. -
GridLayoutManager.SpanSizeLookup()
, чтобы ширина заголовка составляла только три интервала.
Удасити курс:
Документация для разработчиков Android:
В этом разделе перечислены возможные домашние задания для студентов, которые работают с этой кодовой лабораторией в рамках курса, проводимого инструктором. Инструктор должен сделать следующее:
- При необходимости задайте домашнее задание.
- Объясните учащимся, как сдавать домашние задания.
- Оценивайте домашние задания.
Преподаватели могут использовать эти предложения так мало или так часто, как они хотят, и должны свободно давать любые другие домашние задания, которые они считают подходящими.
Если вы работаете с этой кодовой лабораторией самостоятельно, не стесняйтесь использовать эти домашние задания, чтобы проверить свои знания.
Ответьте на эти вопросы
Вопрос 1
Какое из следующих утверждений о ViewHolder
?
▢ Адаптер может использовать несколько классов ViewHolder
для хранения заголовков и различных типов данных.
▢ У вас может быть только один держатель представления для данных и один держатель представления для заголовка.
▢ RecyclerView
поддерживает несколько типов заголовков, но данные должны быть унифицированы.
▢ При добавлении заголовка вы создаете подкласс RecyclerView
, чтобы вставить заголовок в правильную позицию.
вопрос 2
Когда следует использовать сопрограммы с RecyclerView
? Выберите все утверждения, которые верны.
▢ Никогда. RecyclerView
является элементом пользовательского интерфейса и не должен использовать сопрограммы.
▢ Используйте сопрограммы для длительных задач, которые могут замедлить работу пользовательского интерфейса.
▢ Манипуляции со списками могут занять много времени, и вы всегда должны выполнять их с помощью сопрограмм.
▢ Используйте сопрограммы с функциями приостановки, чтобы избежать блокировки основного потока.
Вопрос 3
Что из следующего вам НЕ нужно делать при использовании более чем одного ViewHolder
?
▢ В ViewHolder
предоставьте несколько файлов макета, которые можно будет увеличивать по мере необходимости.
▢ В onCreateViewHolder()
вернуть правильный тип держателя представления для элемента данных.
▢ В onBindViewHolder()
данные только в том случае, если держатель представления является правильным типом держателя представления для элемента данных.
▢ Обобщите сигнатуру класса адаптера, чтобы она принимала любой RecyclerView.ViewHolder
.
Начать следующий урок:
Ссылки на другие лаборатории кода в этом курсе см. на целевой странице лаборатории кода Android Kotlin Fundamentals .