08-recyclerview.md 40 KB

RecyclerView

Оглавление

Введение

Отображение списка данных является одной из самых распространенных задач пользовательского интерфейса в Android. В погодном приложении отображается список прогнозов. В приложении чата отображается список сообщений. В новостном приложении — список новостей. Почти каждое приложение имеет хотя бы один список.

Списки как могут быть простыми, где каждый элемент — это просто текст, отображаемый компонентом TextView. А могут быть и сложными с элементами, состоящими из множества компонентов.

Для реализации разных вариантов списков, как простых так и сложных, в Android используется компонент RecyclerView. У RecyclerView есть несколько преимуществ.

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

Эффективность RecyclerView заключается в том, что он использует адаптеры (о них речь пойдет далее) для отрисовки элементов списка. По умолчанию RecyclerView отрисовывает только элементы, видимые пользователю на экране. Это означает, что если в списке 1000 элементов, а пользователю видны только 10 из них, то RecyclerView будет отрисовывать только 10 элементов из 1000. А во время скроллинга будет автоматически обновлять содержимое новых элементов, которые пользователю становятся видны. Таким образом RecyclerView просто переиспользует существующие элементы, чтобы обновить отображение, вместо полного их пересоздания. Кроме этого, когда обновляется какой-либо элемент списка (например, в результате работ фоновых процессов), то RecyclerView не будет обновлять весь список целиком, а обновит только один конкретный элемент.

Кроме RecyclerView Android предоставляет и другие компоненты для отображения списков.
Например, ListView и GridView. Они более просты в использовании, чем RecyclerView, однако они менее настраиваемые, и эффективно работают лишь с небольшим количеством элементов в списке, не более 100. Также есть LinearLayout, однако, он не подходит для отображения большого числа элементов, генерация которых должна выполняться динамически. LinearLayout подходит лишь для отображения статических компонентов, количество которых меняться не должно.
Таким образом, на текущий момент наиболее подходящим решением для отображения списка элементов является RecyclerView.

В данном уроке мы будем добавлять RecyclerView в приложение "Sleep Tracker", которое разрабатывалось ранее. Сейчас приложение отображает список данных, но отображает их с помощью одного тестового поля TextView, которое заполняется постоянно генерирующейся строкой со всеми данными БД. Это ужасно выглядит с точки зрения пользовательского интерфейса, ужасно реализовано с точки зрения производительности (строка постоянно генерируется заново), и ужасно выглядит с точки зрения чистоты кода.

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

Шаблон проектирования "Адаптер"

В этом уроке будет рассмотрен шаблон проектирования — "Адаптер". Он потребуется для правильной реализации работы с RecyclerView.

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

В случае с приложением "Sleep Tracker" данные хранятся в базе данных Room. Адаптер необходим для адаптации и подготовки данных к отображению в RecyclerView.

Адаптер должен предоставлять для RecyclerView:

  • Информацию о количестве доступных элементов.
  • Информацию о том, каким образом элементы списка должны быть отрисованы.
  • Информацию о том, каким образом должен быть создан новый UI для элементов списка.

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

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

Для реализации переиспользования экземпляров и поддержки сложных видов для элементов списка RecyclerView используются "холдеры видов" ViewHolder.

ViewHolder:

  • Содержит в себе информацию о виде элемента списка.
  • Хранит важную информацию для эффективной работы RecyclerView, например, последнюю позицию текущего элемента в списке.
  • Является интерфейсом для элементов RecyclerView.

Можно подвести небольшое резюме касательно шаблона "Адаптер".
Наше приложение имеет некоторый список элементов (записей о снах). Адаптер будет подготавливать элементы списка для отрисовки их на экране. Отображаться элементы будут в рамках UI-компонента RecyclerView. RecyclerView может запрашивать количество элементов, которые нужно отобразить, у адаптера. Также он будет запрашивать у адаптера новый экземпляр ViewHolder для каждого элемента, который должен быть отрисован. Для эффективности RecyclerView будет переиспользовать ViewHolder'ы, которые скрываются с экрана в процессе скроллинга.

Далее перейдем к добавлению RecyclerView-, Adapter- и ViewHolder-классов шаг за шагом.

Добавление RecyclerView

Перейдем к добавлению RecyclerView на макет фрагмента SleepTrackerFragment и реализации класса-адаптера.

1. Добавление RecyclerView в файл fragment_sleep_tracker.xml:

На текущий момент макет fragment_sleep_tracker.xml содержит компонент ScrollView с TextView внутри. Наша задача — заменить ScrollView на RecyclerView. Именно он будет отображать список элементов.

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/sleep_list"
    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"
    app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>

Компонент RecyclerView имеет идентификатор sleep_list. Он потребуется позже. Кроме этого компонент располагается внутри ConstraintLayout, поэтому для него нет необходимости строго настраивать ширину и высоту, но обязательно настраиваются "привязки" к краям экрана слева и справа, а также к кнопке "Stop" сверху и кнопке "Clear" снизу. Список записей располагается между этими кнопками. В конце настраивается параметр layoutManager. На вход параметр принимает имя класса LinearLayoutManager, который определяет расположение элементов внутри RecyclerView и политику взаимодействия с ними. Кроме LinearLayoutManager это могут быть также GridLayoutManager, StaggeredGridLayoutManager и WearableLinearLayoutManager. LinearLayoutManager определяет, что элементы внутри RecyclerView будут располагаться в виде списка.

2. Добавление класса SleepNightAdapter:

Для добавления адаптера необходимо создать новый класс SleepNightAdapter. Он должен наследоваться от шаблонного класса RecyclerView.Adapter<> и в качестве шаблона указывается ViewHolder-класс TextItemViewHolder, который будет создан следующим.

class SleepNightAdapter : RecyclerView.Adapter<TextItemViewHolder>() {
}

3. Добавление класса TextItemViewHolder:

Для тестов нам необходимо просто отобразить текст в элементах списка, поэтому создадим простейший TextItemViewHolder с элементом TextView внутри.

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

Добавленный код компилятор не соберет, поскольку необходимо переопределить несколько методов класса-адаптера, чтобы предоставить RecyclerView информацию о данных и видах элементов.

4. Добавление данных в адаптер:

Для начала добавим свойство data, которое будет являться списком объектов SleepNight. Это будут данные для RecyclerView.

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

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

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

override fun getItemCount() = data.size

5. Предоставление информации об отрисовке элементов списка:

Далее необходимо предоставить информацию о том, каким образом элементы должны быть отрисованы. Для этого переопределяется метод onBindViewHolder(). Он принимает на вход два параметра: ViewHolder (в нашем случае TextItemViewHolder) и позицию элемента. Метод onBindViewHolder() будет вызван RecyclerView для отображения данных на элементе с определенной позицией в списке.

override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
    val item = data[position]
    holder.textView.text = item.sleepQuality.toString()
}

Для обновления данных на элементе TextItemViewHolder необходимо сперва получить экземпляр модели SleepNight из списка data по позиции. А затем получить, например, значение sleepQuality и записать его в TextView "холдера", который отвечает за отображение элемента в списке. Значение sleepQuality используется исключительно для демонстрации того, как получить данные и поместить их в вид элемента списка.

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

6. Добавление метода создания экземпляров TextItemViewHolder:

Далее перейдем к переопределению метода для создания экземпляров ViewHolderonCreateViewHolder().

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextItemViewHolder {
    val layoutInflater = LayoutInflater.from(parent.context)
    val view = layoutInflater
            .inflate(R.layout.text_item_view, parent, false) as TextView
    return TextItemViewHolder(view)
}

Метод принимает на вход два параметра. Первый — родительский UI-элемент типа ViewGroup, куда будет помещен создаваемый ViewHolder. В нормальной ситуации это именно компонент RecyclerView. Второй параметр — тип, используемый, когда RecyclerView отображает визуальные элементы разных типов и видов. Например, когда RecyclerView может отображать в одном списке элементы с текстом, и элементы с изображениями одновременно.

Точно так же как и для инициализации вида фрагментов, здесь для инициализации вида ViewHolder используется LayoutInflater и метод inflate(). Метод inflate() загружает макет вида элемента списка text_item_view, инициализирует элемент TextView и возвращает его. В конце возвращаем новый экземпляр "холдера" TextItemViewHolder. Конструктор TextItemViewHolder принимает на вход экземпляр TextView.

Загружаемый макет text_item_view.xml "холдера" содержит лишь один компонент TextView.

<?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" />

Таким образом мы подготовились к созданию экземпляров ViewHolder-компонентов, которые RecyclerView будет использовать для отображения в списке. Сам RecyclerView ничего не знает том, как выглядят элементы внутри него. Он лишь будет запрашивать адаптер на создание элементов, а адаптер уже предоставит всю информацию об элементах: их количестве, создании "холдеров" и об отрисовке данных на "холдерах".

7. Установка адаптера для RecyclerView:

Осталось создать экземпляр адаптера и установить его для добавленного ранее RecyclerView. Для этого необходимо в классе SleepTrackerFragment в методе onCreateView() добавить инициализацию адаптера и установку с помощью поля binding.

// SleepTrackerFragment.onCreateView()

val adapter = SleepNightAdapter()
binding.sleepList.adapter = adapter

Таким образом адаптер установлен в RecyclerView и все данные из адаптера автоматически будут отображаться в RecyclerView с помощью ViewHolder-компонентов.

Чтобы адаптер содержал некоторые данные необходимо проинициализировать его поле data, когда данные в БД меняются. Поскольку доступ к данным в БД осуществляется через объект ViewModel, то необходимо добавить подписку на изменение этих данных.

viewModel.nights.observe(viewLifecycleOwner, Observer { nights ->
    if (nights != null)
        adapter.data = nights
})

Если запустить приложение, то можно убедиться, что при нажатии кнопки "Start" в список на экране добавляется один элемент, отображающий "-1" — это оценка сна по умолчанию. После нажатия кнопки "Stop" открывается экран выбора оценки и после выбора запись на странице со списком обновляется и отображает ту оценку, что выбрал пользователь. Этот тест показывает, что добавленный RecyclerView работает. Он получает данные из адаптера (который в свою очередь получает их из БД через ViewModel). Адаптер и ViewHolder также предоставляют для RecyclerView информацию о том, как должны выглядеть элементы списка и как их создать.

8. Повторное использование экземпляров ViewHolder:

Чтобы продемонстрировать каким образом элементы списка RecyclerView переипользуются, необходимо в переопределенный метод onBindViewHolder() добавить условие о том, что, если оценка качества сна менее двойки, то необходимо окрашивать текст с оценкой красным цветом.

override fun onBindViewHolder(holder: TextItemViewHolder, position: Int) {
    val item = data[position]
    if (item.sleepQuality < 2) {
        holder.textView.setTextColor(Color.RED)
    }
    holder.textView.text = item.sleepQuality.toString()
}

Если запустить приложение и добавить столько элементов в список, что они не будут помещаться на экране все одновременно, то можно заметить при добавлении новых элементов, что уже ранее добавленные элементы с оценкой 2 и выше окрашиваются красным, хотя не должны. Это происходит из-за переиспользования экземпляров ViewHolder-объектов, у которых уже установлен цвет, но переопределяется значение текста. Чтобы избежать подобных проблем, нужно сбрасывать состояние элементов в состояние по умолчанию.

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

if (item.sleepQuality < 2) {
    holder.textView.setTextColor(Color.RED)
} else {
    holder.textView.setTextColor(Color.BLACK)
}

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

Отображение записей SleepNight в списке RecyclerView

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

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

class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView)

Класс наследуется от RecyclerView.ViewHolder, имеет лишь одной свойство textView и не имеет тела. То есть вся реализация данного класса заключается в реализации его родителя RecyclerView.ViewHolder.

Если заглянуть в реализацию или документацию класса RecyclerView.ViewHolder, можно найти свойства и методы, общие для ViewHolder-классов.

Например, каждый ViewHoler содержит свойство itemView. Объект itemView является видом элемента списка и экземпляром класса View, который должен быть отображен внутри RecyclerView. В случае с TextItemViewHolder таким видом является TextView, который передается в конструктор класса.

Перейдем к созданию собственного ViewHolder-класса.

1. Добавление макета для ViewHolder:

Сперва необходимо добавить макет элемента списка, который будет макетом для нового ViewHolder-класса. Файл с макетом будет называться list_item_sleep_night.xml.

Макет должен содержать один элемент ImageView для отображения иконки оценки сна и два текстовых поля TextView для отображения даты сна и текстового представления оценки.

Компонент ImageView для отображения иконки:

<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 для отображения даты сна:

<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 для отображения текстового представления оценки:

<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_constraintStart_toStartOf="@+id/sleep_length"
    app:layout_constraintTop_toBottomOf="@+id/sleep_length"
    tools:text="Excellent!!!" />

Подробно на каждом атрибуте останавливаться не будем. Все было рассмотрено в предыдущих уроках.

2. Создание класса SleepNightViewHolder:

Далее необходимо создать класс SleepNightViewHolder и унаследовать его от RecyclerView.ViewHolder.

class SleepNightViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)
    val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
    val quality: TextView = itemView.findViewById(R.id.quality_string)
}

Класс содержит свойства qualityImage, sleepLength и quality, которые инициализируются с помощью findViewById() и являются компонентами, которые располагаются на макете элемента списка list_item_sleep_night.xml.

3. Использование SleepNightViewHolder в классе SleepNightAdapter:

Далее необходимо заменить использование TextItemViewHolder в классе SleepNightAdapter на использование SleepNightViewHolder.

Во-первых, необходимо обновить метод onCreateViewHolder(): заменить тип возвращаемого значения на SleepNightViewHolder, ссылку на ресурс с макетом "холдера" на R.layout.list_item_sleep_night и создание экземпляра "холдера" на SleepNightViewHolder(view).

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SleepNightViewHolder {
    val layoutInflater = LayoutInflater.from(parent.context)
    val view = layoutInflater
            .inflate(R.layout.list_item_sleep_night, parent, false)
    return SleepNightViewHolder(view)
}

Во-вторых, требуется обновить реализацию метода onBindViewHolder(). Метод должен принимать на вход экземпляр SleepNightViewHolder и настраивать его вид в соответствии с данными.

override fun onBindViewHolder(holder: SleepNightViewHolder, position: Int) {
    val item = data[position]
    val res = holder.itemView.context.resources
    holder.sleepLength.text = convertDurationToFormatted(item.startTimeMillis, item.endTimeMillis, 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
    })
}

Метод выполняет настройку текстовых полей sleepLength и quality, используя статические функции из файла Util.kt. Функция convertNumericQualityToString() уже добавлена заранее. А функцию convertDurationToFormatted() необходимо добавить вручную. Ее исходный код можно взять здесь:

private val ONE_MINUTE_MILLIS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES)
private val ONE_HOUR_MILLIS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)

fun convertDurationToFormatted(startTimeMilli: Long, endTimeMilli: Long, res: Resources): String {
    val durationMilli = endTimeMilli - startTimeMilli
    val weekdayString = SimpleDateFormat("EEEE", Locale.getDefault()).format(startTimeMilli)
    return when {
        durationMilli < ONE_MINUTE_MILLIS -> {
            val seconds = TimeUnit.SECONDS.convert(durationMilli, TimeUnit.MILLISECONDS)
            res.getString(R.string.seconds_length, seconds, weekdayString)
        }
        durationMilli < ONE_HOUR_MILLIS -> {
            val minutes = TimeUnit.MINUTES.convert(durationMilli, TimeUnit.MILLISECONDS)
            res.getString(R.string.minutes_length, minutes, weekdayString)
        }
        else -> {
            val hours = TimeUnit.HOURS.convert(durationMilli, TimeUnit.MILLISECONDS)
            res.getString(R.string.hours_length, hours, weekdayString)
        }
    }
}

Кроме этого требуется добавить строки:

<string name="minutes_length">%d minutes on %s</string>
<string name="hours_length">%d hours on %s</string>
<string name="seconds_length">%d seconds on %s</string>

И ресурс с иконкой ic_sleep_active отсюда.

Кроме настройки текстовых полей, метод выполняет установку иконки оценки сна из ресурсов, в зависимости от значения поля sleepQuality записи данных.

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

Рефакторинг класса SleepNightAdapter

Если посмотреть на класс SleepNightAdapter, то можно заметить, что он требует небольшого рефакторинга. Так, например, метод onBindViewHolder содержит код изменяющий внешний вид компонента ViewHolder и логичнее было бы перенести этот код в класс SleepNightViewHolder. Кроме того так мы будем соответствовать принципу инкапсуляции кода.

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

Изменение свойств класса SleepNightViewHolder можно поместить в отдельный метод самого класса. Назвать его можно bind(). Кроме этого, переменную res можно сделать приватным свойством класса. Остальные свойства необходимо сделать приватными, чтобы следовать принципу инкапсуляции и не предоставлять прямой доступ к свойствам другим классам.

// SleepNightViewHolder

private val res: Resources = itemView.context.resources
private val sleepLength: TextView = itemView.findViewById(R.id.sleep_length)
private val quality: TextView = itemView.findViewById(R.id.quality_string)
private val qualityImage: ImageView = itemView.findViewById(R.id.quality_image)

fun bind(item: SleepNight) {
    sleepLength.text = convertDurationToFormatted(item.startTimeMillis, item.endTimeMillis, 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
    })
}

В методе onBindViewHolder() останется только вызов этого метода с передачей объекта SleepNight как параметра.

// SleepNightAdapter

override fun onBindViewHolder(holder: SleepNightViewHolder, position: Int) {
    val item = data[position]
    holder.bind(item)
}

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

2. Рефакторинг onCreateViewHolder():

Кроме выделения кода onBindViewHolder() в класс "холдера", туда же можно вынести и код метода onCreateViewHolder(). Таким образом будет инкапсулирована логика создания экземпляра SleepNightViewHolder.

Методу можно дать имя from() по аналогии с методом LayoutInflater.from(). Метод будет возвращать экземпляр SleepNightViewHolder, необходимый для onCreateViewHolder(), а принимать на вход будет экземпляр класса ViewGroup — родительского элемента.
Поскольку метод создает новый экземпляр класса SleepNightViewHolder, нет смысла вызывать его на экземпляре класса, логичнее было бы вызывать его на самом классе. Поэтому определение метода помещается в блок companion object.

companion object {
    fun from(parent: ViewGroup): SleepNightViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val view = layoutInflater
                .inflate(R.layout.list_item_sleep_night, parent, false)
        return SleepNightViewHolder(view)
    }
}

В методе onCreateViewHolder() остается только лишь вызов SleepNightViewHolder.from().

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SleepNightViewHolder {
    return SleepNightViewHolder.from(parent)
}

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