Основы Android Kotlin 07.1: Основы RecyclerView

Эта практическая работа входит в курс «Основы 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 .

  1. Загрузите приложение RecyclerViewFundamentals-Starter с GitHub.
  2. Создайте и запустите приложение. Обратите внимание, что данные отображаются в виде простого текста.
  3. Откройте файл макета fragment_sleep_tracker.xml на вкладке «Дизайн» в Android Studio.
  4. На панели «Дерево компонентов» удалите ScrollView . Это действие также удалит TextView , находящийся внутри ScrollView .
  5. На панели «Палитра» прокрутите список типов компонентов слева, чтобы найти «Контейнеры» , затем выберите его.
  6. Перетащите RecyclerView из панели «Палитра» на панель «Дерево компонентов» . Поместите RecyclerView в ConstraintLayout .

  1. Если откроется диалоговое окно с вопросом о добавлении зависимости, нажмите «ОК» , чтобы Android Studio добавила зависимость recyclerview в ваш Gradle-файл. Это может занять несколько секунд, после чего ваше приложение будет синхронизировано.

  1. Откройте файл модуля build.gradle , прокрутите до конца и обратите внимание на новую зависимость, которая выглядит примерно так, как показано на коде ниже:
implementation 'androidx.recyclerview:recyclerview:1.0.0'
  1. Вернитесь к fragment_sleep_tracker.xml .
  2. На вкладке «Текст» найдите код RecyclerView , показанный ниже:
<androidx.recyclerview.widget.RecyclerView
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. Присвойте RecyclerView id sleep_list .
android:id="@+id/sleep_list"
  1. Разместите 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"
  1. Добавьте менеджер макета в XML-файл RecyclerView . Каждому RecyclerView нужен менеджер макета, который определяет расположение элементов в списке. В Android есть LinearLayoutManager , который по умолчанию размещает элементы в вертикальном списке строк полной ширины.
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
  1. Перейдите на вкладку «Конструктор» и обратите внимание, что добавленные ограничения привели к тому, что RecyclerView расширился, чтобы заполнить все доступное пространство.

Шаг 2: Создайте макет элемента списка и держатель текстового представления.

RecyclerView — это всего лишь контейнер. На этом этапе вы создаёте макет и инфраструктуру для элементов, отображаемых внутри RecyclerView .

Чтобы как можно быстрее получить работающий RecyclerView , сначала используется простой элемент списка, отображающий только качество сна в виде числа. Для этого необходим контейнер представления TextItemViewHolder . Также необходим контейнер представления TextView для данных. (На следующем этапе вы узнаете больше о контейнерах представления и о том, как организовать все данные о сне.)

  1. Создайте файл макета с именем text_item_view.xml . Неважно, какой элемент вы используете в качестве корневого, так как вы замените код шаблона.
  2. В text_item_view.xml удалите весь указанный код.
  3. Добавьте 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" />
  1. Откройте Util.kt Прокрутите до конца и добавьте показанное ниже определение, которое создаёт класс TextItemViewHolder . Добавьте код в конец файла, после последней закрывающей скобки. Код добавляется в Util.kt поскольку этот контейнер представления временный, и вы замените его позже.
class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)
  1. При появлении соответствующего запроса импортируйте android.widget.TextView и androidx.recyclerview.widget.RecyclerView .

Шаг 3: Создайте SleepNightAdapter

Основная задача при реализации RecyclerView — создание адаптера. У вас есть простой держатель представления для представления элементов и макет для каждого элемента. Теперь вы можете создать адаптер. Адаптер создаёт держатель представления и заполняет его данными для отображения RecyclerView .

  1. В пакете sleeptracker создайте новый класс Kotlin с именем SleepNightAdapter .
  2. Сделайте класс SleepNightAdapter расширением RecyclerView.Adapter . Класс называется SleepNightAdapter , поскольку он адаптирует объект SleepNight к типу, который может использовать RecyclerView . Адаптеру необходимо знать, какой держатель представления использовать, поэтому передайте TextItemViewHolder . Импортируйте необходимые компоненты при появлении запроса. В этом случае вы увидите ошибку, поскольку существуют обязательные методы для реализации.
class SleepNightAdapter: RecyclerView.Adapter<TextItemViewHolder>() {}
  1. На верхнем уровне SleepNightAdapter создайте переменную listOf SleepNight для хранения данных.
var data =  listOf<SleepNight>()
  1. В SleepNightAdapter переопределите getItemCount() , чтобы он возвращал размер списка ночей сна в data . RecyclerView необходимо знать, сколько элементов есть в адаптере, чтобы отобразить их, и он делает это, вызывая getItemCount() .
override fun getItemCount() = data.size
  1. В SleepNightAdapter переопределите функцию onBindViewHolder() , как показано ниже.

    Функция onBindViewHolder() вызывается RecyclerView для отображения данных одного элемента списка в указанной позиции. Таким образом, метод onBindViewHolder() принимает два аргумента: держатель представления и позицию привязываемых данных. В данном приложении держателем является TextItemViewHolder , а позицией — позиция в списке.
override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
}
  1. Внутри onBindViewHolder() создайте переменную для одного элемента в заданной позиции данных.
 val item = data[position]
  1. У созданного вами ViewHolder есть свойство textView . В методе onBindViewHolder() установите text textView значение, соответствующее качеству сна. Этот код отображает только список чисел, но этот простой пример позволяет увидеть, как адаптер передаёт данные в ViewHolder и выводит их на экран.
holder.textView.text = item.sleepQuality.toString()
  1. В SleepNightAdapter переопределите и реализуйте onCreateViewHolder() , который вызывается, когда RecyclerView требуется держатель представления для представления элемента.

    Эта функция принимает два параметра и возвращает ViewHolder . parent параметр, представляющий собой группу представлений, содержащую держатель представления, всегда является RecyclerView . Параметр viewType используется, когда в одном RecyclerView есть несколько представлений. Например, если вы поместите список текстовых представлений, изображение и видео в один RecyclerView , функция onCreateViewHolder() должна знать, какой тип представления использовать.
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
}
  1. В onCreateViewHolder() создайте экземпляр LayoutInflater .

    Модуль расширения макета умеет создавать представления из XML-макетов. context содержит информацию о том, как правильно расширить представление. В адаптере для представления Recycler всегда передаётся контекст parent группы представлений, то есть RecyclerView .
val layoutInflater = LayoutInflater.from(parent.context)
  1. В onCreateViewHolder() создайте view , попросив layoutinflater расширить его.

    Передайте XML-макет для представления и parent группу представлений для представления. Третий аргумент, логический, — attachToRoot . Этот аргумент должен быть false , поскольку RecyclerView автоматически добавит этот элемент в иерархию представлений, когда придёт время.
val view = layoutInflater
       .inflate(R.layout.text_item_view, parent, false) as TextView
  1. В onCreateViewHolder() верните TextItemViewHolder , созданный с помощью view .
return TextItemViewHolder(view)
  1. Адаптер должен сообщать RecyclerView об изменении data , поскольку RecyclerView ничего не знает об этих данных. Он знает только о владельцах представлений, которых ему предоставляет адаптер.

    Чтобы сообщить RecyclerView об изменении отображаемых данных, добавьте пользовательский сеттер к переменной data в верхней части класса SleepNightAdapter . В сеттере присвойте data новое значение, а затем вызовите notifyDataSetChanged() чтобы инициировать перерисовку списка с новыми данными.
var data =  listOf<SleepNight>()
   set(value) {
       field = value
       notifyDataSetChanged()
   }

Шаг 4: Сообщите RecyclerView об адаптере

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

  1. Откройте SleepTrackerFragment.kt .
  2. В onCreateview() создайте адаптер. Разместите этот код после создания модели ViewModel и перед оператором return .
val adapter = SleepNightAdapter()
  1. Свяжите adapter с RecyclerView .
binding.sleepList.adapter = adapter
  1. Очистите и перестройте свой проект, чтобы обновить объект binding .

    Если ошибки, связанные с binding.sleepList или binding.FragmentSleepTrackerBinding , по-прежнему возникают, отмените кэширование и перезапустите систему. (Выберите Файл > Недействительное кэширование / Перезапустить .)

    Если вы запустите приложение сейчас, ошибок не возникнет, но вы не увидите никаких данных, когда нажмете «Пуск» , а затем «Остановить» .

Шаг 5: Загрузите данные в адаптер

Итак, у вас есть адаптер и способ передачи данных из адаптера в RecyclerView . Теперь нужно передать данные в адаптер из ViewModel .

  1. Откройте SleepTrackerViewModel .
  2. Найдите переменную nights , в которой хранятся все ночи сна, и эти данные нужно отобразить. Переменная nights устанавливается вызовом метода getAllNights() в базе данных.
  3. Удалите private из nights , так как вы создадите наблюдателя, которому потребуется доступ к этой переменной. Ваше объявление должно выглядеть так:
val nights = database.getAllNights()
  1. В пакете database откройте SleepDatabaseDao .
  2. Найдите функцию getAllNights() . Обратите внимание, что эта функция возвращает список значений SleepNight в виде LiveData . Это означает, что переменная nights содержит LiveData , обновляемые Room , и вы можете наблюдать за nights , чтобы знать, когда они меняются.
  3. Откройте SleepTrackerFragment .
  4. В onCreateView() , под созданием adapter , создайте наблюдателя для переменной nights .

    Указав viewLifecycleOwner фрагмента в качестве владельца жизненного цикла, можно гарантировать, что этот наблюдатель будет активен только тогда, когда на экране отображается RecyclerView .
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   })
  1. Внутри наблюдателя, всякий раз, когда вы получаете ненулевое значение (для nights ), присваивайте это значение адаптеру data . Вот готовый код для наблюдателя и установки данных:
sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.data = it
   }
})
  1. Создайте и запустите свой код.

    Если ваш адаптер работает, вы увидите показатели качества сна в виде списка. На снимке экрана слева после нажатия кнопки « Старт» отображается значение -1. На снимке экрана справа показан обновлённый показатель качества сна после нажатия кнопки «Стоп» и выбора оценки качества.

Шаг 6: Изучите, как перерабатываются держатели изображений

RecyclerView повторно использует держатели представлений. Когда представление прокручивается за пределы экрана, RecyclerView повторно использует его для представления, которое должно появиться на экране.

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

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

  1. В классе SleepNightAdapter добавьте следующий код в конец onBindViewHolder() .
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
}
  1. Запустите приложение.
  2. Добавьте данные о низком качестве сна, и число станет красным.
  3. Добавляйте высокие оценки качества сна, пока на экране не появится красная высокая цифра.

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

  1. Чтобы исправить это, добавьте оператор else , чтобы установить черный цвет, если качество не меньше или равно единице.

    Если оба условия указаны явно, владелец представления будет использовать правильный цвет текста для каждого элемента.
if (item.sleepQuality <= 1) {
   holder.textView.setTextColor(Color.RED) // red
} else {
   // reset
   holder.textView.setTextColor(Color.BLACK) // black
}
  1. Запустите приложение, и цифры всегда будут иметь правильный цвет.

Поздравляем! Теперь у вас есть полностью функциональный базовый 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-код.

  1. Создайте новый файл ресурсов макета и назовите его list_item_sleep_night .
  2. Замените весь код в файле кодом ниже. Затем ознакомьтесь с созданным вами макетом.
<?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>
  1. Перейдите на вкладку «Дизайн» в Android Studio. В режиме дизайна ваш макет выглядит, как на скриншоте слева. В режиме чертежа он выглядит, как на скриншоте справа.

Шаг 2: Создание ViewHolder

  1. Откройте SleepNightAdapter.kt .
  2. Создайте внутри SleepNightAdapter класс с названием ViewHolder и сделайте его расширением RecyclerView.ViewHolder .
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView){}
  1. Внутри 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

  1. В определении SleepNightAdapter вместо TextItemViewHolder используйте SleepNightAdapter.ViewHolder , который вы только что создали.
class SleepNightAdapter: RecyclerView.Adapter<SleepNightAdapter.ViewHolder>() {

Обновление onCreateViewHolder() :

  1. Измените сигнатуру onCreateViewHolder() так, чтобы она возвращала ViewHolder .
  2. Измените инфлятор макета, чтобы использовать правильный ресурс макета, list_item_sleep_night .
  3. Удалить приведение к TextView .
  4. Вместо возврата 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() :

  1. Измените сигнатуру onBindViewHolder() так, чтобы параметром holder был ViewHolder вместо TextItemViewHolder .
  2. Внутри onBindViewHolder() удалите весь код, кроме определения item .
  3. Определите val res , которое содержит ссылку на resources для этого представления.
val res = holder.itemView.context.resources
  1. Установите текст в текстовом представлении sleepLength равным длительности. Скопируйте код ниже, который вызывает функцию форматирования, предоставленную вместе с кодом запуска.
holder.sleepLength.text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, res)
  1. Это приводит к ошибке, поскольку необходимо определить convertDurationToFormatted() . Откройте Util.kt и раскомментируйте код и связанные с ним импорты. (Выберите «Код» > «Комментировать строкой» .)
  2. Вернитесь в onBindViewHolder() и используйте convertNumericQualityToString() для установки качества.
holder.quality.text= convertNumericQualityToString(item.sleepQuality, res)
  1. Возможно, вам придется вручную импортировать эти функции.
import com.example.android.trackmysleepquality.convertDurationToFormatted
import com.example.android.trackmysleepquality.convertNumericQualityToString
  1. Установите правильный значок для качества. Новый значок 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
})
  1. Вот готовая обновленная функция 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
        })
    }
  1. Запустите приложение. Ваш экран должен выглядеть, как на скриншоте ниже: значок качества сна, а также текст, отображающий продолжительность и качество сна.

Ваш RecyclerView готов! Вы узнали, как реализовать Adapter и ViewHolder , и объединили их для отображения списка с помощью Adapter RecyclerView .

Ваш код пока демонстрирует процесс создания адаптера и держателя представления. Однако вы можете улучшить этот код. Код для отображения и код для управления держателями представления перепутаны, а onBindViewHolder() знает подробности об обновлении ViewHolder .

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

Шаг 1: Рефакторинг onBindViewHolder()

На этом этапе вы проводите рефакторинг кода и переносите весь функционал View Holder во ViewHolder . Цель этого рефакторинга — не изменить внешний вид приложения для пользователя, а сделать работу с кодом проще и безопаснее для разработчиков. К счастью, в Android Studio есть инструменты, которые помогут вам в этом.

  1. В SleepNightAdapter , в onBindViewHolder() , выберите все, кроме оператора объявления переменной item .
  2. Щелкните правой кнопкой мыши, затем выберите Рефакторинг > Извлечь > Функция .
  3. Назовите функцию 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
        })
    }
  1. Установите курсор на слово- holder параметра- holder функции bind() . Нажмите Alt+Enter ( Option+Enter на Mac), чтобы открыть меню намерений. Выберите «Преобразовать параметр в приемник» , чтобы преобразовать его в функцию расширения со следующей сигнатурой:
private fun ViewHolder.bind(item: SleepNight) {...}
  1. Вырежьте и вставьте функцию bind() во ViewHolder .
  2. Сделайте bind() публичным.
  3. При необходимости импортируйте bind() в адаптер.
  4. Поскольку теперь он находится в 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 .

  1. В onCreateViewHolder() выделите весь код в теле функции.
  2. Щелкните правой кнопкой мыши, затем выберите Рефакторинг > Извлечь > Функция .
  3. Назовите функцию from примите предложенные параметры. Нажмите «ОК» .
  4. Установите курсор на имя функции from . Нажмите Alt+Enter ( Option+Enter на Mac), чтобы открыть меню намерений.
  5. Выберите «Переместить в сопутствующий объект» . Функция from() должна находиться в сопутствующем объекте, чтобы её можно было вызвать в классе ViewHolder , а не в экземпляре ViewHolder .
  6. Переместите companion объект в класс ViewHolder .
  7. Сделать from() публичным.
  8. В 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)
   }
}
  1. Измените сигнатуру класса ViewHolder так, чтобы конструктор стал приватным. Поскольку метод from() теперь возвращает новый экземпляр ViewHolder , больше нет причин вызывать конструктор ViewHolder .
class ViewHolder private constructor(itemView: View) : RecyclerView.ViewHolder(itemView){
  1. Запустите приложение. Ваше приложение должно собраться и работать так же, как и раньше, что и является желаемым результатом после рефакторинга.

Проект 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 .

Начните следующий урок: 7.2: DiffUtil и привязка данных с RecyclerView