RecyclerView
SleepNight
в списке RecyclerView
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
и будут реализованы элементы списка, отображающие время сна, его продолжительность, оценку и иконку оценки.
В этом уроке будет рассмотрен шаблон проектирования — "Адаптер". Он потребуется для правильной реализации работы с RecyclerView
.
RecyclerView
служит для отображения большого количества однотипных данных, например записей какой-либо одной таблицы базы данных. Прежде чем отобразить данные, кто-то должен их подготовить для отображения, создать отдельные элементы списка и поместить на них данные. Этим занимается адаптер.
В случае с приложением "Sleep Tracker" данные хранятся в базе данных Room
. Адаптер необходим для адаптации и подготовки данных к отображению в RecyclerView
.
Адаптер должен предоставлять для RecyclerView
:
Когда 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
:
Далее перейдем к переопределению метода для создания экземпляров ViewHolder
— onCreateViewHolder()
.
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)
}
После выполнения рефакторинга также необходимо запустить приложение, чтобы убедиться, что оно продолжает работать правильно.