Android Kotlin Fundamentals 07.2: DiffUtil и привязка данных с RecyclerView

Эта кодовая лаборатория является частью курса Android Kotlin Fundamentals. Вы получите максимальную отдачу от этого курса, если будете последовательно работать с лабораториями кода. Все кодовые лаборатории курса перечислены на целевой странице кодовых лабораторий Android Kotlin Fundamentals .

Введение

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

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

Что вы уже должны знать

  • Создание базового пользовательского интерфейса с использованием действия, фрагментов и представлений.
  • Навигация между фрагментами и использование safeArgs для передачи данных между фрагментами.
  • Просматривайте модели, просматривайте фабрики моделей, преобразования и LiveData и их наблюдателей.
  • Как создать базу данных Room , создать DAO и определить сущности.
  • Как использовать сопрограммы для базы данных и других длительных задач.
  • Как реализовать базовый RecyclerView с Adapter , ViewHolder и макетом элемента.

Что вы узнаете

  • Как использовать DiffUtil для эффективного обновления списка, отображаемого RecyclerView .
  • Как использовать привязку данных с RecyclerView .
  • Как использовать адаптеры привязки для преобразования данных.

Что ты будешь делать

  • Создайте приложение TrackMySleepQuality из предыдущей кодовой лаборатории в этой серии.
  • Обновите SleepNightAdapter , чтобы эффективно обновлять список с помощью DiffUtil .
  • Реализуйте привязку данных для RecyclerView с помощью адаптеров привязки для преобразования данных.

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

Первый экран, показанный слева, имеет кнопки для запуска и остановки отслеживания. На экране показаны некоторые данные о сне пользователя. Кнопка « Очистить » безвозвратно удаляет все данные, которые приложение собрало для пользователя. Второй экран, показанный справа, предназначен для выбора оценки качества сна.

Это приложение спроектировано так, чтобы использовать контроллер пользовательского интерфейса, ViewModel и LiveData , а также базу данных Room для сохранения данных о сне.

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

Вы можете продолжить использовать приложение SleepTracker из предыдущей лаборатории кода или загрузить приложение RecyclerViewDiffUtilDataBinding-Starter с GitHub.

  1. При необходимости загрузите приложение RecyclerViewDiffUtilDataBinding-Starter с GitHub и откройте проект в Android Studio.
  2. Запустите приложение.
  3. Откройте файл SleepNightAdapter.kt .
  4. Изучите код, чтобы ознакомиться со структурой приложения. Обратитесь к приведенной ниже диаграмме для краткого обзора использования RecyclerView с шаблоном адаптера для отображения данных о сне пользователю.

  • На основе пользовательского ввода приложение создает список объектов SleepNight . Каждый объект SleepNight представляет одну ночь сна, ее продолжительность и качество.
  • SleepNightAdapter список объектов SleepNight во что-то, что RecyclerView может использовать и отображать.
  • Адаптер SleepNightAdapter создает ViewHolders , которые содержат представления, данные и метаинформацию для представления ресайклера для отображения данных.
  • RecyclerView использует SleepNightAdapter для определения количества отображаемых элементов ( getItemCount() ). RecyclerView использует onCreateViewHolder() и onBindViewHolder() , чтобы привязать держателей представления к данным для отображения.

Метод notifyDataSetChanged() неэффективен

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

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

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

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

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

К счастью, есть лучший способ.

DiffUtil эффективен и делает всю тяжелую работу за вас

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

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

В этой задаче вы обновите SleepNightAdapter , чтобы использовать DiffUtil для оптимизации RecyclerView для изменений данных.

Шаг 1: Реализуйте SleepNightDiffCallback

Чтобы использовать функциональность класса DiffUtil , расширьте DiffUtil.ItemCallback .

  1. Откройте SleepNightAdapter.kt .
  2. Под полным определением класса SleepNightAdapter создайте новый класс верхнего уровня с именем SleepNightDiffCallback , который расширяет DiffUtil.ItemCallback . Передайте SleepNight в качестве общего параметра.
class SleepNightDiffCallback : DiffUtil.ItemCallback<SleepNight>() {
}
  1. Поместите курсор в имя класса SleepNightDiffCallback .
  2. Нажмите Alt+Enter ( Option+Enter на Mac) и выберите «Реализовать участников ».
  3. В открывшемся диалоговом окне щелкните левой кнопкой мыши, удерживая клавишу Shift, чтобы выбрать areItemsTheSame() и areContentsTheSame() , затем нажмите OK .

    Это создает заглушки внутри SleepNightDiffCallback для двух методов, как показано ниже. DiffUtil использует эти два метода, чтобы выяснить, как изменились список и элементы.
    override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }

    override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
  1. Внутри areItemsTheSame() замените TODO кодом, который проверяет, совпадают ли два переданных элемента SleepNight , oldItem и newItem . Если элементы имеют одинаковый nightId , они являются одним и тем же элементом, поэтому верните true . В противном случае вернуть false . DiffUtil использует этот тест, чтобы определить, был ли элемент добавлен, удален или перемещен.
override fun areItemsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem.nightId == newItem.nightId
}
  1. Внутри areContentsTheSame() проверьте, содержат ли oldItem и newItem одни и те же данные; то есть равны ли они. Эта проверка на равенство проверит все поля, потому что SleepNight — это класс данных. Классы Data автоматически определяют для вас equals и несколько других методов. Если между oldItem и newItem есть различия, этот код сообщает DiffUtil , что элемент был обновлен.
override fun areContentsTheSame(oldItem: SleepNight, newItem: SleepNight): Boolean {
   return oldItem == newItem
}

Распространенным шаблоном является использование RecyclerView для отображения списка, который изменяется. RecyclerView предоставляет класс адаптера ListAdapter , который помогает создать адаптер RecyclerView , поддерживаемый списком.

ListAdapter отслеживает список для вас и уведомляет адаптер, когда список обновляется.

Шаг 1. Измените свой адаптер, чтобы расширить ListAdapter

  1. В файле SleepNightAdapter.kt измените сигнатуру класса SleepNightAdapter на расширение ListAdapter .
  2. При появлении запроса импортируйте androidx.recyclerview.widget.ListAdapter .
  3. Добавьте SleepNight в качестве первого аргумента в ListAdapter перед SleepNightAdapter.ViewHolder .
  4. Добавьте SleepNightDiffCallback() в качестве параметра в конструктор. ListAdapter будет использовать это, чтобы выяснить, что изменилось в списке. Готовая подпись класса SleepNightAdapter должна выглядеть так, как показано ниже.
class SleepNightAdapter : ListAdapter<SleepNight, SleepNightAdapter.ViewHolder>(SleepNightDiffCallback()) {
  1. Внутри класса SleepNightAdapter удалите поле data , включая установщик. Вам это больше не нужно, потому что ListAdapter отслеживает список за вас.
  2. Удалите переопределение getItemCount() , потому что ListAdapter реализует этот метод за вас.
  3. Чтобы избавиться от ошибки в onBindViewHolder() , измените переменную item . Вместо использования data для получения item вызовите метод getItem(position) , предоставляемый ListAdapter .
val item = getItem(position)

Шаг 2: Используйте submitList() для обновления списка

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

  1. Откройте SleepTrackerFragment.kt .
  2. В onCreateView() в обозревателе sleepTrackerViewModel найдите ошибку, в которой содержится ссылка на удаленную вами переменную data .
  3. Замените adapter.data = it вызовом adapter.submitList(it) . Обновленный код показан ниже.

sleepTrackerViewModel.nights.observe(viewLifecycleOwner, Observer {
   it?.let {
       adapter.submitList(it)
   }
})
  1. Запустите свое приложение. Он работает быстрее, может быть, незаметно, если ваш список невелик.

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

Шаг 1. Добавьте привязку данных к файлу макета.

  1. Откройте файл макета list_item_sleep_night.xml на вкладке « Текст ».
  2. Поместите курсор на тег ConstraintLayout и нажмите Alt+Enter ( Option+Enter на Mac). Откроется меню намерения (меню «быстрого исправления»).
  3. Выберите Преобразовать в макет привязки данных . Это оборачивает макет в <layout> и добавляет внутрь тег <data> .
  4. При необходимости прокрутите вверх и внутри <data> объявите переменную с именем sleep .
  5. Сделайте его type полным именем SleepNight , com.example.android.trackmysleepquality.database.SleepNight . Готовый <data> должен выглядеть так, как показано ниже.
   <data>
        <variable
            name="sleep"
            type="com.example.android.trackmysleepquality.database.SleepNight"/>
    </data>
  1. Чтобы принудительно создать объект Binding , выберите Build > Clean Project , затем выберите Build > Rebuild Project . (Если у вас все еще есть проблемы, выберите File > Invalidate Caches/Restart .) Объект привязки ListItemSleepNightBinding вместе со связанным кодом добавляется в созданные файлы проекта.

Шаг 2. Расширьте макет элемента с помощью привязки данных

  1. Откройте SleepNightAdapter.kt .
  2. В классе ViewHolder найдите метод from() .
  3. Удалите объявление переменной view .

Код для удаления :

val view = layoutInflater
       .inflate(R.layout.list_item_sleep_night, parent, false)
  1. Там, где была переменная view , определите новую переменную с именем binding , которая расширяет объект привязки ListItemSleepNightBinding , как показано ниже. Сделайте необходимый импорт объекта привязки.
val binding =
ListItemSleepNightBinding.inflate(layoutInflater, parent, false)
  1. В конце функции вместо возврата view возвращайте binding .
return ViewHolder(binding)
  1. Чтобы избавиться от ошибки, поместите курсор на binding слова. Нажмите Alt+Enter ( Option+Enter на Mac), чтобы открыть меню намерений.
  1. Выберите Изменить тип параметра «itemView» основного конструктора класса «ViewHolder» на «ListItemSleepNightBinding» . Это обновляет тип параметра класса ViewHolder .

  1. Прокрутите вверх до определения класса ViewHolder , чтобы увидеть изменения в подписи. Вы видите ошибку для itemView , потому что вы изменили itemView на binding в методе from() .

    В определении класса ViewHolder щелкните правой кнопкой мыши одно из вхождений itemView и выберите Refactor > Rename . Измените имя на binding .
  2. Добавьте к binding параметра конструктора префикс val , чтобы сделать его свойством.
  3. В вызове родительского класса RecyclerView.ViewHolder измените параметр с binding на binding.root . Вам нужно передать View , а binding.root — это корневой ConstraintLayout в макете вашего элемента.
  4. Готовое объявление класса должно выглядеть так, как показано ниже.
class ViewHolder private constructor(val binding: ListItemSleepNightBinding) : RecyclerView.ViewHolder(binding.root){

Вы также видите ошибку для вызовов findViewById() и исправляете ее дальше.

Шаг 3: Замените findViewById()

Теперь вы можете обновить sleepLength , quality и qualityImage , чтобы использовать объект binding вместо findViewById() .

  1. Измените инициализацию sleepLength , qualityString и qualityImage , чтобы использовать представления объекта binding , как показано ниже. После этого ваш код больше не должен показывать ошибок.
val sleepLength: TextView = binding.sleepLength
val quality: TextView = binding.qualityString
val qualityImage: ImageView = binding.qualityImage

Имея объект привязки, вам больше не нужно определять sleepLength , quality и qualityImage . DataBinding будет кэшировать поиск, поэтому нет необходимости объявлять эти свойства.

  1. Щелкните правой кнопкой мыши sleepLength , quality и qualityImage . Выберите Refactor > Inline или нажмите Control+Command+N ( Option+Command+N на Mac).
  2. Запустите свое приложение. (Возможно, вам придется очистить и перестроить ваш проект, если в нем есть ошибки.)

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

В предыдущей кодовой лаборатории вы использовали класс Transformations для получения LiveData и создания форматированных строк для отображения в текстовых представлениях. Однако, если вам нужно связать разные типы или сложные типы, вы можете предоставить адаптеры привязки, чтобы помочь привязке данных использовать эти типы. Адаптеры привязки — это адаптеры, которые берут ваши данные и адаптируют их во что-то, что привязка данных может использовать для привязки представления, например текста или изображения.

Вы собираетесь реализовать три адаптера привязки, один для качественного изображения и по одному для каждого текстового поля. Таким образом, чтобы объявить адаптер привязки, вы определяете метод, который принимает элемент и представление, и аннотируете его с помощью @BindingAdapter . В теле метода вы реализуете преобразование. В Kotlin вы можете написать адаптер привязки как функцию расширения класса представления, который получает данные.

Шаг 1. Создайте адаптеры привязки

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

  1. Откройте SleepNightAdapater.kt .
  2. Внутри класса ViewHolder найдите метод bind() и напомните себе, что этот метод делает. Вы возьмете код, который вычисляет значения для binding.sleepLength , binding.quality и binding.qualityImage , и вместо этого будете использовать его внутри адаптера. (На данный момент оставьте код как есть; вы переместите его на более позднем этапе.)
  3. В пакете sleeptracker создайте и откройте файл BindingUtils.kt .
  4. Объявите функцию расширения в TextView с именем setSleepDurationFormatted и передайте SleepNight . Эта функция будет вашим адаптером для расчета и форматирования продолжительности сна.
fun TextView.setSleepDurationFormatted(item: SleepNight) {}
  1. В теле setSleepDurationFormatted привяжите данные к представлению, как вы делали это в ViewHolder.bind() . Вызовите convertDurationToFormatted() , а затем установите text TextView в отформатированный текст. (Поскольку это функция расширения TextView , вы можете получить прямой доступ к text свойству.)
text = convertDurationToFormatted(item.startTimeMilli, item.endTimeMilli, context.resources)
  1. Чтобы сообщить привязку данных об этом адаптере привязки, аннотируйте функцию с помощью @BindingAdapter .
  2. Эта функция является адаптером для атрибута sleepDurationFormatted , поэтому передайте sleepDurationFormatted в качестве аргумента @BindingAdapter .
@BindingAdapter("sleepDurationFormatted")
  1. Второй адаптер устанавливает качество сна на основе значения в объекте SleepNight . Создайте функцию расширения с именем setSleepQualityString() для TextView и передайте SleepNight .
  2. В теле привяжите данные к представлению, как вы делали это в ViewHolder.bind() . Вызовите convertNumericQualityToString и установите text .
  3. Аннотируйте функцию с помощью @BindingAdapter("sleepQualityString") .
@BindingAdapter("sleepQualityString")
fun TextView.setSleepQualityString(item: SleepNight) {
   text = convertNumericQualityToString(item.sleepQuality, context.resources)
}
  1. Третий адаптер привязки устанавливает изображение в виде изображения. Создайте функцию расширения в ImageView , вызовите setSleepImage и используйте код из ViewHolder.bind() , как показано ниже.
@BindingAdapter("sleepImage")
fun ImageView.setSleepImage(item: SleepNight) {
   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. Обновите SleepNightAdapter

  1. Откройте SleepNightAdapter.kt .
  2. Удалите все в методе bind() , потому что теперь вы можете использовать привязку данных и ваши новые адаптеры, чтобы сделать эту работу за вас.
fun bind(item: SleepNight) {
}
  1. Внутри bind() назначьте сон item , потому что вам нужно сообщить объекту привязки о вашем новом SleepNight .
binding.sleep = item
  1. Ниже этой строки добавьте binding.executePendingBindings() . Этот вызов представляет собой оптимизацию, которая запрашивает привязку данных для немедленного выполнения любых ожидающих привязок. Всегда рекомендуется вызывать executePendingBindings() , когда вы используете адаптеры привязки в RecyclerView , потому что это может немного ускорить изменение размеров представлений.
 binding.executePendingBindings()

Шаг 3. Добавьте привязки к XML-макету

  1. Откройте list_item_sleep_night.xml .
  2. В ImageView добавьте свойство app с тем же именем, что и у адаптера привязки, который задает изображение. Передайте переменную sleep , как показано ниже.

    Это свойство создает соединение между представлением и объектом привязки через адаптер. Всякий раз, когда ссылаются на sleepImage , адаптер адаптирует данные из SleepNight .
app:sleepImage="@{sleep}"
  1. Сделайте то же самое для текстовых представлений sleep_length и quality_string . Всякий раз, когда ссылаются на sleepDurationFormatted или sleepQualityString , адаптеры будут адаптировать данные из SleepNight .
app:sleepDurationFormatted="@{sleep}"
app:sleepQualityString="@{sleep}"
  1. Запустите свое приложение. Он работает точно так же, как и раньше. Адаптеры привязки берут на себя всю работу по форматированию и обновлению представлений по мере изменения данных, упрощая ViewHolder и придавая коду гораздо лучшую структуру, чем раньше.

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

Поздравляю! На данный момент вы уже на пути к освоению RecyclerView на Android.

Проект Android Studio: RecyclerViewDiffUtilDataBinding .

DiffUtil :

  • В RecyclerView есть класс DiffUtil , предназначенный для вычисления различий между двумя списками.
  • DiffUtil имеет класс ItemCallBack , который вы расширяете, чтобы выяснить разницу между двумя списками.
  • В классе ItemCallback вы должны переопределить areItemsTheSame() и areContentsTheSame() .

ListAdapter :

  • Чтобы бесплатно управлять списками, вы можете использовать класс ListAdapter вместо RecyclerView.Adapter . Однако, если вы используете ListAdapter , вам нужно написать свой собственный адаптер для других макетов, поэтому в этой лаборатории кода показано, как это сделать.
  • Чтобы открыть меню намерений в Android Studio, поместите курсор на любой элемент кода и нажмите Alt+Enter ( Option+Enter на Mac). Это меню особенно полезно для рефакторинга кода и создания заглушек для реализации методов. Меню контекстно-зависимое, поэтому вам нужно точно поместить курсор, чтобы получить правильное меню.

Привязка данных:

  • Используйте привязку данных в макете элемента для привязки данных к представлениям.

Адаптеры для привязки:

  • Ранее вы использовали Transformations для создания строк из данных. Если вам нужно привязать данные разных или сложных типов, предоставьте адаптеры привязки, чтобы помочь привязке данных использовать их.
  • Чтобы объявить адаптер привязки, определите метод, который принимает элемент и представление, и аннотируйте метод с помощью @BindingAdapter . В Kotlin вы можете написать адаптер привязки как функцию расширения для View . Передайте имя свойства, которое адаптирует адаптер. Например:
@BindingAdapter("sleepDurationFormatted")
  • В макете XML задайте свойство app с тем же именем, что и у адаптера привязки. Передайте переменную с данными. Например:
.app:sleepDurationFormatted="@{sleep}"

Удасити курсы:

Документация для разработчиков Android:

Другие источники:

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

  • При необходимости задайте домашнее задание.
  • Объясните учащимся, как сдавать домашние задания.
  • Оценивайте домашние задания.

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

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

Ответьте на эти вопросы

Вопрос 1

Что из следующего необходимо для использования DiffUtil ? Выбрать все, что подходит.

▢ Расширить класс ItemCallBack .

▢ Переопределить areItemsTheSame() .

▢ Переопределить areContentsTheSame() .

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

Вопрос 2

Что из следующего верно в отношении адаптеров привязки?

▢ Адаптер привязки — это функция с аннотацией @BindingAdapter .

▢ Использование адаптера привязки позволяет отделить форматирование данных от держателя представления.

▢ Вы должны использовать RecyclerViewAdapter , если хотите использовать адаптеры привязки.

▢ Адаптеры привязки — хорошее решение, когда вам нужно преобразовать сложные данные.

Вопрос 3

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

▢ Ваши данные просты.

▢ Вы форматируете строку.

▢ Ваш список очень длинный.

▢ Ваш ViewHolder содержит только одно представление.

Начать следующий урок: 7.3: GridLayout с RecyclerView