Упрощаю разработку адаптеров для RecyclerView с BRVAH. Часть 2

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Это продолжение цикла статей про упрощение разработки адаптеров для RecyclerView.

Часть 1

В этой части рассмотрю следующие реализации потребностей отображения списков:

  • Загрузка изображения из сети, с использованием Glide

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

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

  • Удаление и использование встроенного diffUtils

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

Доработал проект, для демонстрации этой задачи. Создал новые activity, dataClass, layout и адаптер. Все по аналогии с прошлым примером, покажу изменения.

DataClass:

data class NotificationWithImageDTO(
    val date: String,
    val text: String,
    var isRead: Boolean = false,
    val imageUrl: String
)

Поле imageUrl хранит в себе ссылку на изображение.

Создал новый layout файл:

Добавил ImageView, для вывода изображения

Создал новый адаптер:

class NotificationWithImageAdapter(data: MutableList<NotificationWithImageDTO>) :
    BaseQuickAdapter<NotificationWithImageDTO, BaseViewHolder>(R.layout.item_notification_with_image, data) {

    init {
        addChildClickViewIds(R.id.ivState)
    }

    override fun convert(holder: BaseViewHolder, item: NotificationWithImageDTO) {
        holder.setGone(R.id.view, holder.layoutPosition == 0)
            .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
            )

        val imageView = holder.getView<ImageView>(R.id.imageView)
        val context = holder.itemView.context

        Glide.with(context)
            .load(item.imageUrl)
            .circleCrop()
            .into(imageView)
    }
}

К функционалу прошлого адаптера добавил получение конкретной view и context, для загрузи изображения через Glide. Для получения конкретной View использую метод холдера getView и типизирую его нужным мне типом. Для получения context, получаю ItemView, это из базовой реализации RecyclerView и из него получаю context. Работа Glide – полностью стоковая.

Результат:

Часто списки, возвращаемые бэком огромны, так что нельзя получить все одним запросом. Для этого используется подгрузка списка или другими словами – пагинация. В этой библиотеке – реализация максимально простая.

Реализовал интерфейс LoadMoreModule в адаптере, методов для переопределения нет.

class NotificationWithImageAdapter(data: MutableList<NotificationWithImageDTO>) :
    BaseQuickAdapter<NotificationWithImageDTO, BaseViewHolder>(R.layout.item_notification_with_image, data),
    LoadMoreModule

При подгрузке списка возможно несколько состояний:

  • Идет подгрузка

  • Подгружено успешно

  • Ошибка подгрузки

  • Подгружены все данные

Для отображения этих состояний в библиотеке предусмотрен абтрактный класс BaseLoadMoreView. Создал свой LoadMoreView являющийся его наследником:

class LoadMoreView : BaseLoadMoreView() {
    override fun getRootView(parent: ViewGroup): View = LayoutInflater.from(parent.context)
        .inflate(R.layout.view_load_more, parent, false)

    override fun getLoadingView(holder: BaseViewHolder): View = holder.getView(R.id.load_more_loading_view)

    override fun getLoadComplete(holder: BaseViewHolder): View = holder.getView(R.id.load_more_load_end_view)

    override fun getLoadEndView(holder: BaseViewHolder): View = holder.getView(R.id.load_more_load_end_view)

    override fun getLoadFailView(holder: BaseViewHolder): View = holder.getView(R.id.load_more_load_fail_view)

В нем описываются layout-файл и методы получения view для каждого состояния

Создал layout:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/dp_40"
    xmlns:tools="http://schemas.android.com/tools">

    <LinearLayout
        android:id="@+id/load_more_loading_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="horizontal">

        <ProgressBar
            android:id="@+id/loading_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            style="?android:attr/progressBarStyleSmall"/>
    </LinearLayout>

    <FrameLayout
        android:id="@+id/load_more_load_fail_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:visibility="visible"
        android:visibility="gone">

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:src="@drawable/ic_refresh"/>

    </FrameLayout>

    <FrameLayout
        android:id="@+id/load_more_load_end_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="Load all data"
            android:textColor="@android:color/darker_gray"/>
    </FrameLayout>
</FrameLayout>

Доработал инициализацию адаптера для возможности подгрузки списка:

private val customLoadMoreView = LoadMoreView()

…


private fun initAdapter() {
    rv.adapter = adapter
    adapter.loadMoreModule.loadMoreView = customLoadMoreView
    adapter.loadMoreModule.setOnLoadMoreListener { loadMore() }
    adapter.loadMoreModule.isAutoLoadMore = true
    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.nextPage()
    adapter.setNewInstance(data)
}

private fun loadMore() {
    val data = repository.nextPage()
    adapter.addData(data)
    adapter.loadMoreModule.isEnableLoadMore = true
    adapter.loadMoreModule.loadMoreComplete()
    if (repository.isEnd()) {
        adapter.loadMoreModule.loadMoreEnd()
    }
}

При инициализации добавилась настройка loadMoreModule.

  • Метод loadMoreView устанавливает view описанную выше

  • isAutoLoadMore – определяет можно ли автоматически подгружать список, если нельзя – то подргузку необходимо будет запускать руками методом loadMoreToLoading()

  • setLoadMoreListener – устанавливает метод вызываемый на событие подгрузки, в моем случае это мой метод loadMore()

В методе loadMore() происходит запрос данных, для следующей страницы. После их получения, добавляю эти данные в адаптер методом addData. После добавления данных, разрешаю подгрузку данных снова, и выставляю статус loadMoreComplete. Этот статус скрывает view загрузки. Далее запрашиваю у репозитория была ли эта страница последней, если это была последняя – то выставляю статус loadMoreEnd, этот статус отображает view окончания загрузки.

Результат:

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

Иногда при подгрузке данных может возникнуть ошибка. Смоделирую ситуацию, чтоб метод репозитория случайным образом мог вкидывать exception. Получение данных оберну в блок try catch. В случае exception укажу, что произошла ошибка при подгрузке, методом adapter.loadMoreModule.loadMoreFail(). В этом случае отобразится errorView в нижней части RecyclerView. При клике на нее запустится метод подгрузки данных.

private fun loadMore() {
    try {
        val data = repository.nextPage()
        adapter.addData(data)
        adapter.loadMoreModule.isEnableLoadMore = true
        adapter.loadMoreModule.loadMoreComplete()
        if (repository.isEnd()) {
            adapter.loadMoreModule.loadMoreEnd()
        }
    } catch (e: Exception) {
        adapter.loadMoreModule.loadMoreFail()
    }
}

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

Реализую локальное удаление, для этого доработаю инициализацию адаптера:

private fun initAdapter() {
        rv.adapter = adapter
        adapter.loadMoreModule.loadMoreView = customLoadMoreView
        adapter.loadMoreModule.setOnLoadMoreListener { loadMore() }
        adapter.loadMoreModule.isAutoLoadMore = true
        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 {
                    deleteLocalItem(position) 
                }
            }
        }
        val data = repository.firstPage()
        adapter.setNewInstance(data)
    }


private fun deleteLocalItem(position: Int){
    val item = adapter.getItem(position)
    adapter.remove(item)
}

Написал метод для локального удаления элемента. Для этого получаю элемент по его позиции в адаптере, и вызываю метод remove(item) у адаптера.

В завершение рассмотрю вариант удаления через бэкенд, когда возвращается новый список. Какие подводные камни тут есть? Вижу, как минимум один, но прямо очень серьезный. Если пользователь проскроллил список вниз, удалил элемент, то при установке нового списка в RecyclerView позиция собьётся, а скролл перенесется в начало списка. Как один из вариантов решения этой проблемы – это использование DiffUtils. В BRVAH он уже интегрирован.

Описание diffCallback отношения к библиотеке не имеет, поэтому показывать его не буду. Дальше этот callback установлю для адаптера, с помощью метода adapter.setDiffCallback(NotificationDiffCallback()). Далее при установке данных в адаптер необходимо использовать метод adapter.setDiffNewData(data).

private fun initAdapter() {
        rv.adapter = adapter
        adapter.loadMoreModule.loadMoreView = customLoadMoreView
        adapter.loadMoreModule.setOnLoadMoreListener { loadMore() }
        adapter.loadMoreModule.isAutoLoadMore = true
        adapter.setDiffCallback(NotificationDiffCallback())
        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 {
//                    deleteLocalItem(position)
                    deleteRemoteItem(position)
                }
            }
        }
        val data = repository.nextPage()
        adapter.setNewInstance(data)
    }

    private fun deleteRemoteItem(position: Int) {
        val data = repository.deleteImagedItem(position)
        adapter.setDiffNewData(data)
    }

Поведение UI не поменялось, результат выполнения показывать смысла нет.

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

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

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

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

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

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

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

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

Библиотека

Источник: https://habr.com/ru/post/576946/


Интересные статьи

Интересные статьи

Воспоминания "бумера" о пути в карьеру: начиная с перфокарт и странных калькуляторов, через PDP-11 и VAX к "лихим 90м". Первая часть, школа. Читать далее ...
Вопрос в заголовке включает в себя неочевидную часть, ведь перед тем, как рассказывать про создание хорошей интеграции стоит определить, какую интеграцию мы считаем хорошей. А ответ на эт...
Всем привет! Не так давно на работе в рамках тестирования нового бизнес-процесса мне понадобилась возможность авторизации под разными пользователями. Переход в соответствующий р...
В обновлении «Сидней» Битрикс выпустил новый продукт в составе Битрикс24: магазины. Теперь в любом портале можно создать не только лендинг или многостраничный сайт, но даже интернет-магазин. С корзино...
Итак, мы рассмотрели, как настроить сеть на компьютере. Что же касается свитча, напомню, что он является устройством 2 уровня модели OSI, а мы должны сконфигурировать IP-адрес, который относится ...