Paging Library. Види DataSource
Якщо адаптер замість реальних даних дає ґ, значить це заглушка і треба відобразити якийсь тимчасовий текст.
Нагадаю, що DataSource - це міст між PagedList і Storage. Де Storage може бути базою даних, сервером або ще якимось джерелом даних, які ви хочете відобразити в списку. І оскільки різні Storage можуть мати різний формат запиту даних, DataSource допомагає привести ці формати в зрозумілі для PagedList методи.
У кожного DataSource є метод для початкового завантаження даних і метод (або методи) для подальшого підвантаження. Первинне завантаження виконується в момент створення PagedList, щоб списку було що показати, коли адаптер отримує PagedList. Далі ми скролимо список, а PagedList довантажує дані, використовуючи для цього методи подальшого довантаження.
На даний момент є три різних DataSource. Загалом вони схожі. Загальні принципи та нюанси їхньої роботи ми вже розглянули дуже докладно в минулих уроках (рекомендую переглянути їх, щоб краще розуміти подальший текст). У цьому уроці я зроблю короткий огляд методів і параметрів, щоб побачити, у чому відмінності.
PositionalDataSource
Презентація з Google IO. Документація
Цей DataSource дає змогу запитувати дані за позицією. Тобто якщо тягнемо дані, наприклад, з БД, то можемо вказати, з якої позиції і скільки даних вантажити. Якщо дані з файлу, то вказуємо з якого рядка і скільки рядків вантажити.
PagedList сам визначає необхідну позицію, з якої треба вантажити чергову порцію даних, і передає її в DataSource.
Порожня реалізація має такий вигляд:
class MyDataSource extends PositionalDataSource<Value> {
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Value> callback) {
}
@Override
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Value> callback) {
}
}
Value — це тип даних, які очікує отримати PagedList
Метод loadInitial — початкове завантаження даних зі Storage
LoadInitialParams містить такі параметри:
int requestedStartPosition— з якої позиції завантажувати.int requestedLoadSize— скільки завантажувати.int pageSize— розмір сторінки (число завантажених даних має бути кратним значеннюpageSize).boolean placeholdersEnabled— чи ввімкнені placeholders.
LoadInitialCallback має два варіанти методу для передачі даних у PagedList:
onResult(List<Value> data, int position)— передаємо дані й указуємо, з якої позиції вони починаються. Якщо список із даними порожній, тоPagedListвважатиме, що даних зовсім немає.onResult(List<Value> data, int position, int totalCount)— те ж саме, що й попередній варіант, але додатково вказуємо загальну кількість записів. Цей варіант використовується, якщо ввімкнені placeholders.
Метод loadRange — подальше підвантаження даних зі Storage
Цей метод викликається під час прокручування списку.
LoadRangeParams містить такі параметри:
int startPosition— з якої позиції завантажувати.int loadSize— скільки завантажувати.
LoadRangeCallback має один варіант методу для передачі даних у PagedList:
onResult(List<T> data)— просто передаємо дані. Якщо список буде порожнім,PagedListвважатиме, що дані закінчилися.
PageKeyedDataSource
Презентація з Google IO. Документація
Цей DataSource підходить для спілкування зі Storage, який разом із черговою порцією даних передає нам якийсь ключ для отримання наступної порції даних.
Це може бути посторінкове завантаження з параметром page. Ми просимо дані, наприклад, зі page = 4. Storage повертає нам їх і повідомляє, що наступну порцію можна отримати, передавши йому page зі значенням 5.
Для числових значень page це виглядає безглуздим, тому що йде просто додавання одиниці. Але ключ може бути і текстовим. Наприклад, так раніше працював API Youtube (не знаю, як зараз). Тобто ми шукаємо відео за якимось пошуковим запитом. Youtube повертає нам першу порцію даних і з ними текстовий токен. У наступному запиті до Youtube ми передаємо цей токен, щоб отримати наступну порцію результатів нашого пошуку. І так далі.
Також сервер може як ключ взагалі передавати готове посилання, яке треба буде використовувати, щоб отримати наступну порцію даних.
Загалом, основний сенс у тому, що наступний запит даних ми зможемо зробити, використавши для цього якийсь ключ, отриманий у попередньому запиті.
Порожня реалізація
class MyDataSource extends PageKeyedDataSource<Key, Value> {
@Override
public void loadInitial(@NonNull LoadInitialParams<Key> params, @NonNull LoadInitialCallback<Key, Value> callback) {
}
@Override
public void loadBefore(@NonNull LoadParams<Key> params, @NonNull LoadCallback<Key, Value> callback) {
}
@Override
public void loadAfter(@NonNull LoadParams<Key> params, @NonNull LoadCallback<Key, Value> callback) {
}
}
Key- це тип ключа (page,токентощо), який використовується для отримання даних.Value- це тип даних, які очікує отриматиPagedList.
Метод loadInitial - початкове завантаження даних.
LoadInitialParams містить такі параметри:
int requestedLoadSize- скільки даних потрібно завантажити.boolean placeholdersEnabled- чи ввімкненоplaceholders.
LoadInitialCallback має два варіанти методу для передачі даних у PagedList:
onResult(List<Value> data, Key previousPageKey, Key nextPageKey)- Передаємо дані та вказуємо ключі для завантаження попередньої та наступної порції даних.
- Якщо замість ключа передати
null,PagedListрозуміє, що даних у цьому напрямку немає. - Зазвичай
previousPageKeyдорівнюєnull, оскільки ми завантажуємо дані з початку, і попередніх порцій немає.
onResult(List<Value> data, int position, int totalCount, Key previousPageKey, Key nextPageKey)- Те саме, що й попередній варіант, але додатково вказуємо загальну кількість записів та позицію, з якої ці дані починаються.
- Цей варіант використовується, якщо ввімкнено
placeholders.
Метод loadBefore - завантаження попередньої порції даних.
- Викликається під час прокрутки списку вгору, якщо ви вказали
previousPageKeyпід час виклику callback уloadInitial. - Зазвичай
previousPageKey = null, тому методloadBeforeчасто не реалізується, оскільки він не викликається.
LoadParams містить такі параметри:
Key key- ключ для отримання даних.int requestedLoadSize- бажаний розмір порції даних. Цей параметр не є обов’язковим, оскільки сервер може мати власне уявлення про розмір порції.
LoadCallback має один варіант методу для передачі даних у PagedList:
onResult(List<Value> data, Key adjacentPageKey)- Передаємо дані та вказуємо, який ключ використовувати для отримання наступної попередньої порції.
- Якщо передати
null,PagedListрозуміє, що у цьому напрямку даних більше немає.
Метод loadAfter - завантаження наступної порції даних.
- Викликається під час прокрутки списку вниз, якщо ви вказали
nextPageKeyпід час виклику callback уloadInitial.
LoadParams містить такі параметри:
Key key- ключ для отримання даних.int requestedLoadSize{lang=java}- бажаний розмір порції даних. Цей параметр не є обов’язковим, оскільки сервер може мати власне уявлення про розмір порції.
LoadCallback має один варіант методу для передачі даних у PagedList:
onResult(List<Value> data, Key adjacentPageKey)- Передаємо дані та вказуємо, який ключ використовувати для отримання наступної порції.
- Якщо передати
null, PagedList розуміє, що у цьому напрямку даних більше немає.
Важливе зауваження!
Можливі варіанти, коли ми можемо обійтися без отримання ключів від сервера. Наприклад, якщо ми запитуємо дані за номером сторінки (page), сервер повертає дані, але не повертає номер наступної сторінки. У такому випадку можна створити лічильник всередині DataSource та змінювати його на одиницю після кожного отримання даних.
Це менш надійно, ніж отримувати ключ із сервера, оскільки через помилку в логіці може виникнути розсинхрон із сервером. Проте, якщо немає можливості вплинути на структуру відповіді сервера, іншого вибору немає.
ItemKeyedDataSource
Презентація з Google IO.
Цей DataSource схожий на щойно розглянутий нами PageKeyedDataSource. Він також використовує попередні отримані від сервера дані для завантаження наступних даних. Але як ключі він використовує не окремі значення типу page або токена, а безпосередньо дані.
Коли ми скролимо список і наближаємося до його кінця, PagedLIst візьме останній елемент списку і дасть його нам. А ми, використовуючи цей елемент, маємо завантажити наступну порцію даних, яка слідує за цим останнім елементом.
Найпоказовіший кейс для цього DataSource - це відсортований список. Наприклад, ми виводимо з БД записи, відсортовані за будь-яким ключем. Під час скролу списку ми в DataSource отримуємо останній елемент списку, і знаючи його ключ, зможемо довантажити наступну за ним порцію даних, використовуючи те ж сортування.
Порожня реалізація
class MyDataSource extends ItemKeyedDataSource<Key, Value> {
@Override
public void loadInitial(@NonNull LoadInitialParams<Key> params, @NonNull LoadInitialCallback<Value> callback) {
}
@Override
public void loadAfter(@NonNull LoadParams<Key> params, @NonNull LoadCallback<Value> callback) {
}
@Override
public void loadBefore(@NonNull LoadParams<Key> params, @NonNull LoadCallback<Value> callback) {
}
@NonNull
@Override
public Key getKey(@NonNull Value item) {
return null;
}
}
- Key - це тип даних, який буде використовуватися в якості ключа.
- Value - це тип даних, які очікує отримати PagedList.
Метод getKey - тут від нас вимагається вказати, як отримати ключ з елемента списку.
Наприклад, якщо у нас тип даних (Value) - це, наприклад, Employee, то тип Key може бути Long, а метод виглядатиме так:
public Long getKey(@NonNull Employee item) {
return item.id;
}
Тепер PagedList знає, як з будь-якого елемента списку отримати ключ, який буде використовуватися для завантаження наступних даних.
Може бути так, що вам у якості ключа потрібен весь елемент. Тоді реалізація виглядатиме так:
public Employee getKey(@NonNull Employee item) {
return item;
}
Тобто типи Key і Value обидва рівні Employee.
Як отримувати Key з Value - залежить від вашого завдання.
Метод loadInitial - початкове завантаження даних.
LoadInitialParams містить такі параметри:
Key requestedInitialKey- ключ для початкового завантаження даних (може бути вказано методомsetInitialKeyуPagedList.Builderабо вLivePagedListBuilder).int requestedLoadSize- скільки даних потрібно завантажити.boolean placeholdersEnabled- чи ввімкнено placeholders.
LoadInitialCallback має два варіанти методу для передачі даних у PagedList:
onResult(@NonNull List<Value> data)- Тільки передаємо дані.
onResult(@NonNull List<Value> data, int position, int totalCount)- Те саме, що й попередній варіант, але додатково вказуємо загальну кількість записів та позицію, з якої ці дані починаються.
- Цей варіант використовується, якщо ввімкнено placeholders.
Метод loadBefore - завантаження попередньої порції даних.
Викликається при прокрутці списку вгору, щоб завантажити попередні записи.
LoadParams містить такі параметри:
Key key- ключ для отримання даних.PagedListбере перший елемент списку, отримує з нього ключ методомgetKeyі дає цей ключ нам.int requestedLoadSize- бажаний розмір порції даних. Цей параметр не є обов'язковим, оскільки сервер може мати власне уявлення про розмір порції.
LoadCallback має один варіант методу для передачі даних у PagedList:
onResult(List<Value> data)- Передаємо дані. Якщо передати порожній список,
PagedListзрозуміє, що в цьому напрямку даних більше немає.
- Передаємо дані. Якщо передати порожній список,
Метод loadAfter - завантаження наступної порції даних.
Викликається при прокрутці списку вниз, щоб завантажити наступні записи.
LoadParams містить такі параметри:
Key key- ключ для отримання даних.PagedListбере останній елемент списку, отримує з нього ключ методомgetKeyі дає цей ключ нам.int requestedLoadSize- бажаний розмір порції даних. Цей параметр не є обов'язковим, оскільки сервер може мати власне уявлення про розмір порції.
LoadCallback має один варіант методу для передачі даних у PagedList:
onResult(List<Value> data)- Передаємо дані. Якщо передати порожній список,
PagedListзрозуміє, що в цьому напрямку даних більше немає.
- Передаємо дані. Якщо передати порожній список,
PagingSource
PagingSource - це джерело даних, яке відповідає за завантаження сторінок даних в обробці пагінації. Він дозволяє завантажувати нові порції даних через методи load(), керуючи процесом їх отримання та кешування. PagingSource використовується для завантаження даних з віддалених джерел або локальних баз даних, де потрібно отримувати сторінки.
Метод load - основний метод, що виконує завантаження даних.
Метод load має параметр LoadParams, який містить необхідну інформацію для отримання сторінки даних:
LoadParams.Key key- ключ для завантаження даних. Зазвичай це сторінка або інший параметр, залежно від специфікацій джерела.int loadSize- кількість елементів, які потрібно завантажити в одну порцію.
Метод load має повернути об'єкт типу LoadResult, який є результатом запиту. Існують три можливі типи результату:
LoadResult.Page- повертає одну сторінку даних.LoadResult.Page<Value> page = new LoadResult.Page<>( data = dataList, prevKey = previousPageKey, nextKey = nextPageKey );data- список завантажених даних.prevKey- ключ для попередньої сторінки, якщо вона є. Якщо немає попередньої сторінки, передаємоnull.nextKey- ключ для наступної сторінки, якщо вона є. Якщо немає наступної сторінки, передаємоnull.
LoadResult.Error- помилка при завантаженні даних.LoadResult.Error<Value> error = new LoadResult.Error<>(exception);- Повертає помилку, яка була викинута під час завантаження.
LoadResult.Invalid- результат, що вказує на невірні параметри для завантаження сторінки.
Ключі завантаження
- Key - це тип даних, що використовуватиметься як ключ для отримання наступної або попередньої сторінки. Зазвичай це сторінка або унікальний ідентифікатор.
- Value - це тип даних, які ви очікуєте отримати після завантаження сторінки.
Параметри LoadParams:
Key key- ключ для отримання даних. Наприклад, це може бути номер сторінки або токен.int loadSize- кількість елементів, які потрібно завантажити в одній порції.
Важливе зауваження:
PagingSource дозволяє отримувати дані і працювати з ними через механізм пагінації. Важливо враховувати правильний вибір ключа, оскільки ключі визначають, як переходити до наступних або попередніх сторінок даних.
Якщо ваш API використовує сторінкові параметри (наприклад, page, limit), ви будете використовувати прості значення для ключів (наприклад, числа для сторінок). У випадку більш складних систем, де відповіді API містять токени або унікальні ідентифікатори для наступних запитів, ваш ключ може бути складнішим (наприклад, рядковий токен).
Підсумок:
PagingSource — це механізм, який дає змогу ефективно працювати з великими наборами даних через пагінацію, дозволяючи контролювати процес завантаження сторінок і обробку ключів для подальших запитів.
Приклад
Припустимо, ми маємо API, яке повертає список користувачів за допомогою пагінації, де кожен запит має параметр page для завантаження наступної порції даних. Ми хочемо створити PagingSource, який буде використовувати цей параметр для завантаження даних.
public class UsersPagingSource extends PagingSource<Integer, User> {
private final ApiService apiService;
public UsersPagingSource(ApiService apiService) {
this.apiService = apiService;
}
@Override
public LoadResult<Integer, User> load(@NonNull LoadParams<Integer> params) {
int page = params.getKey() != null ? params.getKey() : 1; // Починаємо з першої сторінки
try {
// Викликаємо API для завантаження користувачів
Response<UserResponse> response = apiService.getUsers(page, params.getLoadSize()).execute();
if (response.isSuccessful()) {
List<User> users = response.body().getUsers();
// Повертаємо результат як LoadResult.Page
return new LoadResult.Page<>(
users, // Дані користувачів
(page == 1) ? null : page - 1, // Попередній ключ, null на першій сторінці
(users.isEmpty()) ? null : page + 1 // Наступний ключ, null якщо немає більше даних
);
} else {
return new LoadResult.Error<>(new Exception("API error"));
}
} catch (Exception e) {
return new LoadResult.Error<>(e); // Повертаємо помилку
}
}
}
public class UserRepository {
private final ApiService apiService;
public UserRepository(ApiService apiService) {
this.apiService = apiService;
}
public LiveData<PagingData<User>> getUsers() {
return new Pager(
new PagingConfig(20, 10, false), // pageSize = 20, enablePlaceholders = false
() -> new UsersPagingSource(apiService) // Вказуємо наш PagingSource
).getLiveData();
}
}
public class UsersFragment extends Fragment {
private UserRepository userRepository;
private PagingDataAdapter<User, UserViewHolder> adapter;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Ініціалізація адаптера
adapter = new UserAdapter();
// Отримуємо PagingData з репозиторію
userRepository.getUsers().observe(getViewLifecycleOwner(), pagingData -> {
adapter.submitData(getLifecycle(), pagingData);
});
// Підключаємо адаптер до RecyclerView
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
recyclerView.setAdapter(adapter);
}
}