RecyclerView для начинающего Android-разработчика

от автора

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

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

Статья предназначена для новичков, которые хотят разобраться со списками в Android.

Все материалы и исходный код можно найти здесь.

Готовый практический пример, который будет разобран в статье.
Готовый практический пример, который будет разобран в статье.

ListView или RecyclerView?

Для реализации какого-то прокручиваемого списка у Android разработчика существуют два пути — ListView и RecyclerView.

Первый виджет интуитивно понятен и довольно прост. Но, к сожалению, имеет много недостатков, например, ListView позволяет создать только вертикальный список.

В свою же очередь RecyclerView «из коробки» предоставляет гораздо больше инструментов для кастомизации и оптимизации списка, чем ListView. Если кратко характеризовать RecyclerView, то можно сказать, что это список на стероидах.

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

Основные компоненты RecyclerView

Для корректной работы RecyclerView необходимо реализовать следующие компоненты:

  • RecyclerView, который необходимо добавить в макет нашего Activity;

  • Adapter, который содержит, обрабатывает и связывает данные со списком;

  • ViewHolder, который служит для оптимизации ресурсов и является своеобразным контейнером для всех элементов, входящих в список;

  • ItemDecorator, который позволяет отрисовать весь декор;

  • ItemAnimator, который отвечает за анимацию элементов при добавлении, редактировании и других операций;

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

Практический пример

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

Реализация примера будет выполнена на языке Kotlin. Также будут использованы библиотеки Glide и Faker, которые никак не относятся к RecyclerView.

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

    implementation 'androidx.recyclerview:recyclerview:1.2.1'     implementation 'com.github.javafaker:javafaker:1.0.2'     implementation 'com.github.bumptech.glide:glide:4.14.2' 

Примечание: в последних версиях AndroidStudio не обязательно подключать библиотеку RecyclerView. Доступен в библиотеке Material.

И необходимо подключить ViewBinding в файле сборки build.gradle нашего приложения:

buildFeatures {         viewBinding = true }

Также необходимо указать разрешение на доступ в Интернет в файле AndroidManifest.xml (для работы с библиотекой Glide):

<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

После создадим макетные файлы: первый для ActivityMain, который хранит RecyclerView, второй для элемента списка (человек).

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:tools="http://schemas.android.com/tools"     android:id="@+id/activityMain"     android:layout_width="match_parent"     android:layout_height="match_parent"     tools:context=".MainActivity">      <androidx.recyclerview.widget.RecyclerView         android:id="@+id/recyclerView"         android:layout_width="match_parent"         android:layout_height="match_parent" />  </FrameLayout>

item_person.xml:

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     xmlns:tools="http://schemas.android.com/tools"     android:layout_width="match_parent"     android:layout_height="wrap_content"     android:background="?selectableItemBackground"     android:paddingStart="10dp"     android:paddingTop="5dp"     android:paddingEnd="10dp"     android:paddingBottom="5dp">      <ImageView         android:id="@+id/imageView"         android:layout_width="wrap_content"         android:layout_height="match_parent"         android:src="@drawable/ic_person"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent" />      <TextView         android:id="@+id/nameTextView"         style="@style/TextAppearance.AppCompat.Body2"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:layout_marginStart="15dp"         app:layout_constraintStart_toEndOf="@id/imageView"         app:layout_constraintTop_toTopOf="parent" />      <TextView         android:id="@+id/companyTextView"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:layout_marginStart="15dp"         app:layout_constraintStart_toEndOf="@id/imageView"         app:layout_constraintTop_toBottomOf="@id/nameTextView" />      <ImageView         android:id="@+id/likedImageView"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:layout_marginEnd="15dp"         android:src="@drawable/ic_like"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintEnd_toStartOf="@id/more"         app:layout_constraintTop_toTopOf="parent" />      <ImageView         android:id="@+id/more"         android:layout_width="wrap_content"         android:layout_height="wrap_content"         android:src="@drawable/ic_more"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintEnd_toEndOf="parent"         app:layout_constraintTop_toTopOf="parent" />  </androidx.constraintlayout.widget.ConstraintLayout>

После того, как все библиотеки были подключены, все макеты созданы и разрешения получены, необходимо создать данные о людях (в настоящем, боевом примере эти данные будут приходить, например, с сервера, но в нашем случае мы создадим их самостоятельно). Для этого создадим класс PersonService и data-class Person:

data class Person(     val id: Long, // Уникальный номер пользователя     val name: String, // Имя человека     val companyName: String, // Название комании     val photo: String, // Ссылка на фото человека     val isLiked: Boolean // Был ли лайкнут пользователь )
class PersonService {      private var persons = mutableListOf<Person>() // Все пользователи      init {         val faker = Faker.instance() // Переменная для создания случайных данных          persons = (1..50).map {             Person(                 id = it.toLong(),                 name = faker.name().fullName(),                 companyName = faker.company().name(),                 photo = IMAGES[it % IMAGES.size],                 isLiked = false             )         }.toMutableList()     }      companion object {         private val IMAGES = mutableListOf(             "https://images.unsplash.com/photo-1600267185393-e158a98703de?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0NjQ0&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1579710039144-85d6bdffddc9?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0Njk1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1488426862026-3ee34a7d66df?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0ODE0&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1620252655460-080dbec533ca?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0NzQ1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1613679074971-91fc27180061?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0NzUz&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1485795959911-ea5ebf41b6ae?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0NzU4&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1545996124-0501ebae84d0?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0NzY1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/flagged/photo-1568225061049-70fb3006b5be?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0Nzcy&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1567186937675-a5131c8a89ea?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0ODYx&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800",             "https://images.unsplash.com/photo-1546456073-92b9f0a8d413?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=600&ixid=MnwxfDB8MXxyYW5kb218fHx8fHx8fHwxNjI0MDE0ODY1&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=800"         )     } }

В классе PersonService хранится лист пользователей, который мы заполняем в init (initializers blocks), и лист ссылок на фотографии.

После создания классов необходимо класс PersonService сделать Singleton для корректной работы. Для этого создадим класс App, и укажем в нем следующее:

class App : Application() {     val personService = PersonService() }

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

Данный класс будет реализовать RecyclerView.Adapter, которому нужен ViewHolder. Соответственно необходимо создать PersonViewHolder, который будет реализовывать RecyclerView.ViewHolder и принимать наш binding.

Также PersonAdapter должен иметь данные, с которыми ему предстоит работать. Для этого создадим пустой список и перепишем его сеттер. В итоге получаем:

class PersonAdapter : RecyclerView.Adapter<PersonAdapter.PersonViewHolder>() {      var data: List<Person> = emptyList()         set(newValue) {             field = newValue             notifyDataSetChanged()         }      class PersonViewHolder(val binding: ItemPersonBinding) : RecyclerView.ViewHolder(binding.root) }

Но для работы адаптера необходимо переопределить минимум три метода (AndroidStudio подскажет нам).

Метод getItemCount, который будет возвращать количество элементов нашего списка с данными;

Метод onCreateViewHolder, в котором будет происходить создание ViewHolder. Данный метод принимает в себя parent и viewType (используется в том случае, если в списке будут разные типы элементов списка);

Метод onBindViewHolder, в котором будет происходить отрисовка всех элементов в объекте списка (имя человека, компания и т.д.):

После переопределения методов и их реализации получаем:

    override fun getItemCount(): Int = data.size // Количество элементов в списке данных      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PersonViewHolder {         val inflater = LayoutInflater.from(parent.context)         val binding = ItemPersonBinding.inflate(inflater, parent, false)          return PersonViewHolder(binding)     }      override fun onBindViewHolder(holder: PersonViewHolder, position: Int) {         val person = data[position] // Получение человека из списка данных по позиции         val context = holder.itemView.context          with(holder.binding) {             val color = if (person.isLiked) R.color.red else R.color.grey // Цвет "сердца", если пользователь был лайкнут              nameTextView.text = person.name // Отрисовка имени пользователя             companyTextView.text = person.companyName // Отрисовка компании пользователя             likedImageView.setColorFilter( // Отрисовка цвета "сердца"                 ContextCompat.getColor(context, color),                 android.graphics.PorterDuff.Mode.SRC_IN             )             Glide.with(context).load(person.photo).circleCrop() // Отрисовка фотографии пользователя с помощью библиотеки Glide                 .error(R.drawable.ic_person)                  .placeholder(R.drawable.ic_person).into(imageView)         }     }

На этом наш простой адаптер, который будет просто выводить горизонтальный список готов.

Теперь необходимо повесить на наш RecyclerView созданный адаптер и LayoutManager. Для этого в классе MainActivity пропишем следующее:

class MainActivity : AppCompatActivity() {      private lateinit var binding: ActivityMainBinding     private lateinit var adapter: PersonAdapter // Объект Adapter     private val personService: PersonService // Объект PersonService         get() = (applicationContext as App).personService      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         binding = ActivityMainBinding.inflate(layoutInflater)         setContentView(binding.root)          val manager = LinearLayoutManager(this) // LayoutManager         adapter = PersonAdapter() // Создание объекта         adapter.data = personService.getPersons() // Заполнение данными          binding.recyclerView.layoutManager = manager // Назначение LayoutManager для RecyclerView         binding.recyclerView.adapter = adapter // Назначение адаптера для RecyclerView     } } 

Запускаем наше приложение на устройстве и получаем список пользователей!

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

В классе PersonService добавим три метода:

  • likePerson — лайкаем человека;

  • removePerson — удаляем человека;

  • movePerson — перемещаем человека (принимает человека и куда надо переместить: «1» — вниз, «-1» — вверх).

    fun likePerson(person: Person) {         val index = persons.indexOfFirst { it.id == person.id } // Находим индекс человека в списке         if (index == -1) return // Останавливаемся, если не находим такого человека          persons = ArrayList(persons) // Создаем новый список         persons[index] = persons[index].copy(isLiked = !persons[index].isLiked) // Меняем значение "лайка" на противоположное     }      fun removePerson(person: Person) {         val index = persons.indexOfFirst { it.id == person.id } // Находим индекс человека в списке         if (index == -1) return // Останавливаемся, если не находим такого человека          persons = ArrayList(persons) // Создаем новый список         persons.removeAt(index) // Удаляем человека     }      fun movePerson(person: Person, moveBy: Int) {         val oldIndex = persons.indexOfFirst { it.id == person.id } // Находим индекс человека в списке         if (oldIndex == -1) return // Останавливаемся, если не находим такого человека          val newIndex = oldIndex + moveBy // Вычисляем новый индекс, на котором должен находится человек         persons = ArrayList(persons) // Создаем новый список         Collections.swap(persons, oldIndex, newIndex) // Меняем местами людей             }

После того, как были созданы методы взаимодействия с людьми, в классе PersonService необходимо объявить слушателя:

typealias PersonListener = (persons: List<Person>) -> Unit

Так же создадим список слушателей, два метода, которые будут добавлять и удалять слушателей, и один, который будет «регистрировать изменения»:

    private var listeners = mutableListOf<PersonListener>() // Все слушатели      fun addListener(listener: PersonListener) {         listeners.add(listener)         listener.invoke(persons)     }      fun removeListener(listener: PersonListener) {         listeners.remove(listener)         listener.invoke(persons)     }      private fun notifyChanges() = listeners.forEach { it.invoke(persons) }

Метод notifyChanges необходимо обязательно вызвать в методах, в которых происходит модификация данных, то есть в методах likePerson, removePerson и movePerson.

На этом наш сервис людей полностью готов. Перейдем в PersonAdapter, в котором реализуем обработку событий наших людей. Создадим интерфейс PersonActionListener, в котором буду четыре метода:

  • onPersonGetId — получить уникальный номер выбранного человека;

  • onPersonLike — человек был лайкнут;

  • onPersonRemove — удалить человека;

  • onPersonMove — переместить человека.

interface PersonActionListener {     fun onPersonGetId(person: Person)     fun onPersonLike(person: Person)     fun onPersonRemove(person: Person)     fun onPersonMove(person: Person, moveBy: Int) }

Класс PersonAdapter во входные параметры будет принимать наш интерфейс. Также данный класс должен реализовать интерфейс OnClickListener. В итоге сигнатура объявления класса PersonAdaper выглядит следующим образом:

class PersonAdapter(private val personActionListener: PersonActionListener) :     RecyclerView.Adapter<PersonAdapter.PersonViewHolder>(), View.OnClickListener {

Теперь в классе PersonAdapter в методе onBindViewHolder кладём в tag каждого view, на которую будет происходить нажатие, нужного человека:

holder.itemView.tag = person holder.binding.likedImageView.tag = person holder.binding.more.tag = person

Теперь в методе onCreateViewHolder необходимо проинициализировать слушателей при нажатии. В данном примере будет слушатель на нажатие на элемент списка, кнопку more (три точки) и likedImageView (сердце):

binding.root.setOnClickListener(this) binding.more.setOnClickListener(this) binding.likedImageView.setOnClickListener(this)

Теперь создадим метод showPopupMenu, который будет «рисовать» выпадающее меню с доступными действиями, а именно: удалить пользователя, переместить вверх, переместить вниз:

    private fun showPopupMenu(view: View) {         val popupMenu = PopupMenu(view.context, view)         val person = view.tag as Person         val position = data.indexOfFirst { it.id == person.id }          popupMenu.menu.add(0, ID_MOVE_UP, Menu.NONE, "Up").apply {             isEnabled = position > 0         }         popupMenu.menu.add(0, ID_MOVE_DOWN, Menu.NONE, "Down").apply {             isEnabled = position < data.size - 1         }         popupMenu.menu.add(0, ID_REMOVE, Menu.NONE, "Remove")          popupMenu.setOnMenuItemClickListener {             when (it.itemId) {                 ID_MOVE_UP -> personActionListener.onPersonMove(person, -1)                 ID_MOVE_DOWN -> personActionListener.onPersonMove(person, 1)                 ID_REMOVE -> personActionListener.onPersonRemove(person)             }             return@setOnMenuItemClickListener true         }          popupMenu.show()     }      companion object {         private const val ID_MOVE_UP = 1         private const val ID_MOVE_DOWN = 2         private const val ID_REMOVE = 3     }

В методе onClick обработаем нажатия на элементы списка:

    override fun onClick(view: View) {         val person: Person = view.tag as Person // Получаем из тэга человека          when (view.id) {             R.id.more -> showPopupMenu(view)             R.id.likedImageView -> personActionListener.onPersonLike(person)             else -> personActionListener.onPersonGetId(person)         }     }

На этом наш адаптер готов. Теперь перейдем в MainActivity и, при инициализации нашего адаптера, передадим реализацию интерфейса:

adapter = PersonAdapter(object : PersonActionListener { // Создание объекта    override fun onPersonGetId(person: Person) =       Toast.makeText(this@MainActivity, "Persons ID: ${person.id}", Toast.LENGTH_SHORT).show()     override fun onPersonLike(person: Person) = personService.likePerson(person)     override fun onPersonRemove(person: Person) = personService.removePerson(person)     override fun onPersonMove(person: Person, moveBy: Int) = personService.movePerson(person, moveBy)  })

Также добавим слушателя в MainActivity, который будет прослушивать изменения, происходящие в PersonService:

private val listener: PersonListener = {adapter.data = it}

И в методе onCreate добавим этого слушателя:

personService.addListener(listener)

На этом наша работа выполнена. Запускаем проект и смотрим результат:

Результат.
Результат.

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

Ссылка на репозиторий.


ссылка на оригинал статьи https://habr.com/ru/post/705064/


Комментарии

Добавить комментарий

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