Android recyclerview пагинация kotlin

Pagination in Android with Paging 3, Retrofit and kotlin Flow

Haven’t you asked how does Facebook, Instagram, Twitter, Forbes, etc… let you “scroll infinitely” without reach a “end” of information in their apps? Wouldn’t you like to implement something like that?

This “endless scrolling” feature is commonly called “Pagination” and it’s nothing new. In a brief resume, pagination help you to load chunks of data associated with a “page” index.

Let’s assume you have 200 items to display in your RecyclerView. What you would do normally is just execute your request, get the response items and submit your list to your adapter. We already know that RecyclerView by it’s own is optimized for “recycle our views”. But do we really need to get those 200 items immediately? What if our user never reach the top 100, or top 50? The rest of the non displayed items are still keep on memory.

What pagination does (in conjunction with our API) is that it let us establish a page number, and how many items per page can we load. In that way, we can request for the next page of items only when we reach the bottom of our RecyclerView.

Non library approach

Before paging 3, you could have implemented Pagination by adding some listeners to your RecyclerView and those listeners would be triggered when you reach the bottom of your list. There are some good samples about it, here is a video with a vey detailed explanation (it’s on Indi, but it is understandable anyways).

A real PRODUCTION ready example is on the Plaid app. Look at their InfinteScrollListener class.

Android Jetpack Paging 3 and Flow

Today you gonna learn how to implement Pagination by using paging 3 from the android jetpack libraries. For my surprise the codelab of Paging 3 was one of the most easiest I have ever done. Florina Muntenescu did a great job with each step of the codelab, go check it out and give it a try. If you want to go straight to the sample code, check this pull request and see step by step how I implement paging 3 to this project.

Источник

Реализация списка с заголовком, футером и пагинацией в Андроид

RecyclerView

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

Настройка Gradle

Добавьте следующую зависимость в файл build.gradle:

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

После того, как проект будет синхронизирован, добавьте компонент RecyclerView в ваш макет:

Привязка XML с классом JAVA

Теперь в методе onCreate вашей активности добавьте следующий код:

Прежде чем идти дальше, давайте подробно рассмотрим приведенный выше код

  • Layout Manager — Простыми словами, Layout Manager помогает нам определить структуру нашего RecyclerView. Есть три встроенных Layout Managers. Помимо этого, мы можем создать собственный пользовательский Layout Manager, чтобы удовлетворить наши требования.

  1. LinearLayoutManager показывает элементы в списке с вертикальной или горизонтальной прокруткой.
  2. GridLayoutManager показывает элементы в сетке.
  3. StaggeredGridLayoutManager показывает элементы в шахматной сетке.

RecyclerView ItemDecoration

ItemDecoration позволяет приложению добавлять специальный полосы и смещения к определенным представлениям элементов из набора данных адаптера. Это может быть полезно для рисования разделителей между элементами, выделениями, границами визуальной группировки и т. д. – developer.android.com

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

В вышеприведенном классе мы устанавливаем отступы к нулевому элементу.

RecyclerView Adapter

Теперь давайте настроим адаптер ReeyclerView с заголовком и футером.

Пагинация

Теперь, когда адаптер готов, давайте посмотрим, как добавить пагинацию в список RecyclerView. Это довольно легко сделать и должно быть добавлено в onCreate после установки Adapter to Recycler-View.

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

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

Источник

Урок 14. Paging Library. Основы

В этом уроке начнем знакомство с Paging Library. Рассмотрим общую схему работы связки PagedList и DataSource.

Полный список уроков курса:

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

Для подключения к проекту добавьте в dependencies

Итак, мы хотим отобразить данные в списке. Данные могут быть откуда угодно: база данных, сервер, файл со строками и т.д. Т.е. любой источник, который может предоставить нам данные для отображения их в списке. Для удобства давайте называть его общим словом Storage.

Читайте также:  Андроид пие что это

Обычно мы получаем данные из Storage и помещаем их в List в адаптер. Далее RecyclerView будет у адаптера просить View, а адаптер будет просить данные у List.

Получается такая схема:

RecyclerView >> Adapter >> List

где List сразу содержит все необходимые данные и ничего не надо больше подгружать.

С Paging Library схема будет немного сложнее:

RecyclerView >> PagedListAdapter >> PagedList > DataSource > Storage

Т.е. обычный Adapter мы меняем на PagedListAdapter. А вместо List у нас будет связка PagedList + DataSource, которая умеет по мере необходимости подтягивать данные из Storage.

Рассмотрим подробнее эти компоненты.

PagedListAdapter

PagedListAdapter — это RecyclerView.Adapter, заточенный под чтение данных из PagedList.

Как видите, он очень похож на RecyclerView.Adapter. От него также требуется биндить данные в Holder.

1) Ему сразу надо предоставить DiffUtil.Callback. Если вы еще не знакомы с этой штукой, посмотрите мой материал.

2) Нет никакого хранилища данных (List или т.п.)

3) Нет метода getItemCount

Пункты 2 и 3 обусловлены тем, что адаптер внутри себя использует PagedList в качестве источника данных, и он сам будет заниматься хранением данных и определением их количества.

Чтобы передать адаптеру PagedList, мы будем использовать метод адаптера )» target=»_blank» rel=»noopener noreferrer»>submitList.

PagedList

Если не сильно вдаваться в детали, то PagedList — это обертка над List. Он тоже содержит данные и умеет отдавать их методом get(position). Но при этом он проверяет, насколько запрашиваемый элемент близок к концу имеющихся у него данных и при необходимости подгружает себе новые данные с помощью DataSource.

Т.е. у PagedList в списке уже есть, например, 40 элементов. Адаптер просит у него элемент с позицией 31. PagedList дает ему этот элемент и при этом понимает, что адаптер просил элемент, близкий к концу его данных. А значит есть вероятность, что скоро адаптер придет за элементами с позицией 40 и далее. Поэтому PagedList обращается к DataSource за новой порцией данных, например, от 41 до 50.

Создается PagedList с помощью билдера:

От нас требуется предоставить пару Executor-ов. Один для выполнения запроса данных в отдельном потоке, а второй для возврата результатов в UI поток.

На вход конструктору билдера необходимо предоставить DataSource и PagedList.Config. Про DataSource мы поговорим чуть позже, а PagedList.Config — это конфиг PagedList. В нем мы можем задать различные параметры, например, размер страницы.

Создание PagedList.Config может выглядеть так:

Подробно все его параметры мы рассмотрим позже.

Вариант реализации MainThreadExecutor:

DataSource

DataSource — это посредник между PagedList и Storage. Возникает вопрос: зачем нужен этот посредник? Почему PagedList не может напрямую попросить очередную порцию данных у Storage? Потому что у Storage могут быть разные требования к способу запроса данных.

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

Paging Library предоставляет три разных DataSource, которые должны нам помочь связать между собой PagedList и Storage. Это PositionalDataSource, PageKeyedDataSource и ItemKeyedDataSource. В отдельном уроке мы еще подробно рассмотрим, в чем разница между ними. А пока будем работать с PositionalDataSource, т.к. он проще и понятнее остальных.

Практика

Давайте перейдем к практическому примеру и все станет понятнее. В качестве DataSource будем использовать PositionalDataSource.

Итак, чтобы вся схема заработала, нам надо создать DataSource, PagedList и адаптер:

DataSource передаем в PagedList. PagedList передаем в адаптер. Адаптер передаем в RecyclerView.

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

MyPositionalDataSource наследует PositionalDataSource и должен реализовать пару методов:

Когда мы создаем PagedList, он сразу запрашивает порцию данных у DataSource. Делает он это методом loadInitial. В качестве параметров он передает нам:
requestedStartPosition — с какой позиции подгружать
requestedLoadSize — размер порции

Используя эти параметры, мы запрашиваем данные у Storage. Полученный результат передаем в callback.onResult

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

Используя эти параметры, мы запрашиваем данные у Storage. Полученный результат передаем в callback.onResult

Я добавил логов в эти методы, чтобы было видно, что происходит.

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

Для наглядности я сделал гифку, в которой вы можете видеть, какие логи появляются по мере прокрутки списка.

Разбираемся, что происходит.

Сразу после запуска в логах видим строку:

loadInitial, requestedStartPosition = 0, requestedLoadSize = 30

PagedList запросил первоначальную порцию данных размером 30 элементов (requestedLoadSize), начиная с нулевого (requestedStartPosition). DataSource передает эти параметры в Storage, получает данные и возвращает их в PagedList. В итоге адаптер отображает эти записи.

Откуда взялось число 30? По умолчанию размер первоначальной загрузки равен размер страницы * 3. Размер страницы мы установили равным 10 (в PagedList.Config методом setPageSize), поэтому requestedLoadSize равен 30.

Читайте также:  Как сохранить заметки android

Теперь начинаем скроллить список вниз. Когда список показал запись с позицией 20, PagedList запросил следующую порцию данных:

loadRange, startPosition 30, loadSize = 10

Почему он сделал это именно по достижении записи с позицией 20? За это отвечает параметр prefetchDistance. По умолчанию он равен pageSize, т.е. 10. Соответственно, когда до конца списка остается 10 записей, PagedList подгружает следующую порцию.

По мере прокрутки списка, подгружаются следующие порции данных

loadRange, startPosition = 40, loadSize = 10
loadRange, startPosition = 50, loadSize = 10
loadRange, startPosition = 60, loadSize = 10
loadRange, startPosition = 70, loadSize = 10
loadRange, startPosition = 80, loadSize = 10
loadRange, startPosition = 90, loadSize = 10
loadRange, startPosition = 100, loadSize = 10

После сотой записи список не прокручивается. Так происходит потому, что мой EmployeeStorage содержит всего 100 записей. При попытке получить у него 10 записей, начиная с позиции 100, он просто вернет пустой список. Когда DataSource передаст этот пустой список в callback.onResult, это будет сигналом для PagedList, что данные закончились. После этого PagedList больше не будет пытаться подгружать данные и список не будет скроллиться.

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

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Источник

Кэшируем пагинацию в Android

Наверняка каждый Android разработчик работал со списками, используя RecyclerView. А также многие успели посмотреть как организовать пагинацию в списке, используя Paging Library из Android Architecture Components.

Все просто: устанавливаем PositionalDataSource, задаем конфиги, создаем PagedList и скармливаем все это вместе с адаптером и DiffUtilCallback нашему RecyclerView.

Но что если у нас несколько источников данных? Например, мы хотим иметь кэш в Room и получать данные из сети.

Кейс получается довольно кастомный и в интернете не так уж много информации на эту тему. Я постараюсь это исправить и показать как можно решить такой кейс.

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

Как бы выглядело решение без пагинации:

  • Обращение к кэшу (в нашем случае это БД)
  • Если кэш пуст — отправка запроса на сервер
  • Получаем данные с сервера
  • Отображаем их в листе
  • Пишем в кэш
  • Если кэш имеется — отображаем его в списке
  • Получаем актуальные данные с сервера
  • Отображаем их в списке○
  • Пишем в кэш

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

Алгоритм примерно следующий:

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

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

В Google задумались над этим и создали решение, которое идет из коробки PagingLibrary — BoundaryCallback.

BoundaryCallback сообщает когда локальный источник данных “заканчивается” и уведомляет об этом репозиторий для загрузки новых данных.

На официальном сайте Android Dev есть ссылка на ​репозиторий​ с примером проекта, использующего список с пагинацией с двумя источниками данных: Network (Retrofit 2) + Database (Room). Для того, чтобы лучше понять как работает такая система попробуем разобрать этот пример, немного его упростим.

Начнем со слоя data. Создадим два DataSource.

В этом интерфейсе описаны запросы к API Reddit и классы модели (ListingResponse, ListingData, RedditChildrenResponse), в объекты которых будут сворачиваться ответы API.

И сразу сделаем модель для Retrofit и Room

Класс RedditDb.kt, который будет наследовать RoomDatabase.

Помним, что создавать класс RoomDatabase каждый раз для выполнения запроса к БД очень затратно, поэтому в реальном кейсе создавайте его единожды за все время жизни приложения!

И класс Dao с запросами к БД RedditPostDao.kt

Вы наверное заметили, что метод получения записей postsBySubreddit возвращает
DataSource.Factory. Это необходимо для создания нашего PagedList, используя
LivePagedListBuilder, в фоновом потоке. Подробнее об этом вы можете почитать в
уроке.

Отлично, слой data готов. Переходим к слою бизнес логики.Для реализации паттерна “Репозиторий” принято создавать интерфейс репозитория отдельно от его реализации. Поэтому создадим интерфейс RedditPostRepository.kt

И сразу вопрос — что за Listing? Это дата класс, необходимый для отображения списка.

Создаем реализацию репозитория MainRepository.kt

Давайте посмотрим что происходит в нашем репозитории.

Создаем инстансы наших датасорсов и интерфейсы доступа к данным. Для базы данных:

RoomDatabase и Dao, для сети: Retrofit и интерфейс апи.

Далее реализуем обязательный метод репозитория

который настраивает пагинацию:

  • Создаем SubRedditBoundaryCallback, наследующий PagedList.BoundaryCallback<>
  • Используем конструктор с параметрами и передадим все, что нужно для работы BoundaryCallback
  • Создаем триггер refreshTrigger для уведомления репозитория о необходимости обновить данные
  • Создаем и возвращаем Listing объект
Читайте также:  Голосовые будильники для андроид

В Listing объекте:

  • livePagedList
  • networkState — состояние сети
  • retry — callback для вызова повторного получения данных с сервера
  • refresh — тригер для обновления данных
  • refreshState — состояние процесса обновления

Реализуем вспомогательный метод

для записи ответа сети в БД. Он будет использоваться, когда нужно будет обновить список или записать новую порцию данных.

Реализуем вспомогательный метод

для тригера обновления данных. Тут все довольно просто: получаем данные с сервера, чистим БД, записываем новые данные в БД.

С репозиторием разобрались. Теперь давайте взглянем поближе на SubredditBoundaryCallback.

В классе, который наследует BoundaryCallback есть несколько обязательных методов:

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

Метод вызывается, когда “итератор” дошел до “дна” БД, здесь мы должны выполнить запрос на сервер для получения следующей страницы, передав ключ, с помощью которого сервер выдаст данные, следующие сразу за последней записью локального стора.

Метод вызывается, когда “итератор” дошел до первого элемента нашего стора. Для реализации нашего кейса можем проигнорировать реализацию этого метода.

Дописываем колбэк для получения данных и передачи их дальше

Дописываем метод записи полученных данных в БД

Что за хэлпер PagingRequestHelper? Это ЗДОРОВЕННЫЙ класс, который нам любезно предоставил Google и предлагает вынести его в библиотеку, но мы просто скопируем его в пакет слоя логики.

* The helper provides an API to observe combined request status, which can be reported back to the * application based on your business rules. * */ // THIS class is likely to be moved into the library in a future release. Feel free to copy it // from this sample. public class PagingRequestHelper < private final Object mLock = new Object(); private final Executor mRetryService; @GuardedBy("mLock") private final RequestQueue[] mRequestQueues = new RequestQueue[] ; @NonNull final CopyOnWriteArrayList
mListeners = new CopyOnWriteArrayList<>(); /** * Creates a new com.memebattle.pagingwithrepository.domain.util.PagingRequestHelper with the given <@link Executor>which is used to run * retry actions. * * @param retryService The <@link Executor>that can run the retry actions. */ public PagingRequestHelper(@NonNull Executor retryService) < mRetryService = retryService; >/** * Adds a new listener that will be notified when any request changes <@link Status state>. * * @param listener The listener that will be notified each time a request’s status changes. * @return True if it is added, false otherwise (e.g. it already exists in the list). */ @AnyThread public boolean addListener(@NonNull Listener listener) < return mListeners.add(listener); >/** * Removes the given listener from the listeners list. * * @param listener The listener that will be removed. * @return True if the listener is removed, false otherwise (e.g. it never existed) */ public boolean removeListener(@NonNull Listener listener) < return mListeners.remove(listener); >/** * Runs the given <@link Request>if no other requests in the given request type is already * running. *

* If run, the request will be run in the current thread. * * @param type The type of the request. * @param request The request to run. * @return True if the request is run, false otherwise. */ @SuppressWarnings(«WeakerAccess») @AnyThread public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) < boolean hasListeners = !mListeners.isEmpty(); StatusReport report = null; synchronized (mLock) < RequestQueue queue = mRequestQueues[type.ordinal()]; if (queue.mRunning != null) < return false; >queue.mRunning = request; queue.mStatus = Status.RUNNING; queue.mFailed = null; queue.mLastError = null; if (hasListeners) < report = prepareStatusReportLocked(); >> if (report != null) < dispatchReport(report); >final RequestWrapper wrapper = new RequestWrapper(request, this, type); wrapper.run(); return true; > @GuardedBy(«mLock») private StatusReport prepareStatusReportLocked() < Throwable[] errors = new Throwable[]< mRequestQueues[0].mLastError, mRequestQueues[1].mLastError, mRequestQueues[2].mLastError >; return new StatusReport( getStatusForLocked(RequestType.INITIAL), getStatusForLocked(RequestType.BEFORE), getStatusForLocked(RequestType.AFTER), errors ); > @GuardedBy(«mLock») private Status getStatusForLocked(RequestType type) < return mRequestQueues[type.ordinal()].mStatus; >@AnyThread @VisibleForTesting void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) < StatusReport report = null; final boolean success = throwable == null; boolean hasListeners = !mListeners.isEmpty(); synchronized (mLock) < RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()]; queue.mRunning = null; queue.mLastError = throwable; if (success) < queue.mFailed = null; queue.mStatus = Status.SUCCESS; >else < queue.mFailed = wrapper; queue.mStatus = Status.FAILED; >if (hasListeners) < report = prepareStatusReportLocked(); >> if (report != null) < dispatchReport(report); >> private void dispatchReport(StatusReport report) < for (Listener listener : mListeners) < listener.onStatusChange(report); >> /** * Retries all failed requests. * * @return True if any request is retried, false otherwise. */ public boolean retryAllFailed() < final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length]; boolean retried = false; synchronized (mLock) < for (int i = 0; i

Со слоем бизнес логики закончили, можем переходить к реализации представления.
В слое представления у нас новая MVVM от Google на ViewModel и LiveData.

В методе onCreate инициализируем ViewModel, адаптер списка, подписываемся на изменение названия подписки и вызываем через модель запуск работы репозитория.

Если вы не знакомы с механизмами LiveData и ViewModel, то рекомендую ознакомиться с уроками.

В модели реализуем методы, которые будут дергать методы репозитория: retry и refesh.

Адаптер списка будет наследовать PagedListAdapter. Тут все также как и работе с пагинацией и одним источником данных.

И все те же ViewHolder ы для отображения записи и итема состояния загрузки данных из сети.

Если мы запустим приложение, то можем увидеть прогресс бар, а затем и данные с Reddit по запросу androiddev. Если отключим сеть и долистаем до конца нашего списка, то будет сообщение об ошибке и предложение попытаться загрузить данные снова.

Все работает, супер!

И мой репозиторий, где я постарался немного упростить пример от Google.

На этом все. Если вы знаете другие способы как “закэшировать” пагинацию, то обязательно напишите в комменты.

Источник

Оцените статью