В этом уроке начнем знакомство с Paging Library. Рассмотрим общую схему работы связки PagedList и DataSource.
Полный список уроков курса:
- Урок 1. Lifecycle
- Урок 2. LiveData
- Урок 3. LiveData. Дополнительные возможности
- Урок 4. ViewModel
- Урок 5. Room. Основы
- Урок 6. Room. Entity
- Урок 7. Room. Insert, Update, Delete, Transaction
- Урок 8. Room. Query
- Урок 9. Room. RxJava
- Урок 10. Room. Запрос из нескольких таблиц. Relation
- Урок 11. Room. Type converter
- Урок 12. Room. Миграция версий базы данных
- Урок 13. Room. Тестирование
- Урок 14. Paging Library. Основы
- Урок 15. Paging Library. PagedList и DataSource. Placeholders.
- Урок 16. Paging Library. LivePagedListBuilder. BoundaryCallback.
- Урок 17. Paging Library. Виды DataSource
- Урок 18. Android Data Binding. Основы
- Урок 19. Android Data Binding. Код в layout. Доступ к View
- Урок 20. Android Data Binding. Обработка событий
- Урок 21. Android Data Binding. Observable поля. Двусторонний биндинг.
- Урок 22. Android Data Binding. Adapter. Conversion.
- Урок 23. Android Data Binding. Использование с include, ViewStub и RecyclerView.
- Урок 24. Navigation Architecture Component. Введение
- Урок 25. Navigation. Передача данных. Type-safe аргументы.
- Урок 26. Navigation. Параметры навигации
- Урок 27. Navigation. NavigationUI.
- Урок 28. Navigation. Вложенный граф. Global Action. Deep Link.
- Урок 29. WorkManager. Введение
- Урок 30. WorkManager. Критерии запуска задачи.
- Урок 31. WorkManager. Последовательность выполнения задач.
- Урок 32. WorkManager. Передача и получение данных
- Урок 33. Практика. О чем это будет.
- Урок 34. Практика. TodoApp. Список задач.
- Урок 35. Практика. TodoApp. Просмотр задачи
Paging Library содержит инструменты для постраничной подгрузки данных. Т.е. когда данные подгружаются не все сразу, а по мере прокрутки списка. Давайте сначала рассмотрим в общих чертах, чем этот способ отличается от обычного, а потом выполним несколько примеров.
Для подключения к проекту добавьте в dependencies
// Paging implementation "android.arch.paging:runtime:1.0.0"
Итак, мы хотим отобразить данные в списке. Данные могут быть откуда угодно: база данных, сервер, файл со строками и т.д. Т.е. любой источник, который может предоставить нам данные для отображения их в списке. Для удобства давайте называть его общим словом 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.
Пример:
class EmployeeAdapter extends PagedListAdapter<Employee, EmployeeViewHolder> { protected EmployeeAdapter(DiffUtil.ItemCallback<Employee> diffUtilCallback) { super(diffUtilCallback); } @NonNull @Override public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.employee, parent, false); EmployeeViewHolder holder = new EmployeeViewHolder(view); return holder; } @Override public void onBindViewHolder(@NonNull EmployeeViewHolder holder, int position) { holder.bind(getItem(position)); } }
Как видите, он очень похож на RecyclerView.Adapter. От него также требуется биндить данные в Holder.
Отличия следующие:
1) Ему сразу надо предоставить DiffUtil.Callback. Если вы еще не знакомы с этой штукой, посмотрите мой материал.
2) Нет никакого хранилища данных (List или т.п.)
3) Нет метода getItemCount
Пункты 2 и 3 обусловлены тем, что адаптер внутри себя использует PagedList в качестве источника данных, и он сам будет заниматься хранением данных и определением их количества.
Чтобы передать адаптеру PagedList, мы будем использовать метод адаптера submitList.
PagedList
Если не сильно вдаваться в детали, то PagedList - это обертка над List. Он тоже содержит данные и умеет отдавать их методом get(position). Но при этом он проверяет, насколько запрашиваемый элемент близок к концу имеющихся у него данных и при необходимости подгружает себе новые данные с помощью DataSource.
Т.е. у PagedList в списке уже есть, например, 40 элементов. Адаптер просит у него элемент с позицией 31. PagedList дает ему этот элемент и при этом понимает, что адаптер просил элемент, близкий к концу его данных. А значит есть вероятность, что скоро адаптер придет за элементами с позицией 40 и далее. Поэтому PagedList обращается к DataSource за новой порцией данных, например, от 41 до 50.
Создается PagedList с помощью билдера:
PagedList<Employee> pagedList = new PagedList.Builder<>(dataSource, config) .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) .setMainThreadExecutor(new MainThreadExecutor()) .build();
От нас требуется предоставить пару Executor-ов. Один для выполнения запроса данных в отдельном потоке, а второй для возврата результатов в UI поток.
На вход конструктору билдера необходимо предоставить DataSource и PagedList.Config. Про DataSource мы поговорим чуть позже, а PagedList.Config - это конфиг PagedList. В нем мы можем задать различные параметры, например, размер страницы.
Создание PagedList.Config может выглядеть так:
PagedList.Config config = new PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(10) .build();
Подробно все его параметры мы рассмотрим позже.
Вариант реализации MainThreadExecutor:
class MainThreadExecutor implements Executor { private final Handler mHandler = new Handler(Looper.getMainLooper()); @Override public void execute(Runnable command) { mHandler.post(command); } }
DataSource
DataSource - это посредник между PagedList и Storage. Возникает вопрос: зачем нужен этот посредник? Почему PagedList не может напрямую попросить очередную порцию данных у Storage? Потому что у Storage могут быть разные требования к способу запроса данных.
Например, базе данных мы можем дать позицию и желаемое количество записей, и в ответ получим порцию данных, начиная с указанной позиции. А вот сервер может работать совсем по-другому. Например, он отдает данные постранично и будет ожидать от нас номер следующей страницы, чтобы отдать новую порцию данных. Также у сервера бывает схема, когда с очередной порцией данных он присылает нам токен. Этот токен необходимо использовать для получения следующей порции данных.
Paging Library предоставляет три разных DataSource, которые должны нам помочь связать между собой PagedList и Storage. Это PositionalDataSource, PageKeyedDataSource и ItemKeyedDataSource. В отдельном уроке мы еще подробно рассмотрим, в чем разница между ними. А пока будем работать с PositionalDataSource, т.к. он проще и понятнее остальных.
Практика
Давайте перейдем к практическому примеру и все станет понятнее. В качестве DataSource будем использовать PositionalDataSource.
Итак, чтобы вся схема заработала, нам надо создать DataSource, PagedList и адаптер:
// DataSource MyPositionalDataSource dataSource = new MyPositionalDataSource(new EmployeeStorage()); // PagedList PagedList.Config config = new PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(10) .build(); PagedList<Employee> pagedList = new PagedList.Builder<>(dataSource, config) .setMainThreadExecutor(new MainThreadExecutor()) .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) .build(); // Adapter adapter = new EmployeeAdapter(diffUtilCallback); adapter.submitList(pagedList); // RecyclerView recyclerView.setAdapter(adapter);
DataSource передаем в PagedList. PagedList передаем в адаптер. Адаптер передаем в RecyclerView.
Код MyPositionalDataSource:
class MyPositionalDataSource extends PositionalDataSource<Employee> { private final EmployeeStorage employeeStorage; public MyPositionalDataSource(EmployeeStorage employeeStorage) { this.employeeStorage = employeeStorage; } @Override public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Employee> callback) { Log.d(TAG, "loadInitial, requestedStartPosition = " + params.requestedStartPosition + ", requestedLoadSize = " + params.requestedLoadSize); List<Employee> result = employeeStorage.getData(params.requestedStartPosition, params.requestedLoadSize); callback.onResult(result, 0); } @Override public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Employee> callback) { Log.d(TAG, "loadRange, startPosition = " + params.startPosition + ", loadSize = " + params.loadSize); List<Employee> result = employeeStorage.getData(params.startPosition, params.loadSize); callback.onResult(result); } }
EmployeeStorage - это созданный мною класс, который эмулирует Storage и содержит 100 Employee записей. Не привожу здесь реализацию этого класса, потому что она не имеет значения. В реальном примере вместо него будет база данных или сервер (Retrofit), к которым мы обращаемся за данными.
MyPositionalDataSource наследует PositionalDataSource и должен реализовать пару методов:
1) loadInitial - первоначальная загрузка данных.
Когда мы создаем PagedList, он сразу запрашивает порцию данных у DataSource. Делает он это методом loadInitial. В качестве параметров он передает нам:
requestedStartPosition - с какой позиции подгружать
requestedLoadSize - размер порции
Используя эти параметры, мы запрашиваем данные у Storage. Полученный результат передаем в callback.onResult
2) loadRange - подгрузка новой порции данных
Когда мы прокручиваем список, 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.
Теперь начинаем скроллить список вниз. Когда список показал запись с позицией 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, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня