Эта практическая работа входит в курс «Основы Android Kotlin». Вы получите максимальную пользу от этого курса, если будете выполнять практические работы последовательно. Все практические работы курса перечислены на целевой странице практической работы «Основы Android Kotlin» .
Введение
В этой практической работе вы научитесь использовать RecyclerView
для отображения списков элементов. Развивая приложение для отслеживания сна из предыдущей серии практических занятий, вы изучите более эффективный и универсальный способ отображения данных с помощью RecyclerView
с рекомендуемой архитектурой.
Что вам уже следует знать
Вам должно быть знакомо:
- Создание базового пользовательского интерфейса (UI) с использованием активности, фрагментов и представлений.
- Навигация между фрагментами и использование
safeArgs
для передачи данных между фрагментами. - Использование моделей представлений, фабрик моделей представлений, преобразований, а также
LiveData
и их наблюдателей. - Создание базы данных
Room
, создание DAO и определение сущностей. - Использование сопрограмм для задач базы данных и других длительных задач.
Чему вы научитесь
- Как использовать
RecyclerView
сAdapter
иViewHolder
для отображения списка элементов.
Что ты будешь делать?
- Измените приложение TrackMySleepQuality из предыдущего урока так, чтобы оно использовало
RecyclerView
для отображения данных о качестве сна.
В этой лабораторной работе вы создадите компонент RecyclerView
приложения, отслеживающего качество сна. Приложение использует базу данных Room
для хранения данных о сне с течением времени.
Стартовое приложение для отслеживания сна имеет два экрана, представленных фрагментами, как показано на рисунке ниже.
На первом экране, показанном слева, расположены кнопки для запуска и остановки отслеживания. Здесь также отображаются все данные о сне пользователя. Кнопка «Очистить» безвозвратно удаляет все данные, собранные приложением. Второй экран, показанный справа, предназначен для выбора оценки качества сна.
Это приложение использует упрощённую архитектуру с контроллером пользовательского интерфейса, ViewModel
и LiveData
. Приложение также использует базу данных Room
для сохранения данных о сне.
Список ночей сна, отображаемый на первом экране, функционален, но не слишком привлекателен. Приложение использует сложный форматировщик для создания текстовых строк для текстового представления и числовых значений для оценки качества. Кроме того, этот дизайн не масштабируется. После исправления всех этих проблем в этой практической работе финальное приложение будет иметь ту же функциональность, а главный экран будет выглядеть так:
Отображение списка или таблицы данных — одна из самых распространённых задач пользовательского интерфейса в Android. Списки могут быть как простыми, так и очень сложными. Список текстовых представлений может отображать простые данные, например, список покупок. Сложный список, например, аннотированный список мест отдыха, может отображать множество данных внутри прокручиваемой сетки с заголовками.
Для поддержки всех этих вариантов использования Android предоставляет виджет RecyclerView
.
Наибольшее преимущество RecyclerView
заключается в том, что он очень эффективен для больших списков:
- По умолчанию
RecyclerView
обрабатывает и отображает только те элементы, которые в данный момент видны на экране. Например, если ваш список содержит тысячу элементов, но видны только 10,RecyclerView
выполняет ровно столько работы, сколько необходимо для отображения 10 элементов на экране. При прокрутке страницы пользователемRecyclerView
определяет, какие новые элементы должны быть на экране, и выполняет ровно столько работы, сколько необходимо для их отображения. - Когда элемент прокручивается за пределы экрана, его представления перезаписываются. Это означает, что элемент заполняется новым контентом, который прокручивается на экран. Такое поведение
RecyclerView
значительно экономит время обработки и обеспечивает плавную прокрутку списков. - При изменении элемента
RecyclerView
может обновить только один элемент, не перерисовывая весь список. Это значительно повышает эффективность при отображении сложных списков!
В последовательности, показанной ниже, видно, что одно представление заполнено данными, ABC
. После того, как это представление прокручивается за пределы экрана, RecyclerView
повторно использует его для новых данных, XYZ
.
Шаблон адаптера
Если вы когда-либо путешествовали между странами, где используются разные розетки, вы, вероятно, знаете, как подключать устройства к розеткам с помощью адаптера. Адаптер позволяет преобразовать один тип вилки в другой, то есть фактически преобразовать один интерфейс в другой.
Шаблон «адаптер» в программной инженерии помогает объекту работать с другим API. RecyclerView
использует адаптер для преобразования данных приложения в то, что RecyclerView
может отобразить, не изменяя способ хранения и обработки данных приложением. Для приложения для отслеживания сна вы создаете адаптер, который адаптирует данные из базы данных Room
к тому, что RecyclerView
умеет отображать, не изменяя ViewModel
.
Реализация RecyclerView
Для отображения данных в RecyclerView
вам понадобятся следующие компоненты:
- Данные для отображения.
- Экземпляр
RecyclerView
, определенный в файле макета, который будет действовать как контейнер для представлений. - Макет для одного элемента данных.
Если все элементы списка выглядят одинаково, вы можете использовать для них один и тот же макет, но это не обязательно. Макет элемента должен быть создан отдельно от макета фрагмента, чтобы можно было создавать и заполнять данными только одно представление элемента за раз. - Менеджер по макетированию.
Менеджер макетов управляет организацией (макетом) компонентов пользовательского интерфейса в представлении. - Обладатель мнения.
Держатель представления расширяет классViewHolder
. Он содержит информацию о представлении для отображения одного элемента из макета. Держатели представления также добавляют информацию, которуюRecyclerView
использует для эффективного перемещения представлений по экрану. - Адаптер.
Адаптер подключает ваши данные кRecyclerView
. Он адаптирует данные для отображения вViewHolder
.RecyclerView
использует адаптер для определения способа отображения данных на экране.
В этой задаче вы добавите RecyclerView
в свой файл макета и настроите Adapter
для предоставления данных о сне в RecyclerView
.
Шаг 1: Добавьте RecyclerView с LayoutManager
На этом этапе вы заменяете ScrollView
на RecyclerView
в файле fragment_sleep_tracker.xml
.
- Загрузите приложение RecyclerViewFundamentals-Starter с GitHub.
- Создайте и запустите приложение. Обратите внимание, что данные отображаются в виде простого текста.
- Откройте файл макета
fragment_sleep_tracker.xml
на вкладке «Дизайн» в Android Studio. - На панели «Дерево компонентов» удалите
ScrollView
. Это действие также удалитTextView
, находящийся внутриScrollView
. - На панели «Палитра» прокрутите список типов компонентов слева, чтобы найти «Контейнеры» , затем выберите его.
- Перетащите
RecyclerView
из панели «Палитра» на панель «Дерево компонентов» . ПоместитеRecyclerView
вConstraintLayout
.
- Если откроется диалоговое окно с вопросом о добавлении зависимости, нажмите «ОК» , чтобы Android Studio добавила зависимость
recyclerview
в ваш Gradle-файл. Это может занять несколько секунд, после чего ваше приложение будет синхронизировано.
- Откройте файл модуля
build.gradle
, прокрутите до конца и обратите внимание на новую зависимость, которая выглядит примерно так, как показано на коде ниже:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
- Вернитесь к
fragment_sleep_tracker.xml
. - На вкладке «Текст» найдите код
RecyclerView
, показанный ниже:
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
- Присвойте
RecyclerView
id
sleep_list
.
android:id="@+id/sleep_list"
- Разместите
RecyclerView
так, чтобы он занимал оставшуюся часть экрана внутриConstraintLayout
. Для этого ограничьте верхнюю частьRecyclerView
кнопкой «Пуск» , нижнюю — кнопкой « Очистить », а каждую сторону — родительским элементом. Установите ширину и высоту макета равными 0 dp в редакторе макетов или в XML, используя следующий код:
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/clear_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/stop_button"
- Добавьте менеджер макета в XML-файл
RecyclerView
. КаждомуRecyclerView
нужен менеджер макета, который определяет расположение элементов в списке. В Android естьLinearLayoutManager
, который по умолчанию размещает элементы в вертикальном списке строк полной ширины.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
- Перейдите на вкладку «Конструктор» и обратите внимание, что добавленные ограничения привели к тому, что
RecyclerView
расширился, чтобы заполнить все доступное пространство.
Шаг 2: Создайте макет элемента списка и держатель текстового представления.
RecyclerView
— это всего лишь контейнер. На этом этапе вы создаёте макет и инфраструктуру для элементов, отображаемых внутри RecyclerView
.
Чтобы как можно быстрее получить работающий RecyclerView
, сначала используется простой элемент списка, отображающий только качество сна в виде числа. Для этого необходим контейнер представления TextItemViewHolder
. Также необходим контейнер представления TextView
для данных. (На следующем этапе вы узнаете больше о контейнерах представления и о том, как организовать все данные о сне.)
- Создайте файл макета с именем
text_item_view.xml
. Неважно, какой элемент вы используете в качестве корневого, так как вы замените код шаблона. - В
text_item_view.xml
удалите весь указанный код. - Добавьте
TextView
с отступами16dp
в начале и конце и размером текста24sp
. Ширина должна соответствовать родительскому элементу, а высота должна обтекать содержимое. Поскольку это представление отображается внутриRecyclerView
, вам не нужно помещать его внутрьViewGroup
.
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:textSize="24sp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
- Откройте
Util.kt
Прокрутите до конца и добавьте показанное ниже определение, которое создаёт классTextItemViewHolder
. Добавьте код в конец файла, после последней закрывающей скобки. Код добавляется вUtil.kt
поскольку этот контейнер представления временный, и вы замените его позже.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
- При появлении соответствующего запроса импортируйте
android.widget.TextView
иandroidx.recyclerview.widget.RecyclerView
.
Шаг 3: Создайте SleepNightAdapter
Основная задача при реализации RecyclerView
— создание адаптера. У вас есть простой держатель представления для представления элементов и макет для каждого элемента. Теперь вы можете создать адаптер. Адаптер создаёт держатель представления и заполняет его данными для отображения RecyclerView
.
- В пакете
sleeptracker
создайте новый класс Kotlin с именемSleepNightAdapter
. - Сделайте класс
SleepNightAdapter
расширениемRecyclerView.Adapter
. Класс называетсяSleepNightAdapter
, поскольку он адаптирует объектSleepNight
к типу, который может использоватьRecyclerView
. Адаптеру необходимо знать, какой держатель представления использовать, поэтому передайтеTextItemViewHolder
. Импортируйте необходимые компоненты при появлении запроса. В этом случае вы увидите ошибку, поскольку существуют обязательные методы для реализации.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
- На верхнем уровне
SleepNightAdapter
создайте переменнуюlistOf
SleepNight
для хранения данных.
var data = listOf<SleepNight>()
- В
SleepNightAdapter
переопределитеgetItemCount()
, чтобы он возвращал размер списка ночей сна вdata
.RecyclerView
необходимо знать, сколько элементов есть в адаптере, чтобы отобразить их, и он делает это, вызываяgetItemCount()
.
override fun getItemCount() = data.size
- В
SleepNightAdapter
переопределите функциюonBindViewHolder()
, как показано ниже.
ФункцияonBindViewHolder()
вызываетсяRecyclerView
для отображения данных одного элемента списка в указанной позиции. Таким образом, методonBindViewHolder()
принимает два аргумента: держатель представления и позицию привязываемых данных. В данном приложении держателем являетсяTextItemViewHolder
, а позицией — позиция в списке.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
- Внутри
onBindViewHolder()
создайте переменную для одного элемента в заданной позиции данных.
val item = data[position]
- У созданного вами
ViewHolder
есть свойствоtextView
. В методеonBindViewHolder()
установитеtext
textView
значение, соответствующее качеству сна. Этот код отображает только список чисел, но этот простой пример позволяет увидеть, как адаптер передаёт данные в ViewHolder и выводит их на экран.
holder.textView.text = item.sleepQuality.toString()
- В
SleepNightAdapter
переопределите и реализуйтеonCreateViewHolder()
, который вызывается, когдаRecyclerView
требуется держатель представления для представления элемента.
Эта функция принимает два параметра и возвращаетViewHolder
.parent
параметр, представляющий собой группу представлений, содержащую держатель представления, всегда являетсяRecyclerView
. ПараметрviewType
используется, когда в одномRecyclerView
есть несколько представлений. Например, если вы поместите список текстовых представлений, изображение и видео в одинRecyclerView
, функцияonCreateViewHolder()
должна знать, какой тип представления использовать.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
- В
onCreateViewHolder()
создайте экземплярLayoutInflater
.
Модуль расширения макета умеет создавать представления из XML-макетов.context
содержит информацию о том, как правильно расширить представление. В адаптере для представления Recycler всегда передаётся контекстparent
группы представлений, то естьRecyclerView
.
val layoutInflater = LayoutInflater.from(parent.context)
- В
onCreateViewHolder()
создайтеview
, попросивlayoutinflater
расширить его.
Передайте XML-макет для представления иparent
группу представлений для представления. Третий аргумент, логический, —attachToRoot
. Этот аргумент должен бытьfalse
, посколькуRecyclerView
автоматически добавит этот элемент в иерархию представлений, когда придёт время.
val view = layoutInflater
.inflate(R.layout.text_item_view, parent, false) as TextView
- В
onCreateViewHolder()
вернитеTextItemViewHolder
, созданный с помощьюview
.
return TextItemViewHolder(view)
- Адаптер должен сообщать
RecyclerView
об измененииdata
, посколькуRecyclerView
ничего не знает об этих данных. Он знает только о владельцах представлений, которых ему предоставляет адаптер.
Чтобы сообщитьRecyclerView
об изменении отображаемых данных, добавьте пользовательский сеттер к переменнойdata
в верхней части классаSleepNightAdapter
. В сеттере присвойтеdata
новое значение, а затем вызовитеnotifyDataSetChanged()
чтобы инициировать перерисовку списка с новыми данными.
var data = listOf<SleepNight>()
set(value) {
field = value
notifyDataSetChanged()
}
Шаг 4: Сообщите RecyclerView об адаптере
RecyclerView
необходимо знать об адаптере, который будет использоваться для получения держателей представлений.
- Откройте
SleepTrackerFragment.kt
. - В
onCreateview()
создайте адаптер. Разместите этот код после создания моделиViewModel
и перед операторомreturn
.
val adapter = SleepNightAdapter()
- Свяжите
adapter
сRecyclerView
.
binding.sleepList.adapter = adapter
- Очистите и перестройте свой проект, чтобы обновить объект
binding
.
Если ошибки, связанные сbinding.sleepList
илиbinding.FragmentSleepTrackerBinding
, по-прежнему возникают, отмените кэширование и перезапустите систему. (Выберите Файл > Недействительное кэширование / Перезапустить .)
Если вы запустите приложение сейчас, ошибок не возникнет, но вы не увидите никаких данных, когда нажмете «Пуск» , а затем «Остановить» .
Шаг 5: Загрузите данные в адаптер
Итак, у вас есть адаптер и способ передачи данных из адаптера в RecyclerView
. Теперь нужно передать данные в адаптер из ViewModel
.
- Откройте
SleepTrackerViewModel
. - Найдите переменную
nights
, в которой хранятся все ночи сна, и эти данные нужно отобразить. Переменнаяnights
устанавливается вызовом методаgetAllNights()
в базе данных. - Удалите
private
изnights
, так как вы создадите наблюдателя, которому потребуется доступ к этой переменной. Ваше объявление должно выглядеть так:
val nights = database.getAllNights()
- В пакете
database
откройтеSleepDatabaseDao
. - Найдите функцию
getAllNights()
. Обратите внимание, что эта функция возвращает список значенийSleepNight
в видеLiveData
. Это означает, что переменнаяnights
содержитLiveData
, обновляемыеRoom
, и вы можете наблюдать заnights
, чтобы знать, когда они меняются. - Откройте
SleepTrackerFragment
. - В
onCreateView()
, под созданиемadapter
, создайте наблюдателя для переменнойnights
.
УказавviewLifecycleOwner
фрагмента в качестве владельца жизненного цикла, можно гарантировать, что этот наблюдатель будет активен только тогда, когда на экране отображаетсяRecyclerView
.
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
})
- Внутри наблюдателя, всякий раз, когда вы получаете ненулевое значение (для
nights
), присваивайте это значение адаптеруdata
. Вот готовый код для наблюдателя и установки данных:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
it?.let {
adapter.data = it
}
})
- Создайте и запустите свой код.
Если ваш адаптер работает, вы увидите показатели качества сна в виде списка. На снимке экрана слева после нажатия кнопки « Старт» отображается значение -1. На снимке экрана справа показан обновлённый показатель качества сна после нажатия кнопки «Стоп» и выбора оценки качества.
Шаг 6: Изучите, как перерабатываются держатели изображений
RecyclerView
повторно использует держатели представлений. Когда представление прокручивается за пределы экрана, RecyclerView
повторно использует его для представления, которое должно появиться на экране.
Поскольку эти держатели представлений повторно используются, убедитесь, что onBindViewHolder()
устанавливает или сбрасывает все настройки, которые предыдущие элементы могли установить для держателя представления.
Например, вы можете установить красный цвет текста для объектов, рейтинг качества которых меньше или равен 1 и которые символизируют плохой сон.
- В классе
SleepNightAdapter
добавьте следующий код в конецonBindViewHolder()
.
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
}
- Запустите приложение.
- Добавьте данные о низком качестве сна, и число станет красным.
- Добавляйте высокие оценки качества сна, пока на экране не появится красная высокая цифра.
ПосколькуRecyclerView
повторно использует держатели представлений, в конечном итоге один из красных держателей представлений повторно используется для высокого рейтинга качества. Высокий рейтинг ошибочно отображается красным цветом.
- Чтобы исправить это, добавьте оператор
else
, чтобы установить черный цвет, если качество не меньше или равно единице.
Если оба условия указаны явно, владелец представления будет использовать правильный цвет текста для каждого элемента.
if (item.sleepQuality <= 1) {
holder.textView.setTextColor(Color.RED) // red
} else {
// reset
holder.textView.setTextColor(Color.BLACK) // black
}
- Запустите приложение, и цифры всегда будут иметь правильный цвет.
Поздравляем! Теперь у вас есть полностью функциональный базовый RecyclerView
.
В этой задаче вы замените простой держатель представления на тот, который может отображать больше данных о сне ночью.
Простой ViewHolder
, который вы добавили в Util.kt
просто оборачивает TextView
в TextItemViewHolder
.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
Так почему же RecyclerView
не использует TextView
напрямую? Эта строка кода обеспечивает массу функций. ViewHolder
описывает представление элемента и метаданные о его месте в RecyclerView
. RecyclerView
использует эту функциональность для правильного позиционирования представления при прокрутке списка и для реализации интересных функций, таких как анимация представлений при добавлении или удалении элементов в Adapter
.
Если RecyclerView
требуется доступ к представлениям, хранящимся во ViewHolder
, он может сделать это, используя свойство itemView
держателя представления. RecyclerView
использует itemView
при привязке элемента к отображению на экране, при отрисовке декоративных элементов вокруг представления, например, рамки, а также для реализации специальных возможностей.
Шаг 1: Создайте макет элемента
На этом этапе вы создадите файл макета для одного элемента. Макет состоит из ConstraintLayout
с ImageView
для качества сна, TextView
для продолжительности сна и TextView
для качества сна в текстовом формате. Поскольку вы уже работали с макетами, скопируйте и вставьте предоставленный XML-код.
- Создайте новый файл ресурсов макета и назовите его
list_item_sleep_night
. - Замените весь код в файле кодом ниже. Затем ознакомьтесь с созданным вами макетом.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/quality_image"
android:layout_width="@dimen/icon_size"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_sleep_5" />
<TextView
android:id="@+id/sleep_length"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/quality_image"
app:layout_constraintTop_toTopOf="@+id/quality_image"
tools:text="Wednesday" />
<TextView
android:id="@+id/quality_string"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="@+id/sleep_length"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/sleep_length"
app:layout_constraintTop_toBottomOf="@+id/sleep_length"
tools:text="Excellent!!!" />
</androidx.constraintlayout.widget.ConstraintLayout>
- Перейдите на вкладку «Дизайн» в Android Studio. В режиме дизайна ваш макет выглядит, как на скриншоте слева. В режиме чертежа он выглядит, как на скриншоте справа.
Шаг 2: Создание ViewHolder
- Откройте
SleepNightAdapter.kt
. - Создайте внутри
SleepNightAdapter
класс с названиемViewHolder
и сделайте его расширениемRecyclerView.ViewHolder
.
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
- Внутри
ViewHolder
получите ссылки на представления. Вам нужна ссылка на представления, которые этотViewHolder
будет обновлять. Каждый раз при привязке этогоViewHolder
вам необходимо получать доступ к изображению и обоим текстовым представлениям. (Позже вы преобразуете этот код для использования привязки данных.)
val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
val quality: TextView = itemView.findViewById(R.id.quality_string)
val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)
Шаг 3: использование ViewHolder в SleepNightAdapter
- В определении
SleepNightAdapter
вместоTextItemViewHolder
используйтеSleepNightAdapter.ViewHolder
, который вы только что создали.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {
Обновление onCreateViewHolder()
:
- Измените сигнатуру
onCreateViewHolder()
так, чтобы она возвращалаViewHolder
. - Измените инфлятор макета, чтобы использовать правильный ресурс макета,
list_item_sleep_night
. - Удалить приведение к
TextView
. - Вместо возврата
TextItemViewHolder
вернитеViewHolder
.
Вот готовая обновленная функцияonCreateViewHolder()
:
override fun onCreateViewHolder(
parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater =
LayoutInflater.from(parent.context)
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night,
parent, false)
return ViewHolder(view)
}
Обновление onBindViewHolder()
:
- Измените сигнатуру
onBindViewHolder()
так, чтобы параметромholder
былViewHolder
вместоTextItemViewHolder
. - Внутри
onBindViewHolder()
удалите весь код, кроме определенияitem
. - Определите
val
res
, которое содержит ссылку наresources
для этого представления.
val res = holder.itemView.context.resources
- Установите текст в текстовом представлении
sleepLength
равным длительности. Скопируйте код ниже, который вызывает функцию форматирования, предоставленную вместе с кодом запуска.
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
- Это приводит к ошибке, поскольку необходимо определить
convertDurationToFormatted()
. ОткройтеUtil.kt
и раскомментируйте код и связанные с ним импорты. (Выберите «Код» > «Комментировать строкой» .) - Вернитесь в
onBindViewHolder()
и используйтеconvertNumericQualityToString()
для установки качества.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
- Возможно, вам придется вручную импортировать эти функции.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
- Установите правильный значок для качества. Новый значок
ic_sleep_active
предоставлен в стартовом коде.
holder.qualityImage.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
})
- Вот готовая обновленная функция
onBindViewHolder()
, устанавливающая все данные дляViewHolder
:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
val res = holder.itemView.context.resources
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
holder.qualityImage.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
})
}
- Запустите приложение. Ваш экран должен выглядеть, как на скриншоте ниже: значок качества сна, а также текст, отображающий продолжительность и качество сна.
Ваш RecyclerView
готов! Вы узнали, как реализовать Adapter
и ViewHolder
, и объединили их для отображения списка с помощью Adapter
RecyclerView
.
Ваш код пока демонстрирует процесс создания адаптера и держателя представления. Однако вы можете улучшить этот код. Код для отображения и код для управления держателями представления перепутаны, а onBindViewHolder()
знает подробности об обновлении ViewHolder
.
В производственном приложении может быть несколько держателей представлений, более сложные адаптеры и несколько разработчиков, вносящих изменения. Структурируйте код так, чтобы всё, что связано с держателем представления, находилось только в нём.
Шаг 1: Рефакторинг onBindViewHolder()
На этом этапе вы проводите рефакторинг кода и переносите весь функционал View Holder во ViewHolder
. Цель этого рефакторинга — не изменить внешний вид приложения для пользователя, а сделать работу с кодом проще и безопаснее для разработчиков. К счастью, в Android Studio есть инструменты, которые помогут вам в этом.
- В
SleepNightAdapter
, вonBindViewHolder()
, выберите все, кроме оператора объявления переменнойitem
. - Щелкните правой кнопкой мыши, затем выберите Рефакторинг > Извлечь > Функция .
- Назовите функцию
bind
и примите предложенные параметры. Нажмите OK .
Функцияbind()
размещена нижеonBindViewHolder()
.
private fun bind(holder: ViewHolder, item: SleepNight) {
val res = holder.itemView.context.resources
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
holder.quality.text = convertNumericQualityToString(item.sleepQuality, res)
holder.qualityImage.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
})
}
- Установите курсор на слово-
holder
параметра-holder
функцииbind()
. НажмитеAlt+Enter
(Option+Enter
на Mac), чтобы открыть меню намерений. Выберите «Преобразовать параметр в приемник» , чтобы преобразовать его в функцию расширения со следующей сигнатурой:
private fun ViewHolder.bind(item: SleepNight) {...}
- Вырежьте и вставьте функцию
bind()
воViewHolder
. - Сделайте
bind()
публичным. - При необходимости импортируйте
bind()
в адаптер. - Поскольку теперь он находится в
ViewHolder
, вы можете удалить частьViewHolder
из сигнатуры. Вот окончательный код функцииbind()
в классеViewHolder
.
fun bind(item: SleepNight) {
val res = itemView.context.resources
sleepLength.text = convertDurationToFormatted(
item.startTimeMilli, item.endTimeMilli, res)
quality.text = convertNumericQualityToString(
item.sleepQuality, res)
qualityImage.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: Рефакторинг onCreateViewHolder
Метод onCreateViewHolder()
в адаптере в настоящее время расширяет представление из ресурса макета для ViewHolder
. Однако расширение никак не связано с адаптером, а связано исключительно с ViewHolder
. Расширение должно происходить во ViewHolder
.
- В
onCreateViewHolder()
выделите весь код в теле функции. - Щелкните правой кнопкой мыши, затем выберите Рефакторинг > Извлечь > Функция .
- Назовите функцию
from
примите предложенные параметры. Нажмите «ОК» . - Установите курсор на имя функции
from
. НажмитеAlt+Enter
(Option+Enter
на Mac), чтобы открыть меню намерений. - Выберите «Переместить в сопутствующий объект» . Функция
from()
должна находиться в сопутствующем объекте, чтобы её можно было вызвать в классеViewHolder
, а не в экземпляреViewHolder
. - Переместите
companion
объект в классViewHolder
. - Сделать
from()
публичным. - В
onCreateViewHolder()
измените операторreturn
так, чтобы он возвращал результат вызоваfrom()
в классеViewHolder
.
Ваши готовые методыonCreateViewHolder()
иfrom()
должны выглядеть так, как показано ниже, а код должен компилироваться и запускаться без ошибок.
override fun onCreateViewHolder(parent: ViewGroup, viewType:
Int): ViewHolder {
return ViewHolder.from(parent)
}
companion object {
fun from(parent: ViewGroup): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val view = layoutInflater
.inflate(R.layout.list_item_sleep_night, parent, false)
return ViewHolder(view)
}
}
- Измените сигнатуру класса
ViewHolder
так, чтобы конструктор стал приватным. Поскольку методfrom()
теперь возвращает новый экземплярViewHolder
, больше нет причин вызывать конструкторViewHolder
.
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
- Запустите приложение. Ваше приложение должно собраться и работать так же, как и раньше, что и является желаемым результатом после рефакторинга.
Проект Android Studio: RecyclerViewFundamentals
- Отображение списка или таблицы данных — одна из самых распространённых задач пользовательского интерфейса в Android.
RecyclerView
разработан для эффективного отображения даже очень больших списков. -
RecyclerView
выполняет только ту работу, которая необходима для обработки или отрисовки элементов, которые в данный момент видны на экране. - Когда элемент прокручивается за пределы экрана, его представления перезаписываются. Это означает, что элемент заполняется новым контентом, который прокручивается на экран.
- Шаблон «адаптер» в программной инженерии помогает объекту взаимодействовать с другим API.
RecyclerView
использует адаптер для преобразования данных приложения в то, что оно может отображать, без необходимости изменения способа хранения и обработки данных приложением.
Для отображения данных в RecyclerView
вам понадобятся следующие компоненты:
- RecyclerView
Чтобы создать экземплярRecyclerView
, определите элемент<RecyclerView>
в файле макета. - LayoutManager
RecyclerView
используетLayoutManager
для организации расположения элементов вRecyclerView
, например, для их размещения в сетке или в линейном списке.
В<RecyclerView>
файла макета задайте атрибутapp:layoutManager
для менеджера макета (например,LinearLayoutManager
илиGridLayoutManager
).
Вы также можете программно настроитьLayoutManager
дляRecyclerView
. (Этот метод рассматривается в следующей лабораторной работе.) - Макет для каждого элемента
Создайте макет для одного элемента данных в файле макета XML. - Адаптер
Создайте адаптер, который подготавливает данные и определяет, как они будут отображаться вViewHolder
. Свяжите адаптер сRecyclerView
.
При запускеRecyclerView
он будет использовать адаптер, чтобы определить, как отображать данные на экране.
Адаптер требует реализации следующих методов:
–getItemCount()
для возврата количества элементов.
–onCreateViewHolder()
для возвратаViewHolder
для элемента в списке.
–onBindViewHolder()
для адаптации данных к представлениям элемента в списке. - ViewHolder
ViewHolder
содержит информацию о представлении для отображения одного элемента из макета элемента. - Метод
onBindViewHolder()
в адаптере адаптирует данные к представлениям. Этот метод всегда переопределяется. Как правило,onBindViewHolder()
расширяет макет элемента и помещает данные в представления макета. - Поскольку
RecyclerView
ничего не знает о данных,Adapter
должен информироватьRecyclerView
об изменении этих данных. ИспользуйтеnotifyDataSetChanged()
, чтобы уведомитьAdapter
об изменении данных.
Курс Udacity:
Документация для разработчиков Android:
В этом разделе перечислены возможные домашние задания для студентов, работающих над этой лабораторной работой в рамках курса, проводимого преподавателем. Преподаватель должен выполнить следующие действия:
- При необходимости задавайте домашнее задание.
- Объясните учащимся, как следует сдавать домашние задания.
- Оцените домашние задания.
Преподаватели могут использовать эти предложения так часто или редко, как пожелают, и могут свободно задавать любые другие домашние задания, которые они сочтут подходящими.
Если вы работаете с этой лабораторной работой самостоятельно, можете использовать эти домашние задания для проверки своих знаний.
Ответьте на эти вопросы
Вопрос 1
Как RecyclerView
отображает элементы? Выберите все подходящие варианты.
▢ Отображает элементы в виде списка или сетки.
▢ Прокручивается вертикально или горизонтально.
▢ Прокручивается по диагонали на больших устройствах, таких как планшеты.
▢ Позволяет создавать пользовательские макеты, когда списка или сетки недостаточно для конкретного случая использования.
Вопрос 2
Каковы преимущества использования RecyclerView
? Выберите все подходящие варианты.
▢ Эффективно отображает большие списки.
▢ Автоматически обновляет данные.
▢ Минимизирует необходимость обновлений при обновлении, удалении или добавлении элемента в список.
▢ Повторно использует представление, прокручивающееся за пределы экрана, для отображения следующего элемента, прокручивающегося на экране.
Вопрос 3
Каковы причины использования адаптеров? Выберите все подходящие варианты.
▢ Разделение задач упрощает изменение и тестирование кода.
▢ RecyclerView
не зависит от отображаемых данных.
▢ Уровни обработки данных не должны беспокоиться о том, как будут отображаться данные.
▢ Приложение будет работать быстрее.
Вопрос 4
Какие из следующих утверждений верны для ViewHolder
? Выберите все подходящие варианты.
▢ Макет ViewHolder
определяется в файлах макета XML.
▢ Для каждой единицы данных в наборе данных существует один ViewHolder
.
▢ В RecyclerView
может быть более одного ViewHolder
.
▢ Adapter
привязывает данные к ViewHolder
.
Начните следующий урок: