Я действующий разработчик приложений под платформу Android. Хочу поделиться крутой библиотекой, облегчающей разработку адаптеров для RecyclerView, и описать ее использование. RecyclerView – это View элемент в Android для отображения списков, и редкое современное приложение обходится без него. Стоковая реализация адаптеров и вьюхолдеров очень громоздкая и пугающая, особенно для новичков. Благо существует библиотека BaseRecyclerViewAdapterHelper облегчающая разработку этих компонентов. В 100% проектов, которые я разрабатывал – я подключал её, и все коллеги достойно оценивали это деяние.

Цель BaseRecyclerViewAdapterHelper – упростить работу с отображением списков в Android. Чтобы понять, как можно облегчить работу с RecyclerView, рассмотрим базовые потребности отображения списков и базовые потребности элементов списка

Базовые потребности отображения списков в андроид:

  • Отобразить список

  • Иметь возможность взаимодействия с элементами списка

Базовые потребности элемента списка:

  • Отображать текст

  • Отображать изображения

  • Возможность менять свое состояние

За исполнение потребностей элемента – отвечает ViewHolder. В данной библиотеке есть реализация BaseViewHolder, им по умолчанию типизирован BaseAdapter. Это позволяет не описывать свой ViewHolder, а пользоваться базовыми методами:

  • Установка текста из данных

  • Установка видимости View

  • Установка изображения из ресурса

  • И много других, не рассмотренных в данной статье

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

Первым делом создам DataClass, который описывает элемент списка с точки зрения данных:

data class NotificationDTO(
    val date: String,
    val text: String,
    var isRead: Boolean = false
)
  • date - хранит в себе дату уведомления в строковом виде

  • text - текст уведомления

  • isRead – статус уведомления, то есть было ли оно прочитано или нет.

Далее создам layout для элемента списка:

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

Далее создам адаптер для отображения данных:

class NotificationAdapter(data: MutableList<NotificationDTO>) :
    BaseQuickAdapter<NotificationDTO, BaseViewHolder>(R.layout.item_notification, data) {

    init {
        addChildClickViewIds(R.id.ivState)
    }

    override fun convert(holder: BaseViewHolder, item: NotificationDTO) {
        holder.setGone(R.id.view, holder.layoutPosition == 0)

        holder.setText(R.id.tvDateTime, item.date)
            .setText(R.id.tvDsc, item.text)
            .setImageResource(
                R.id.ivState,
                if (item.isRead) R.drawable.ic_delete
                else R.drawable.ic_read
            )
    }
}

Рассмотрим этот адаптер. Он наследуется от BaseQuickAdapter и типизируется двумя параметрами. Первый – это элемент, который нужно отобразить и BaseViewHolder. В конструктор адаптера передается список элементов для отображения, а конструктор BaseQuickAdapter передается id layout элемента списка.

При инициализации адаптера, в методе init, указываю id элементов, для которых нам нужны слушатели нажатий, в нашем случае это только ImageView, нажатиями будем менять статус элемента.  Заполнение элемента данными происходит в методе convert. В нем есть доступ к ViewHolder и элементу списка. Методы, используемые для отображения данных:

  • setGone – устанавливает видимость элемента. В данном примере есть разделитель. Его необходимо делать видимым для всех элементов, кроме нулевого. Для получения порядкового номера элемента использую метод holder.layoutPosition . В метод передаю id элемента и Boolean значение, скрывать или отображать элемент

  • setText – устанавливает текст на TextView и его наследников. Принимает в себя id элемента и необходимый текст. В данном случае, получаем его из объекта данных

  • setImageResource – устанавливает изображение из ресурсов в ImageView

Чтобы отобразить данные – проинициализирую адаптер в activity, свяжу его с RecyclerView и заполню данными:

class MainActivity : AppCompatActivity() {
    private lateinit var rv: RecyclerView
    private val adapter = NotificationAdapter(mutableListOf())
    private val repository = Repository()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        rv = findViewById(R.id.rvNotification)
        rv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        initAdapter()
    }

    private fun initAdapter() {
        rv.adapter = adapter
        val data = repository.getAll()
        adapter.setNewInstance(data)
    }
}

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

В результате отобразил весь список, пока без обработки нажатий. Добавлю обработчик.

class MainActivity : AppCompatActivity() {
    private lateinit var rv: RecyclerView
    private val adapter = NotificationAdapter(mutableListOf())
    private val repository = Repository()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        rv = findViewById(R.id.rvNotification)
        rv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
        initAdapter()
    }

    private fun initAdapter() {
        rv.adapter = adapter
        adapter.setOnItemChildClickListener { _, view, position ->
            if (view.id == R.id.ivState) {
                val item = adapter.getItem(position)
                if (!item.isRead) {
                    item.isRead = true
                    adapter.notifyItemChanged(position)
                } else {
                    Toast.makeText(this, "Элемент будет удален, реализация в следющей части", Toast.LENGTH_SHORT).show()
                }
            }
        }
        val data = repository.getAll()
        adapter.setNewInstance(data)
    }
}

В initAdapter, установил слушатель нажатий методом setOnItemChildClickListener. ВАЖНО у этого адаптера есть похожий метод setOnItemClickListener, но его принципиальное отличие в том, что он обрабатывает нажатия на элемент в целом, и не будет обрабатывать нажатия на «дочерние» элементы. В слушателе, проверяю на какой элемент было нажатие, в текущем примере один такой элемент, но их может быть сколько угодно. Далее проверяю прочитано это уведомление или нет. Для получения этой информации, в слушатель передается параметр «position», да получения элемента по его позиции, использую метод адаптера getItem(position). Если элемент не прочитан, то меняю ему статус и сообщаю адаптеру о том, что элемент был изменен методом notifyItemChanged(position)

Результат:

Получился читабельный адаптер в тридцать строк, покрывающий более 90% потребностей в отображении списков на Android. Другие возможности библиотеки буду рассмотрены в следующих частях это:

  • Установка изображение в список из сети

  • Анимация появления элементов

  • Подгрузка списка(пагинация)

  • Отображение загрузки списка и ошибки загрузки списка

  • Удаление элементов

  • Обработка «долгих» нажатий

  • Удаление элемента «свайпом»

  • Перемещение элементов

  • Использование нескольких layout в одном списке

Возможно, придумаю еще интересную тему и добавлю ее в список

Проект на Гите

Библиотека

Комментарии (5)


  1. elzahaggard13
    05.09.2021 10:16
    +1

    Очень интересный подход. Есть у меня вопросы по читаемости методов setGone/text/resource, но на вкус и цвет...


  1. AlexSkvortsov
    06.09.2021 12:55
    +1

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

    Советую изучить хотя бы наиболее известный вариант из таких

    https://github.com/sockeqwe/AdapterDelegates

    П.с. а еще библиотека из поста кажись заброшена


    1. bsod_keks Автор
      06.09.2021 15:53

      Спасибо, будем посмотреть


    1. bsod_keks Автор
      06.09.2021 15:57

      P.S била из статьи была обновлена 6мес назад, а Ваша 17мес назад. так что по поводу заброшенности не уверен


      1. AlexSkvortsov
        06.09.2021 17:03
        +1

        Я главным образом обращал внимание на issues и pull requests, их очень много у либы из статьи.
        Ну и аккаунт сам по себе активный у адаптер-делегатов, там почти каждый месяц куда-нибудь контрибутится что-то, а у либы из статьи - ни разу за год.
        В любом случае, спасибо за статью)