Здравствуй, дорогой читатель. Каждый 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, для декора наших элементов и т.д.
