В этом уроке рассмотрим, какие параметры мы можем задать для PagedList. Подробно разберем, какие значения необходимо передавать в callback.onResult в DataSource. Научимся использовать режим Placeholders.
Полный список уроков курса:
- Урок 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. Напомню основные моменты кода:
Адаптер:
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)); } }
DataSource:
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 - это просто эмуляция какого-то внешнего источника данных, который содеержит 100 записей
Код в MainActivity, где мы все это собираем вместе:
// 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);
Мы уже знаем, как в целом работает схема. Теперь будем разбираться с нюансами.
Давайте рассмотрим, какие параметры и настройки PagedList нам доступны
Initial Key
В нашем примере PagedList запрашивает первоначальную порцию данных с позиции 0. Но вполне может быть ситуация, когда необходимо запросить данные не с самого начала.
Например, в приложении открыт список и уже прокручен до какой-то позиции. Пользователь сворачивает приложение и занимается другими делами. Системе вдруг не хватает памяти, и она убивает приложение. Пользователь решает вернуться, открывает приложение и вот тут нам надо показать список на том же месте, где он был. Перед тем как приложение было убито, мы в onSaveInstanceState сохранили текущую позицию списка и теперь хотим показать данные в нем с этой же позиции. PagedList дает нам такую возможность.
В билдере PagedList есть параметр initialKey. Для примера зададим ему значение 50.
PagedList<Employee> pagedList = new PagedList.Builder<>(dataSource, config) .setMainThreadExecutor(new MainThreadExecutor()) .setBackgroundThreadExecutor(Executors.newSingleThreadExecutor()) .setInitialKey(50) .build();
А в методе loadInitial в MyPositionalDataSource надо будет немного подправить параметры вызова callback.onResult.
@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, params.requestedStartPosition); }
При запросе данных параметр params.requestedStartPosition будет равен 50, т.к. мы задали это в setInitialKey.
В callback.onResult мы возвращаем не только данные из Storage, но и позицию начального элемента этих данных. Т.е. мы сообщаем PagedList, что данные, которые мы ему передали, начинаются с позиции params.requestedStartPosition, т.е. 50.
Понимаю, что это выглядит как минимум странно. PagedList вроде как сам попросил нас предоставить ему данные с позиции 50. Т.е. он знает об этом. Зачем мы ему об этом дополнительно сообщаем в callback.onResult? Потому что он ждет от нас это значение. Даже если оно будет равно тому значению, которое он нам сам и прислал в params.requestedStartPosition. В дальнейшей работе он будет использовать именно то значение, которые мы вернули ему.
В простейших случаях два этих значения будут равны. И код в примере выше будет работать. Но в сложных случаях эти значения могут отличаться. Т.е. PagedList просил нас данные с позиции 50, а мы смогли предоставить только с позиции 40. Давайте рассмотрим такой нетривиальный сценарий.
Вспоминаем вышеописанный случай, когда приложение убивается системой, пока оно висит в фоне. Допустим, что перед тем, как быть убитым, оно подгрузило все 100 записей. Список был промотан до конца и показывал последние 10 записей: c 90 по 100. Значит при восстановлении приложения PagedList попросит у нас записи, начиная с позиции 90.
Небольшое отступление. PagedList попросит 30 записей, начиная с 90. Почему 30, ведь всего было 100 записей, и он должен попросить 10? Потому что он ничего не знает про то, сколько у нас данных в Storage. А 30 - это размер его порции начальных данных по умолчанию. И если в Storage есть только 10 записей, начиная с 90, то мы просто их и возвращаем в PagedList. Запрашиваемая и получаемая начальные порции данных необязательно должны совпадать по размеру.
Давайте представим, что, пока приложение было убито, количество данных в Storage уменьшилось до 70. А PagedList просит с 90. Наш Storage должен уметь обрабатывать такие ситуации. Т.е. когда мы у него в первоначальной загрузке попросим записи, начиная с 90, он должен понять, что таких записей больше нет, и вернуть ближайшие доступные записи.
Например, если в Storage всего 70 записей, и мы запрашиваем 30 записей, начиная с 90, то Storage может вернуть нам 30 последних записей, т.е. 30 записей с позиции 40. Мы передаем эти данные в callback.onResult, и обязательно указываем, что их позиция начинается с 40, а не с 90, как рассчитывал PagedList.
В итоге получается, что нас просили о данных с позиции 90 (params.requestedStartPosition), а в callback.onResult мы передаем список и сообщаем, что получилось добыть данные только с позиции 40. Таким образом мы скорректировали начальную позицию PagedList. Он знает, что у него есть записи с позиции 40 по позицию 70. И это позволит ему запрашивать у DataSource остальные данные, используя правильные позиции.
Если же мы в callback.onResult просто передадим params.requestedStartPosition (т.е. 90), то PagedList будет думать, что он получил начальные данные с позиции 90 по позицию 120. И дальше он будет запрашивать данные с 120 по 130, с 80 по 90 и т.д. Т.е. получится рассинхрон со Storage. Поэтому нам необходимо вернуть в PagedList корректное значение начальной позиции полученных данных. А чтобы мы могли это сделать, нам надо получить это значение от Storage.
Для этого я немного переписал 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); EmployeeData result = employeeStorage.getInitialData(params.requestedStartPosition, params.requestedLoadSize); callback.onResult(result.data, result.position); } @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); } }
В методе loadInitial я для получения данных использую employeeStorage.getInitialData. Этот метод вернет данные и позицию, с которой эти данные начинаются. Если вдруг запрашиваемых данных нет, то он сможет вернуть ближайшие данные. Это важно, потому что мы не можем передать пустой список в callback.onResult в методе loadInitial. Если мы в callback.onResult вернем пустой список и позицию 90, тем самым сообщая, что нам не удалось получить эти данные, то PagedList сообщит нам, что это наша проблема:
Initial result cannot be empty if items are present in data set
Поэтому нам надо обязательно получить от Storage какие-то начальные данные. В нашем примере я от employeeStorage.getInitialData получу 30 записей и позицию 40 и передам это в PagedList.
Разумеется, может возникнуть ситуация, что данных в Storage нет совсем. Даже для первоначальной загрузки. Тогда мы передаем в callback.onResult пустой список и позицию 0. Только с позицией 0 PagedList примет от нас пустой список в методе loadInitial и поймет, что данных вообще никаких нет.
В методе loadRange мы для получения данных продолжаем использовать метод employeeStorage.getData. Он не должен ничего учитывать и определять. От него требуется просто вернуть данные, если они есть. А если их нет, то это будет сигналом для PagedList, что данные закончились.
Давайте посмотрим, как это выглядит в работе
PagedList просит 30 записей, начиная с 90.
loadInitial, requestedStartPosition = 90, requestedLoadSize = 30
DataSource возвращает ему 30 записей, начиная с 40, и список их отображает.
Сразу после первой загрузки PagedList просит у DataSource новую порцию данных: 10 штук, начиная с позиции 30.
loadRange, startPosition = 30, loadSize = 10
Т.е. он понял, что в начале списка должны быть еще данные и подгрузил одну порцию. Причем, если бы мы не скорректировали его позицию на 40 (в callback.onResult), то он считал бы, что отображает записи с 90 по 120, и запросил бы 10 записей с позиции 80.
Последуюшие скроллы вверх, будут также подгружать предыдущие записи, пока не дойдем до нуля.
loadRange, startPosition = 20, loadSize = 10
loadRange, startPosition = 10, loadSize = 10
loadRange, startPosition = 0, loadSize = 10
При скролле в конец списка, PagedList попытается подгрузить следующие записи после 70.
loadRange, startPosition = 70, loadSize = 10
Но в Storage больше ничего нет. PagedList получит пустой список (в callback в методе loadRange) и успокоится.
pageSize
Параметр pageSize позволяет задать размер страницы
PagedList.Config config = new PagedList.Config.Builder() .setEnablePlaceholders(false) .setPageSize(15) .build();
Если мы зададим ему значение 15, то PagedList будет просить у DataSource по 15 записей.
loadInitial, requestedStartPosition = 0, requestedLoadSize = 45
loadRange, startPosition = 45, loadSize = 15
loadRange, startPosition = 60, loadSize = 15
loadRange, startPosition = 75, loadSize = 15
loadRange, startPosition = 90, loadSize = 15
loadRange, startPosition = 100, loadSize = 15
Параметр requestedLoadSize также изменился, он равен размер страницы * 3, т.е. 45
initialLoadSizeHint
Этот параметр отвечает за количество данных, которое будет запрашивать PagedList при первоначальной загрузке. По умолчанию он равен pageSize * 3, но мы можем задать свое значение.
PagedList.Config config = configBuilder .setEnablePlaceholders(false) .setPageSize(10) .setInitialLoadSizeHint(50) .build();
Размер страницы задаем 10, а initialLoadSizeHint в 50.
Логи:
loadInitial, requestedStartPosition = 0, requestedLoadSize = 50
loadRange, startPosition = 50, loadSize = 10
loadRange, startPosition = 60, loadSize = 10
loadRange, startPosition = 70, loadSize = 10
…
При первой загрузке PagedList получил 50 записей. И дальше подгружает порциями по 10.
prefetchDistance
PagedList использует этот параметр, чтобы определить, когда надо подгружать следующую порцию данных. По умолчанию этот параметр равен pageSize. Можно задать свое значение.
PagedList.Config config = configBuilder .setEnablePlaceholders(false) .setPageSize(10) .setPrefetchDistance(20) .build();
Теперь PagedList будет подгружать новые данные, когда при прокрутке остается 20 записей до конца списка.
Placeholders
Презентация с Google IO.
PagedList обычно не знает сколько всего будет данных. Он по мере прокрутки списка подгружает данные и добавляет их в список. Он будет так делать, пока в Storage не закончатся данные.
Но есть и другой режим. Когда PagedList выполняет первоначальную загрузку данных, мы можем сразу сообщить ему, что у нас ожидается, например, 100 записей. PagedList сообщит об этом адаптеру, и список сразу покажет 100 записей. Реальными из них будут только те, которые были получены при первоначальной загрузке. Остальные будут фейковыми, вместо реальных данных там будут null-заглушки.
Т.е. для таких записей метод адаптера getItem(position) будет возвращать null. Соответственно, нам надо будет научить Holder адекватно реагировать на null данные, которые мы ему передаем, и отображать какую-то визуальную заглушку, показывающую пользователю, что данные пока не доступны.
Далее, по мере прокрутки списка, PagedList будет подгружать реальные данные и отображать их вместо заглушек.
Я сделал пример с размером страницы = 5
Список содержит 100 строк. Именно это значение я передал в PagedList во время загрузки первоначальной загрузки данных. Видно, что весь список кроме начальных данных заполнен заглушками. По мере прокрутки списка выполняется подгрузка данных и заглушки заменяются полученными реальными данными. Новых записей в список добавляться уже не будет.
Реализовать это несложно.
В конфиге PagedList надо включить placeholders.
PagedList.Config config = configBuilder .setEnablePlaceholders(true) .setPageSize(5) .build();
Код метода loadInitial надо будет немного переписать.
@Override public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Employee> callback) { Log.d(TAG, "loadInitial, requestedStartPosition = " + params.requestedStartPosition + ", requestedLoadSize = " + params.requestedLoadSize); EmployeeData result = employeeStorage.getInitialData(params.requestedStartPosition, params.requestedLoadSize); if (params.placeholdersEnabled) { callback.onResult(result.data, result.position, result.count); } else { callback.onResult(result.data, result.position); } }
EmployeeStorage должен уметь возвращать нам количество записей, которые он содержит. Он может возвращать это значение вместе с данными в методе getInitialData. Либо можно получать это значение вызовом отдельного метода. Тут все зависит от реализации Storage.
Полученное значение нам следует передать в качестве третьего параметра в callback.onResult. Тем самым PagedList будет знать, сколько записей надо отобразить в списке.
Обратите внимание, в коде я делаю проверку параметра placeholdersEnabled. Если он включен, то я передаю количество записей в колбэк, иначе - не передаю. Если у вас будет включен параметр placeholdersEnabled и вы не передадите количество записей, то будет ошибка:
Placeholders requested, but totalCount not provided. Please call the three-parameter onResult method, or disable placeholders in the PagedList.Config
PagedList не сможет заполнить список заглушками, потому что он не знает, сколько записей в нем будет.
Ну и изменения в холдере, чтобы обеспечить поддержку заглушек
public void bind(Employee employee) { if (employee == null) { textViewName.setText(R.string.wait); textViewSalary.setText(R.string.wait); } else { textViewName.setText(employee.name); textViewSalary.setText(employee.salary); } }
Если адаптер вместо реальных данных дает null, значит это заглушка и надо отобразить какой-то временный текст.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня