# RecyclerView ## Оглавление - [Введение](#введение) - [Шаблон проектирования "Адаптер"](#шаблон-проектирования-адаптер) - [Добавление `RecyclerView`](#добавление-recyclerview) - [Отображение записей `SleepNight` в списке `RecyclerView`](#отображение-записей-sleepnight-в-списке-recyclerview) - [Рефакторинг класса `SleepNightAdapter`](#рефакторинг-класса-sleepnightadapter) ## Введение Отображение списка данных является одной из самых распространенных задач пользовательского интерфейса в 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` и будут реализованы элементы списка, отображающие время сна, его продолжительность, оценку и иконку оценки. ![](sleep-tracker-with-recyclerview.png) ## Шаблон проектирования "Адаптер" В этом уроке будет рассмотрен шаблон проектирования — "Адаптер". Он потребуется для правильной реализации работы с `RecyclerView`. `RecyclerView` служит для отображения большого количества однотипных данных, например записей какой-либо одной таблицы базы данных. Прежде чем отобразить данные, кто-то должен их подготовить для отображения, создать отдельные элементы списка и поместить на них данные. Этим занимается адаптер. В случае с приложением "Sleep Tracker" данные хранятся в базе данных `Room`. Адаптер необходим для адаптации и подготовки данных к отображению в `RecyclerView`. Адаптер должен предоставлять для `RecyclerView`: * Информацию о количестве доступных элементов. * Информацию о том, каким образом элементы списка должны быть отрисованы. * Информацию о том, каким образом должен быть создан новый UI для элементов списка. Когда `RecyclerView` запущен, он опрашивает адаптер о том, как много элементов он должен содержать. Затем узнает каким образом первый элемент должны быть отрисован на экране и отрисовывает его. Затем узнает как отрисовать второй элемент и отрисовывает его тоже, и т.д до тех пор пока место на экране не закончится. В этот момент `RecyclerView` заканчивает отрисовку элементов. Когда пользователь начинает скроллить экран, элемент, скрывающийся от пользователя, переиспользуется для создания нового элемента, который пользователю показывается во время скроллинга. Для реализации переиспользования экземпляров и поддержки сложных видов для элементов списка `RecyclerView` используются "холдеры видов" `ViewHolder`. `ViewHolder`: * Содержит в себе информацию о виде элемента списка. * Хранит важную информацию для эффективной работы `RecyclerView`, например, последнюю позицию текущего элемента в списке. * Является интерфейсом для элементов `RecyclerView`. Можно подвести небольшое резюме касательно шаблона "Адаптер". Наше приложение имеет некоторый список элементов (записей о снах). Адаптер будет подготавливать элементы списка для отрисовки их на экране. Отображаться элементы будут в рамках UI-компонента `RecyclerView`. `RecyclerView` может запрашивать количество элементов, которые нужно отобразить, у адаптера. Также он будет запрашивать у адаптера новый экземпляр `ViewHolder` для каждого элемента, который должен быть отрисован. Для эффективности `RecyclerView` будет переиспользовать `ViewHolder`'ы, которые скрываются с экрана в процессе скроллинга. ![](adapter-pattern.png) Далее перейдем к добавлению `RecyclerView`-, `Adapter-` и `ViewHolder`-классов шаг за шагом. ## Добавление `RecyclerView` Перейдем к добавлению `RecyclerView` на макет фрагмента `SleepTrackerFragment` и реализации класса-адаптера. **1. Добавление `RecyclerView` в файл `fragment_sleep_tracker.xml`:** На текущий момент макет `fragment_sleep_tracker.xml` содержит компонент `ScrollView` с `TextView` внутри. Наша задача — заменить `ScrollView` на `RecyclerView`. Именно он будет отображать список элементов. ```kotlin ``` Компонент `RecyclerView` имеет идентификатор `sleep_list`. Он потребуется позже. Кроме этого компонент располагается внутри `ConstraintLayout`, поэтому для него нет необходимости строго настраивать ширину и высоту, но обязательно настраиваются "привязки" к краям экрана слева и справа, а также к кнопке "Stop" сверху и кнопке "Clear" снизу. Список записей располагается между этими кнопками. В конце настраивается параметр `layoutManager`. На вход параметр принимает имя класса `LinearLayoutManager`, который определяет расположение элементов внутри `RecyclerView` и политику взаимодействия с ними. Кроме `LinearLayoutManager` это могут быть также `GridLayoutManager`, `StaggeredGridLayoutManager` и `WearableLinearLayoutManager`. `LinearLayoutManager` определяет, что элементы внутри `RecyclerView` будут располагаться в виде списка. **2. Добавление класса `SleepNightAdapter`:** Для добавления адаптера необходимо создать новый класс `SleepNightAdapter`. Он должен наследоваться от шаблонного класса `RecyclerView.Adapter<>` и в качестве шаблона указывается `ViewHolder`-класс `TextItemViewHolder`, который будет создан следующим. ```kotlin class SleepNightAdapter : RecyclerView.Adapter() { } ``` **3. Добавление класса `TextItemViewHolder`:** Для тестов нам необходимо просто отобразить текст в элементах списка, поэтому создадим простейший `TextItemViewHolder` с элементом `TextView` внутри. ```kotlin class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView) ``` Добавленный код компилятор не соберет, поскольку необходимо переопределить несколько методов класса-адаптера, чтобы предоставить `RecyclerView` информацию о данных и видах элементов. **4. Добавление данных в адаптер:** Для начала добавим свойство `data`, которое будет являться списком объектов `SleepNight`. Это будут данные для `RecyclerView`. ```kotlin var data = listOf() set(value) { field = value notifyDataSetChanged() } ``` Для свойства `data` определим сеттер, который будет не только сохранять новое значение свойства, но и вызывать метод адаптера `notifyDataSetChanged()`. Данный метод оповещает `RecyclerView` о том, что данные изменились и `RecyclerView`, получая такое сообщение, автоматически перерисовывает свое содержимое. Далее необходимо предоставить информацию о том, сколько элементов будет содержаться в `RecyclerView`. Для этого необходимо переопределить метод `getItemCount()`, чтобы он возвращал общее число элементов в списке `data`. ```kotlin override fun getItemCount() = data.size ``` **5. Предоставление информации об отрисовке элементов списка:** Далее необходимо предоставить информацию о том, каким образом элементы должны быть отрисованы. Для этого переопределяется метод `onBindViewHolder()`. Он принимает на вход два параметра: `ViewHolder` (в нашем случае `TextItemViewHolder`) и позицию элемента. Метод `onBindViewHolder()` будет вызван `RecyclerView` для отображения данных на элементе с определенной позицией в списке. ```kotlin 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`:** Далее перейдем к переопределению метода для создания экземпляров `ViewHolder` — `onCreateViewHolder()`. ```kotlin 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 ``` Таким образом мы подготовились к созданию экземпляров `ViewHolder`-компонентов, которые `RecyclerView` будет использовать для отображения в списке. Сам `RecyclerView` ничего не знает том, как выглядят элементы внутри него. Он лишь будет запрашивать адаптер на создание элементов, а адаптер уже предоставит всю информацию об элементах: их количестве, создании "холдеров" и об отрисовке данных на "холдерах". **7. Установка адаптера для `RecyclerView`:** Осталось создать экземпляр адаптера и установить его для добавленного ранее `RecyclerView`. Для этого необходимо в классе `SleepTrackerFragment` в методе `onCreateView()` добавить инициализацию адаптера и установку с помощью поля `binding`. ```kotlin // SleepTrackerFragment.onCreateView() val adapter = SleepNightAdapter() binding.sleepList.adapter = adapter ``` Таким образом адаптер установлен в `RecyclerView` и все данные из адаптера автоматически будут отображаться в `RecyclerView` с помощью `ViewHolder`-компонентов. Чтобы адаптер содержал некоторые данные необходимо проинициализировать его поле `data`, когда данные в БД меняются. Поскольку доступ к данным в БД осуществляется через объект `ViewModel`, то необходимо добавить подписку на изменение этих данных. ```kotlin viewModel.nights.observe(viewLifecycleOwner, Observer { nights -> if (nights != null) adapter.data = nights }) ``` Если запустить приложение, то можно убедиться, что при нажатии кнопки "Start" в список на экране добавляется один элемент, отображающий "-1" — это оценка сна по умолчанию. После нажатия кнопки "Stop" открывается экран выбора оценки и после выбора запись на странице со списком обновляется и отображает ту оценку, что выбрал пользователь. Этот тест показывает, что добавленный `RecyclerView` работает. Он получает данные из адаптера (который в свою очередь получает их из БД через `ViewModel`). Адаптер и `ViewHolder` также предоставляют для `RecyclerView` информацию о том, как должны выглядеть элементы списка и как их создать. **8. Повторное использование экземпляров `ViewHolder`:** Чтобы продемонстрировать каким образом элементы списка `RecyclerView` переипользуются, необходимо в переопределенный метод `onBindViewHolder()` добавить условие о том, что, если оценка качества сна менее двойки, то необходимо окрашивать текст с оценкой красным цветом. ```kotlin 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 и выше устанавливать черный цвет текста. ```kotlin if (item.sleepQuality < 2) { holder.textView.setTextColor(Color.RED) } else { holder.textView.setTextColor(Color.BLACK) } ``` Теперь при запуске приложения и добавлении новых элементов можно заметить, что проблемы с неверным окрашиванием элементов списка нет. ## Отображение записей `SleepNight` в списке `RecyclerView` В предыдущей главе научились добавлять `RecyclerView` с адаптером и `ViewHolder`'ом для отображения конкретных данных на отдельных элементах списка. В этой главе подробнее пойдет речь о `ViewHolder`'ах, о создании собственного сложного `ViewHolder`-класса. Прежде чем создавать собственный сложный `ViewHolder`-класс рассмотрим класс `TextItemViewHolder`, который используется в данный момент. ```kotlin class TextItemViewHolder(val textView: TextView): RecyclerView.ViewHolder(textView) ``` Класс наследуется от `RecyclerView.ViewHolder`, имеет лишь одной свойство `textView` и не имеет тела. То есть вся реализация данного класса заключается в реализации его родителя `RecyclerView.ViewHolder`. Если заглянуть в реализацию или [документацию](https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ViewHolder) класса `RecyclerView.ViewHolder`, можно найти свойства и методы, общие для `ViewHolder`-классов. Например, каждый `ViewHoler` содержит свойство `itemView`. Объект `itemView` является видом элемента списка и экземпляром класса `View`, который должен быть отображен внутри `RecyclerView`. В случае с `TextItemViewHolder` таким видом является `TextView`, который передается в конструктор класса. Перейдем к созданию собственного `ViewHolder`-класса. **1. Добавление макета для `ViewHolder`:** Сперва необходимо добавить макет элемента списка, который будет макетом для нового `ViewHolder`-класса. Файл с макетом будет называться `list_item_sleep_night.xml`. Макет должен содержать один элемент `ImageView` для отображения иконки оценки сна и два текстовых поля `TextView` для отображения даты сна и текстового представления оценки. ![](list_item_sleep_night.png) Компонент `ImageView` для отображения иконки: ```xml ``` Компонент `TextView` для отображения даты сна: ```xml ``` Компонент `TextView` для отображения текстового представления оценки: ```xml ``` Подробно на каждом атрибуте останавливаться не будем. Все было рассмотрено в предыдущих уроках. **2. Создание класса `SleepNightViewHolder`:** Далее необходимо создать класс `SleepNightViewHolder` и унаследовать его от `RecyclerView.ViewHolder`. ```kotlin 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)`. ```kotlin 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` и настраивать его вид в соответствии с данными. ```kotlin 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()` необходимо добавить вручную. Ее исходный код можно взять [здесь](https://github.com/udacity/andfun-kotlin-sleep-tracker-with-recyclerview/blob/Step.04-Solution-Display-SleepQuality-List/app/src/main/java/com/example/android/trackmysleepquality/Util.kt#L51): ```kotlin 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) } } } ``` Кроме этого требуется добавить строки: ```xml %d minutes on %s %d hours on %s %d seconds on %s ``` И ресурс с иконкой `ic_sleep_active` [отсюда](https://github.com/udacity/andfun-kotlin-sleep-tracker-with-recyclerview/blob/Step.04-Solution-Display-SleepQuality-List/app/src/main/res/drawable/ic_sleep_active.xml). Кроме настройки текстовых полей, метод выполняет установку иконки оценки сна из ресурсов, в зависимости от значения поля `sleepQuality` записи данных. Если запустить приложение, то можно удостовериться, что код работает. `RecyclerView` отображает список из элементов, где каждый элемент обладает видом, созданного класса `SleepNightViewHolder`. При добавлении новых записей иконки и текст на элементах обновляется. ## Рефакторинг класса `SleepNightAdapter` Если посмотреть на класс `SleepNightAdapter`, то можно заметить, что он требует небольшого рефакторинга. Так, например, метод `onBindViewHolder` содержит код изменяющий внешний вид компонента `ViewHolder` и логичнее было бы перенести этот код в класс `SleepNightViewHolder`. Кроме того так мы будем соответствовать принципу инкапсуляции кода. **1. Рефакторинг `onBindViewHolder():`** Изменение свойств класса `SleepNightViewHolder` можно поместить в отдельный метод самого класса. Назвать его можно `bind()`. Кроме этого, переменную `res` можно сделать приватным свойством класса. Остальные свойства необходимо сделать приватными, чтобы следовать принципу инкапсуляции и не предоставлять прямой доступ к свойствам другим классам. ```kotlin // 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` как параметра. ```kotlin // 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`. ```kotlin 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()`. ```kotlin override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SleepNightViewHolder { return SleepNightViewHolder.from(parent) } ``` После выполнения рефакторинга также необходимо запустить приложение, чтобы убедиться, что оно продолжает работать правильно.