Android RecyclerView: что такое, учитесь на простых примерах

⚡ Умное резюме

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

  • 📱 Что это: RecyclerView — это гибкий преемник ListView и GridView для отображения больших массивов данных.
  • 🧱 Представления и группы представлений: Android Интерфейс отрисовывается с использованием классов View и ViewGroup, а LayoutParams управляет размером и положением.
  • 🔄 Адаптер: Шаблон адаптера преобразует массив данных в ViewHolders, которые может принимать RecyclerView.
  • 📐 Менеджер макетов: Менеджер компоновки располагает элементы линейно или в виде сетки и указывает, когда элементы интерфейса можно использовать повторно.
  • ♻️ Утилизация отходов: Элементы, находящиеся за пределами экрана, используются повторно, а не создаются заново, что экономит память и обеспечивает плавную прокрутку.
  • 🇧🇷 Шаги сборки: Добавьте Gradle Создайте зависимость, макет элемента, ViewHolder, адаптер и LayoutManager.

Android RecyclerView

Что такое RecyclerView Android?

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

Виды

Android Платформа использует классы View и ViewGroup для отрисовки элементов на экране. Эти классы являются аббревиатурами.tracКлассы `TextView` и `EditText` расширяются до различных реализаций в зависимости от конкретного случая использования. Например, `TextView` имеет простую цель — отображение текстового содержимого на экране. `EditText` наследуется от того же класса `View` и добавляет дополнительные функции, позволяющие пользователю вводить данные.

Можно создавать собственные пользовательские представления для большей гибкости при разработке.ping пользовательские интерфейсы. Класс View предоставляет методы, которые мы можем переопределить для отрисовки на экране, а также средства для передачи параметров, таких как ширина, высота и наши собственные пользовательские атрибуты, которые мы хотели бы добавить к нашему View, чтобы он вел себя так, как нам нужно.

Группы просмотра

Класс ViewGroup — это разновидность класса View, но, в отличие от простого класса View, задача которого — просто отображение, ViewGroup позволяет объединять несколько представлений в одно, на которое можно ссылаться как на единое целое. В этом случае представление, созданное на верхнем уровне, к которому мы добавляем другие простые представления, называется «родительским», а представления, добавленные внутрь, — «дочерними».

Мы можем представить View как массив, а ViewGroup как массив массивов. Учитывая, что массив массивов сам по себе является массивом, мы можем понять, как ViewGroup можно рассматривать как View.

var arr1 = [1,2,3] //imagine a simple View as an Array
//we can imagine this as a NumberTextView which doesn't really exist
//but we could imagine there's one that makes it easy to use numbers
var arr2 = ["a","b","c"] // We can imagine this as another simple view

var nestedArr = [arr1,arr2] //in our analogy, we can now group views
//together and the structure that would hold that would be what we call the ViewGroup

ViewGroup также позволяет нам определять, как дочерние элементы организованы внутри представления, например, расположены ли они вертикально или горизонтально. Мы можем установить различные правила взаимодействия внутри представления. Например, текстовые поля, следующие друг за другом, должны иметь расстояние 12dp, а изображение, за которым следует текстовое поле, — расстояние 5dp.

Так было бы, если бы мы были развивающимисяping Создаем собственный ViewGroup с нуля. Для упрощения этих настроек, Android предоставляет класс LayoutParams, который мы можем использовать для ввода этих конфигураций.

Android документации Предоставляет некоторые параметры по умолчанию, которые мы бы использовали при настройке собственного ViewGroup. Некоторые распространенные параметры относятся к ширине, высоте и отступам. По умолчанию эти конфигурации имеют структуру android:layout_height для высоты и android:layout_width для ширины. В этом отношении при создании ViewGroup вы можете дополнительно создать LayoutParams, специфичные для того, как вы хотите, чтобы ваш ViewGroup себя вел.

Android В комплект входят стандартные представления и группы представлений, которые можно использовать для выполнения многих распространенных задач. Например, мы уже упоминали TextView. Это простое представление с настраиваемыми параметрами, такими как высота, ширина и размер текста. Также, как мы уже говорили, есть ImageView для отображения изображений и EditText, и это далеко не полный список. Android также есть пользовательские группы представлений, в которые мы можем добавлять наши представления и получать ожидаемое поведение.

Линейный макет

LinearLayout позволяет добавлять в него элементы View. У LinearLayout есть атрибут orientation, который определяет, как он будет располагаться на экране. Также у него есть LinearLayout.LayoutParams, которые задают правила для элементов View внутри. Например, атрибут android:center_horizontal центрирует элементы View по горизонтальной оси, а android:center_vertical — по вертикальной оси.

Вот несколько изображений, которые помогут понять принцип центрирования. Предположим, это простой элемент TextView в пространстве размером 200x200 пикселей; атрибуты центрирования заставят его вести себя следующим образом.

Android:center_horizontal

Горизонтально центрированный контент

Горизонтально центрированный контент

Android:center_vertical

Содержимое, центрированное по вертикали

Вертикально центрированное содержимое

андроид: центр

Центрированное содержимое

Центрированное содержимое

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

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

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

Ниже приведены важные компоненты RecyclerView:

RecyclerView.Адаптер

Основа №1 – Шаблон адаптера

Адаптер — это устройство, которое преобразует характеристики одной системы или устройства в характеристики несовместимого устройства или системы. Некоторые адаптеры изменяют характеристики сигнала или питания, в то время как другие просто адаптируют физическую форму одного разъема к другому.

Простой пример из реальной жизни, иллюстрирующий необходимость адаптера, — это ситуация, когда нам нужно соединить устройства, но их разъемы не совпадают. Это может произойти, например, в другой стране, где используются другие типы розеток. Если вы возьмете с собой зарядное устройство для телефона или ноутбука, подключить его к розетке будет невозможно. Однако вы не сдадитесь и просто приобретете адаптер, который соединяет розетку и зарядное устройство и позволяет осуществлять зарядку.

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

Рассмотрим простой пример устройства и зарядного устройства. У нас будет два зарядных устройства: американское и британское.

class AmericanCharger() {
    var chargingPower = 10
}
class BritishCharger(){
    var chargingPower = 5
}

Затем мы создадим два устройства.

class AmericanDevice()
class BritishDevice()

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

var myAmericanPhone = new AmericanDevice()
var myBritishPhone = new BritishDevice()

Затем мы введем концепцию зарядки для обоих устройств, добавив в них метод `charge()`. Этот метод принимает в качестве параметра соответствующее зарядное устройство и осуществляет зарядку в зависимости от него.

sealed trait Device
class AmericanDevice : Device{
    fun charge(charger:AmericanCharger){
        //Do some American charging
    }
}
class BritishDevice: Device{
    fun charge(charger:BritishCharger){
        //Do some British charging
    }
}

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

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

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

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

class AmericanToBritishChargerAdapter(theAmericanCharger:AmericanCharger){
    fun returnNewCharger(): BritishCharger{
            //convert the American charger to a BritishCharger
            //we would change the American charging functionality
            //to British charging functionality to make sure the
            //adapter doesn't destroy the device. The adapter could
            //, for example, control the power output by dividing by 2
            //our adapter could encompass this functionality in here

           var chargingPower:Int = theAmericanCharger.chargingPower / 2
           var newBritishCharger = new BritishCharger()
           newBritishCharger.chargingPower = theAmericanCharger.chargingPower/2

        return newBritishCharger
    }
}

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

var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()

Попытка вызвать метод charge() у объекта myBritishDevice с помощью americanChargerIFound не сработает, поскольку AmericanDevice принимает только AmericanCharger. Поэтому это невозможно:

var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()
myBritishDevice.charge(americanChargerIFound)

В этом сценарии созданный нами адаптер AmericanToBritishChargerAdapter может оказаться очень полезным. Мы можем использовать метод returnNewCharger() для создания нового устройства BritishCharger, которое мы сможем использовать для зарядки. Все, что нам нужно сделать, это создать экземпляр нашего адаптера и передать ему имеющийся у нас AmericanCharger, и он создаст устройство BritishCharger, которое мы сможем использовать.

var myBritishDevice = new BritishDevice()
var americanChargerIFound = new AmericanCharger()
//We create the adapter and feed it the americanCharger
var myAdapter =  AmericanToBritishChargerAdapter(americanChargerIFound)
//calling returnNewCharger from myAdapter would return a BritishCharger
var britishChargerFromAdapter = myAdapter.returnNewCharger()
//and once we have the britishCharger we can now use it
myBritishDevice.charge(britishChargerFromAdapter)

RecyclerView.LayoutManager

При работе с ViewGroup внутри него размещаются представления (Views). Задача LayoutManager — описать, как представления располагаются внутри него.

Для сравнения, при работе с ViewGroup на основе LinearLayout нам нужна возможность размещать элементы как вертикально, так и горизонтально. Это легко реализовать, добавив атрибут ориентации, который указывает, как LinearLayout будет размещен на экране. Мы можем сделать это, используя android:orientation=VERTICAL|HORIZONTAL атрибутов.

У нас также есть еще один ViewGroup, называемый GridLayout. Он используется, когда нам нужно разместить представления в прямоугольной сетке. Это может быть необходимо, например, для того, чтобы сделать данные, предоставляемые пользователю приложения, более удобными для восприятия. По своей конструкции GridLayout позволяет настраивать параметры, чтобы помочь вам достичь этой цели, определяя размеры сетки; например, мы можем использовать сетку 4×4 или 3×2.

RecyclerView.ViewHolder

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

Большие списки

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

Если взять в качестве примера список контактов, у нас будет общее представление о том, как будет выглядеть один контакт в списке. Затем мы создадим шаблонный макет, который фактически представляет собой представление (View), с ячейками, куда будут помещаться различные данные из нашего списка контактов. Ниже приведён псевдокод, объясняющий всю цель:

//OneContactView
<OneContact>
<TextView>{{PlaceHolderForName}}</TextView>
<TextView>{{PlaceHolderForAddress}}</TextView>
<ImageView>{{PlaceHolderForProfilePicture}}</ImageView>
<TextView>{{PlaceHolderForPhoneNumber}}</TextView>
</OneContact>

В таком случае у нас будет список контактов следующего вида:

<ContactList>
</ContactList>

Если бы мы жестко задавали содержимое, у нас не было бы программного способа добавлять новый контент в список без переписывания приложения. К счастью, добавление представления в группу представлений поддерживается с помощью addView(view:View) метод. Тем не менее, это не тот способ, которым RecyclerView добавляет дочерние представления.

В нашем случае у нас будет длинный список контактов. Для каждого контакта в списке нам потребуется создать OneContactView и заполнить его данными, соответствующими полям нашего класса Contact. Затем, получив представление, нам нужно будет добавить его в RecyclerView, чтобы отобразить список.

data  class Contact(var name:String, var address:String, var pic:String, var phoneNumber:Int)

var contact1 = Contact("Guru","Guru97", "SomePic1.jpg", 991)
var contact2 = Contact("Guru","Guru98", "SomePic2.jpg", 992)
var contact3 = Contact("Guru","Guru99", "SomePic3.jpg", 993)

var myContacts:ArrayList<Contact> = arrayListOf<Contact>(contact1,contact2,contact3)

У нас есть массив контактов. В OneContactView есть слоты для получения содержимого из класса Contact и его отображения. В RecyclerView нам нужно добавить Views, чтобы он мог помочь нам с возможностью повторного использования.

RecyclerView не позволяет нам напрямую добавлять представление, но дает возможность добавить ViewHolder. Таким образом, в этом сценарии у нас есть два элемента, которые мы хотим соединить, но которые не совпадают. Вот тут-то и пригодится наш адаптер. RecyclerView предоставляет нам адаптер, подобно нашему AmericanToBritishChargerAdapter() Благодаря этому нам удалось превратить наше американское зарядное устройство AmericanCharger, которое было непригодно для использования с нашим британским устройством BritishDevice, в нечто пригодное для использования, похожее на настоящий адаптер питания.

В этом сценарии адаптер возьмет наш массив контактов и наше представление, и на основе этого сгенерирует ViewHolders, которые RecyclerView будет готов принять.

RecyclerView предоставляет интерфейс, который мы можем расширить для создания нашего адаптера через класс RecyclerView.Adapter. Внутри этого адаптера находится способ создания класса ViewHolder, с которым RecyclerView хочет работать. Таким образом, у нас та же ситуация, что и раньше, но с одним дополнительным элементом — адаптером.

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

fun createViewHolder(@NonNull parent: ViewGroup, viewType: Int): ViewHolder

RecyclerView.ViewHolder — это аббревиатура.tracЭто класс, который принимает наш View в качестве аргумента и преобразует его в ViewHolder. Он использует шаблон обертки, применяемый для расширения возможностей классов.

Основа № 2 – Шаблон обертки

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

sealed trait Animal{
    fun sound():String
}

data class Cat(name:String):Animal{
    fun sound(){
        "Meow"
        }
}
data class Dog(name:String):Animal{
    fun sound(){
        "Woof"
        }
}

var cat1 = Cat("Tubby")
var dog1 = Dog("Scooby")
cat1.sound() //meow
dog1.sound() //woof

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

class SpeechPoweredAnimalByWrapper(var myAnimal:Animal){

    fun sound(){
        myAnimal.sound()
    }

    fun speak(){
        println("Hello, my name is ${myAnimal.name}")
    }
}

Теперь мы можем передать экземпляр животного в SpeechPoweredAnimalByWrapper. Вызов метода sound() этого экземпляра вызовет переданный метод sound() животного. У нас также есть дополнительный метод speak(), который считается новой функциональностью, добавляемой к переданным животным. Мы можем использовать его следующим образом:

var cat1 =  Cat("Garfield")
cat1.sound()//"meow"
cat1.speak()// doesn't work as it isn't implemented
var talkingCat = new SpeechPoweredAnimalByWrapper(cat1)
talkingCat.sound() //"meow" the sound method calls the one defined for cat1
talkingCat.speak() //"Hello, my name is Garfield"

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

В приведенном выше примере мы использовали конкретный класс. Также можно реализовать то же самое в абсолютном классе.tracкласс t. Нам нужно будет изменить класс SpeechPoweredAnimalByWrapper на abs.tracИ на этом всё. Мы изменим название класса на более короткое, чтобы оно было более читабельным.

abstract class SpeechPowered(var myAnimal:Animal){

    fun sound(){
        myAnimal.sound()
    }

    fun speak(){
        println("Hello, my name is ${myAnimal.name}")
    }
}

Всё то же самое, что и раньше, но это будет означать нечто другое. В обычном классе мы можем иметь экземпляр класса точно так же, как мы создали cat1 и dog1. АбсолютноtracОднако классы типа `t` не предназначены для создания экземпляров, а предназначены для расширения других классов. Итак, как же нам использовать новый объект `SpeechPowered(var myAnimal:Animal)`?tracЧто такое класс T? Мы можем использовать его, создавая новые классы, которые будут его расширять и, в свою очередь, получать его функциональность.

В нашем примере мы создадим класс SpeechPoweredAnimal, который наследует этот класс.

class SpeechPoweredAnimal(var myAnimal:Animal):SpeechPowered(myAnimal)
var cat1 =  Cat("Tubby")
var speakingKitty = SpeechPoweredAnimal(cat1)
speakingKitty.speak() //"Hello, my name is Tubby"

Это тот же шаблон, что и в ViewHolder. Класс RecyclerView.ViewHolder является абстракцией.tracЭто класс, который добавляет функциональность к представлению, подобно тому, как мы добавили метод speak к животным. Добавленная функциональность обеспечивает его работу при взаимодействии с RecyclerView.

Вот как мы создадим OneContactViewHolder из OneContactView:

//The View argument we pass is converted to a ViewHolder which uses the View to give it more abilities and in turn work with the RecyclerView
class OneContactViewHolder(ourContactView: View) : RecyclerView.ViewHolder(ourContactView)

RecyclerView имеет адаптер, который позволяет нам подключить наш массив контактов к ContactsView с помощью RecyclerView.

Добавление представления

ViewGroup не перерисовывается автоматически, а следует определенному расписанию. На вашем устройстве перерисовка может происходить каждые 10 или 100 мс; или, если мы выберем абсурдное число, скажем, 1 минуту, то при добавлении View в ViewGroup вы увидите изменения через 1 минуту, когда ViewGroup «обновится».

RecyclerView.Recycler

Подготовка №3 – Кэширование

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

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

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

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

Информация, которую мы отправляем на веб-сайт о содержимом, называется метаданными. По сути, мы не просто говорим: «Мы хотим загрузить ваш сайт», мы говорим: «Мы хотим загрузить ваш сайт, и вот часть контента, которую мы уже сохранили с момента последней загрузки; пожалуйста, используйте это для отправки нам только того, чего там нет, чтобы мы не использовали много пропускной способности».

Звонки по макету: список твитов, должно быть, сумасшедший

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

В случае, если мы вызываем этот метод до того, как добавим LayoutManager к нашему RecyclerView и до того, как добавим RecyclerView.Adapter, вызов scrollToPosition(n:Int) просто игнорируется.

Связь между компонентами RecyclerView

Подготовка №4 – Повторные звонки

В процессе своей работы RecyclerView включает в себя множество компонентов. Он взаимодействует с LayoutManager, который определяет, как организовать представления — линейно или в виде сетки. Он также взаимодействует с адаптером, который преобразует элементы (contactList) в представления (OneContactView), а затем в ViewHolders (OneContactViewHolder), с которыми RecyclerView готов работать, используя предоставляемые им методы.

Исходным материалом для RecyclerView являются наши представления (например, OneContactView) и источник данных.

contactList:Array<Contact>

В качестве отправной точки мы использовали простой сценарий, чтобы понять, чего пытается достичь RecyclerView. Базовый случай, когда у нас есть статический массив из 1000 контактов, которые мы хотим показать пользователю, легко понять. Механизм RecyclerView по-настоящему оживает, когда список перестает быть статическим. При работе с динамическим списком нам нужно подумать о том, что происходит с представлением на экране при добавлении или удалении элемента из списка.

RecyclerView.LayoutManager

Помимо определения способа размещения элементов интерфейса — линейно или в виде сетки — LayoutManager выполняет большую часть работы «под капотом».ping Сотрудники службы переработки знают, когда нужно сдать отходы на переработку.

Оно отвечает за...ping tracМенеджер макета (LayoutManager) управляет k элементами, которые в данный момент видны на экране, и передает эту информацию механизму повторного использования. Когда пользователь прокручивает страницу вниз, LayoutManager отвечает за информирование системы повторного использования о элементах, которые теряют фокус в верхней части экрана, чтобы их можно было повторно использовать, вместо того чтобы они оставались там и потребляли память или уничтожались только для создания новых.

Это означает, что LayoutManager должен сохранять track определяет положение пользователя при прокрутке списка. Это достигается за счет использования списка позиций, основанных на индексах, то есть первый элемент начинается с 0 и увеличивается в соответствии с количеством элементов в списке.

Если мы можем просмотреть 10 элементов из нашего списка, скажем, из 100, то вначале LayoutManager знает, что в фокусе находятся элементы от View-0 до View-9. По мере прокрутки LayoutManager может вычислить, какие элементы выходят из фокуса.

LayoutManager позволяет передавать эти представления в механизм повторного использования (к ним можно привязывать новые данные; например, контактные данные представления можно удалить, а новые контактные данные из следующего сегмента заменить заполнителями).

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

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

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

Многие основные операции, связанные с RecyclerView, вращаются вокруг взаимодействия между RecyclerView.LayoutManager и RecyclerView.Adapter, которые хранят наши списки данных, иногда статических, а иногда динамических.

Кроме того, RecyclerView предоставляет нам методы, которые мы можем использовать для отслеживания событий, например, onBindViewHolder, когда наш RecyclerView.Adapter привязывает контент из нашего списка (например, контакт) к ViewHolder, чтобы тот использовался для отображения информации на экране.

Ещё один способ — onCreateViewHolder, который сообщает нам, когда RecyclerView.Adapter берёт обычное представление, например OneContactView, и преобразует его в элемент ViewHolder, с которым может работать RecyclerView.

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

Как нам известно, RecyclerView.LayoutManager который представляет пользователю представления, может на мгновение иметь другой список элементов, чем RecyclerView.Adapter Список контактов хранится в базе данных или поступает из источника в потоковом режиме. Размещение событий OnClick непосредственно в представлениях может привести к неожиданному поведению, например, к удалению не того контакта.

Gradle

Если мы хотим использовать RecyclerView, нам нужно добавить его в качестве зависимости в файл build.gradle. В следующем примере мы использовали реализацию “androidx.recyclerview:recyclerview:1.1.0”, которая является самой актуальной версией согласно этой статье.

После добавления зависимости в наш Gradle файл, нам будет предложено Android Studio для синхронизации изменений. Вот как это работает у нас. Gradle Этот файл будет использоваться после добавления RecyclerView в пустой проект с настройками по умолчанию.

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.guru99.learnrecycler"
        minSdkVersion 17
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
             'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation "androidx.recyclerview:recyclerview:1.1.0"
}

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

Список предметов

Перейдем к файлу MainActivity и создадим массив с названиями фруктов непосредственно перед методом onCreate(), который был сгенерирован во время настройки.

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

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

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

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/fruitName"
    />
</TextView>

В приведенном выше TextView мы добавили поле id, которое будет использоваться для идентификации представления. Оно не генерируется по умолчанию. Мы присвоили нашему TextView идентификатор fruitName, чтобы он соответствовал данным, которые будут к нему привязаны.

Добавление RecyclerView в основной макет

В том же действии есть файл макета main_layout.xml, который был сгенерирован для нас по умолчанию. Если бы мы выбрали пустой проект, он бы был сгенерирован. XML Внутри будет находиться ConstraintLayout, а внутри — TextView с текстом «Hello». Мы удалим всё содержимое, и в макете останется только RecyclerView, как показано ниже:

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView 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:id="@+id/fruitRecyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity" />

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

android:id="@+id/fruitRecyclerView"

Затем мы вернёмся к файлу MainActivity. Используя созданные нами идентификаторы, мы сможем ссылаться на только что созданные представления. Начнём с обращения к RecyclerView с помощью метода findViewById(), предоставляемого библиотекой. AndroidМы сделаем это в нашем методе onCreate(). Наш метод onCreate() будет выглядеть следующим образом.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
}

Создайте ViewHolder

Далее мы создадим RecyclerView.ViewHolder, который будет отвечать за преобразование нашего View в ViewHolder, который RecyclerView будет использовать для отображения элементов. Мы сделаем это сразу после метода onCreate().

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }

    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

}

Создайте RecyclerViewAdapter

Далее мы создадим класс FruitArrayAdapter, который расширяет класс RecyclerView.Adapter. Созданный нами класс FruitArrayAdapter будет отвечать за следующее: он будет брать названия фруктов из массива фруктов, создавать ViewHolder, используя наше представление one_fruit_view.xml, затем привязывать фрукты к ViewHolder и динамически привязывать содержимое к созданному нами представлению.

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)
    }

    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

    class FruitArrayAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>()
}

Android Studio Это добавит красные волнистые линии к нашему FruitArrayAdapter, указывая на необходимость реализации методов, которые RecyclerView сможет использовать для подключения нашего массива к ViewHolder.

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

     }

     override fun getItemCount(): Int {

     }

     override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

     }
 }

Начнём с самого простого фрагмента сгенерированного кода — метода getItemCount(). Мы знаем, как получить количество элементов в нашем массиве, вызвав свойство size массива.

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {
        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

        }

        override fun getItemCount(): Int {
         return fruitArray.size
        }

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

        }
    }

Затем мы реализуем метод onCreateViewHolder. Именно здесь RecyclerView просит нас помочь ему создать FruitViewHolder. Если мы помним, наш класс FruitViewHolder выглядел следующим образом:

class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

Для этого требуется наш объект fruitView, который мы создали в виде XML-файла one_fruit_view.xml. Мы можем создать ссылку на этот XML-файл и преобразовать его в представление следующим образом.

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder

        }

        override fun getItemCount(): Int {
            return fruitArray.size
        }

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

        }
    }

Оставшаяся часть — это переопределение метода onBindViewHolder.

fun onBindViewHolder(holder: FruitViewHolder, position: Int)

RecyclerView.Adapter запрашивает целочисленную позицию, которую мы используем для получения элемента из нашего списка. Он также предоставляет нам держатель, чтобы мы могли привязать полученный из fruitArray элемент к представлению, которое находится внутри держателя представления. Представление, находящееся внутри держателя представления, доступно через поле ViewHolder.itemView. Получив представление, мы можем использовать созданный ранее идентификатор fruitName для установки содержимого.

override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

          var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

          var aFruitName = fruitArray.get(position)

          ourFruitTextView.setText(aFruitName)
      }

Таким образом, наш FruitArrayAdapter готов и выглядит следующим образом.

class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder
        }

        override fun getItemCount(): Int {
            return fruitArray.size
        }

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }
    }

Наконец, мы готовы подключить оставшиеся компоненты нашего RecyclerView, а именно LayoutManager, который будет указывать RecyclerView, как отображать содержимое списка: линейно, используя LinearLayoutManager, или в виде сетки, используя GridLayoutManager или StaggeredGridLayoutManager.

Создать менеджер макетов

Вернемся в функцию onCreate и добавим LayoutManager.

override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_main)

       var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)

       var fruitLinearLayout = LinearLayoutManager(this)

       myFruitRecyclerView.layoutManager =fruitLinearLayout

   }

Подключите наш адаптер к предметам и установите его на RecyclerView.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)

        var fruitLinearLayout = LinearLayoutManager(this)

        myFruitRecyclerView.layoutManager =fruitLinearLayout

        var fruitListAdapter = FruitListAdapter(fruitNames)

        myFruitRecyclerView.adapter =fruitListAdapter
    }

Мы также создали экземпляр fruitListAdapter и передали ему массив названий фруктов. И, по сути, на этом всё. Полный файл MainActivity.kt выглядит следующим образом.

package com.guru99.learnrecycler

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class MainActivity : AppCompatActivity() {

    var fruitNames:Array<String> = arrayOf<String>("Banana", "Mango", "Passion fruit", "Orange", "Grape")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        var myFruitRecyclerView: RecyclerView = findViewById(R.id.fruitRecyclerView)

        var fruitLinearLayout = LinearLayoutManager(this)

        myFruitRecyclerView.layoutManager =fruitLinearLayout

        var fruitListAdapter = FruitListAdapter(fruitNames)

        myFruitRecyclerView.adapter =fruitListAdapter
    }

    class FruitViewHolder(fruitView: View): RecyclerView.ViewHolder(fruitView)

    class FruitListAdapter(var fruitArray: Array<String>) : RecyclerView.Adapter<FruitViewHolder>() {

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FruitViewHolder {

            var fruitView = LayoutInflater.from(parent.context).inflate(R.layout.one_fruit_view, parent, false)

            var fruitViewHolder = FruitViewHolder(fruitView)

            return fruitViewHolder
        }

        override fun getItemCount(): Int {
            return fruitArray.size
        }

        override fun onBindViewHolder(holder: FruitViewHolder, position: Int) {

            var ourFruitTextView = holder.itemView.findViewById<TextView>(R.id.fruitName)

            var aFruitName = fruitArray.get(position)

            ourFruitTextView.setText(aFruitName)
        }
    }
}

Скачать проект

Часто задаваемые вопросы (FAQ)

RecyclerView — более гибкий и эффективный преемник ListView. Он использует шаблон ViewHolder, поддерживает линейную, сеточную и ступенчатую компоновку с помощью менеджеров компоновки, а также добавляет встроенную анимацию элементов, в то время как ListView по умолчанию ничего из этого не предлагает.

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

Этот пример RecyclerView написан на Kotlin, языке, официально рекомендованном... Google для Android Разработка. Те же API RecyclerView, адаптера и менеджера макета также работают в Java.

Прикрепите обработчик кликов внутри ViewHolder или onBindViewHolder, а затем предоставьте к нему доступ через коллбэк интерфейса для активности или фрагмента. Использование позиционирования ViewHolder позволяет избежать привязки кликов к неправильному переиспользуемому элементу.

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

Jetpack Compose использует LazyColumn и LazyRow вместо RecyclerView для создания новых экранов. Однако RecyclerView по-прежнему широко используется в существующих проектах на основе View и может быть встроен в Compose с помощью API взаимодействия.

Да. Искусственные интеллекты, такие как GitHub Copilot и инструменты на основе GPT, могут сгенерировать шаблон кода для адаптера RecyclerView, ViewHolder и onBindViewHolder с помощью короткой подсказки. Разработчикам все равно следует проверять сгенерированный код на корректность и производительность.

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

Подведем итог этой публикации следующим образом: