Это продолжение цикла статей про упрощение разработки адаптеров для RecyclerView.
В этой части рассмотрю следующие реализации потребностей отображения списков:
-
Загрузка изображения из сети, с использованием 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 файл:
![](https://habrastorage.org/getpro/habr/upload_files/2b1/270/d01/2b1270d01611be5a3a79d6c6549cd015.png)
Добавил 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 – полностью стоковая.
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/b8f/1ce/845/b8f1ce845c7ae8913a474ddea91acaa2.gif)
Часто списки, возвращаемые бэком огромны, так что нельзя получить все одним запросом. Для этого используется подгрузка списка или другими словами – пагинация. В этой библиотеке – реализация максимально простая.
Реализовал интерфейс 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 окончания загрузки.
Результат:
![](https://habrastorage.org/getpro/habr/upload_files/65b/e7b/128/65be7b12856e7a0752b2d0134c706e94.gif)
Подлагивания связаны с тем, что я ставил 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() } }
![](https://habrastorage.org/getpro/habr/upload_files/dca/907/80c/dca90780c0eb268851f9f43c217fc42b.gif)
Осталось рассмотреть удаление элементов. Удаление я разбил на две части. Первая – это удаление локальных данных, вторая – удаление на бэкенде, когда сообщаем бэку какой элемент удалить и в качестве ответа получаем новый список, без этого элемента.
Реализую локальное удаление, для этого доработаю инициализацию адаптера:
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/articles/576946/
Добавить комментарий