Концепции и реализация Jetpack Compose
Библиотека Paging предоставляет мощные возможности для загрузки и отображения постраничных данных из большого набора данных. В этом руководстве показано, как использовать библиотеку Paging для настройки потока постраничных данных из сетевого источника данных и отображения его в RecyclerView .
Определите источник данных
Первый шаг — определить реализацию класса PagingSource для идентификации источника данных. Класс API PagingSource включает метод load , который вы переопределяете, чтобы указать, как получать постраничные данные из соответствующего источника данных.
Для асинхронной загрузки используйте непосредственно класс PagingSource , чтобы задействовать сопрограммы Kotlin. Библиотека Paging также предоставляет классы для поддержки других асинхронных фреймворков:
- Для использования RxJava следует реализовать
RxPagingSource. - Чтобы использовать
ListenableFutureиз Guava, реализуйте интерфейсListenableFuturePagingSource.
Выберите типы ключей и значений.
PagingSource<Key, Value> имеет два параметра типа: Key и Value . Параметр Key определяет идентификатор, используемый для загрузки данных, а параметр Value — тип самих данных. Например, если вы загружаете страницы объектов User из сети, передавая в Retrofit Int номера страниц, выберите Int в качестве типа Key и User в качестве типа Value .
Определите источник пейджинга
В следующем примере реализован компонент PagingSource , который загружает страницы элементов по номеру страницы. Тип Key — Int , а тип Value — User .
Java (RxJava)
class ExamplePagingSource extends RxPagingSource<Integer, User> {
@NonNull
private ExampleBackendService mBackend;
@NonNull
private String mQuery;
ExamplePagingSource(@NonNull ExampleBackendService backend,
@NonNull String query) {
mBackend = backend;
mQuery = query;
}
@NotNull
@Override
public Single<LoadResult<Integer, User>> loadSingle(
@NotNull LoadParams<Integer> params) {
// Start refresh at page 1 if undefined.
Integer nextPageNumber = params.getKey();
if (nextPageNumber == null) {
nextPageNumber = 1;
}
return mBackend.searchUsers(mQuery, nextPageNumber)
.subscribeOn(Schedulers.io())
.map(this::toLoadResult)
.onErrorReturn(LoadResult.Error::new);
}
private LoadResult<Integer, User> toLoadResult(
@NonNull SearchUserResponse response) {
return new LoadResult.Page<>(
response.getUsers(),
null, // Only paging forward.
response.getNextPageNumber(),
LoadResult.Page.COUNT_UNDEFINED,
LoadResult.Page.COUNT_UNDEFINED);
}
@Nullable
@Override
public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
// Try to find the page key of the closest page to anchorPosition from
// either the prevKey or the nextKey; you need to handle nullability
// here.
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey are null -> anchorPage is the
// initial page, so return null.
Integer anchorPosition = state.getAnchorPosition();
if (anchorPosition == null) {
return null;
}
LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
if (anchorPage == null) {
return null;
}
Integer prevKey = anchorPage.getPrevKey();
if (prevKey != null) {
return prevKey + 1;
}
Integer nextKey = anchorPage.getNextKey();
if (nextKey != null) {
return nextKey - 1;
}
return null;
}
}
Java (Guava/LiveData)
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> {
@NonNull
private ExampleBackendService mBackend;
@NonNull
private String mQuery;
@NonNull
private Executor mBgExecutor;
ExamplePagingSource(
@NonNull ExampleBackendService backend,
@NonNull String query, @NonNull Executor bgExecutor) {
mBackend = backend;
mQuery = query;
mBgExecutor = bgExecutor;
}
@NotNull
@Override
public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) {
// Start refresh at page 1 if undefined.
Integer nextPageNumber = params.getKey();
if (nextPageNumber == null) {
nextPageNumber = 1;
}
ListenableFuture<LoadResult<Integer, User>> pageFuture =
Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber),
this::toLoadResult, mBgExecutor);
ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture =
Futures.catching(pageFuture, HttpException.class,
LoadResult.Error::new, mBgExecutor);
return Futures.catching(partialLoadResultFuture,
IOException.class, LoadResult.Error::new, mBgExecutor);
}
private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) {
return new LoadResult.Page<>(response.getUsers(),
null, // Only paging forward.
response.getNextPageNumber(),
LoadResult.Page.COUNT_UNDEFINED,
LoadResult.Page.COUNT_UNDEFINED);
}
@Nullable
@Override
public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
// Try to find the page key of the closest page to anchorPosition from
// either the prevKey or the nextKey; you need to handle nullability
// here.
// * prevKey == null -> anchorPage is the first page.
// * nextKey == null -> anchorPage is the last page.
// * both prevKey and nextKey are null -> anchorPage is the
// initial page, so return null.
Integer anchorPosition = state.getAnchorPosition();
if (anchorPosition == null) {
return null;
}
LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
if (anchorPage == null) {
return null;
}
Integer prevKey = anchorPage.getPrevKey();
if (prevKey != null) {
return prevKey + 1;
}
Integer nextKey = anchorPage.getNextKey();
if (nextKey != null) {
return nextKey - 1;
}
return null;
}
}
Типичная реализация PagingSource передает параметры, указанные в конструкторе, методу load для загрузки соответствующих данных для запроса. В приведенном выше примере этими параметрами являются:
-
backend: экземпляр серверной службы, предоставляющей данные. -
query: поисковый запрос, который необходимо отправить в указанныйbackendсервис.
Объект LoadParams содержит информацию о выполняемой операции загрузки. Сюда входят загружаемый ключ и количество загружаемых элементов.
Объект LoadResult содержит результат операции загрузки. LoadResult — это закрытый класс, который принимает одну из двух форм в зависимости от того, был ли вызов load успешным:
- Если загрузка прошла успешно, верните объект
LoadResult.Page. - Если загрузка не удалась, верните объект
LoadResult.Error.
На следующем рисунке показано, как функция load в этом примере получает ключ для каждой загрузки и предоставляет ключ для последующей загрузки.
load используется и обновляется ключ. Реализация PagingSource также должна реализовывать метод getRefreshKey , который принимает в качестве параметра объект PagingState . Он возвращает ключ, который передается в метод load при обновлении или аннулировании данных после первоначальной загрузки. Библиотека Paging Library автоматически вызывает этот метод при последующих обновлениях данных.
Обработка ошибок
Запросы на загрузку данных могут завершаться неудачей по ряду причин, особенно при загрузке по сети. Сообщайте об ошибках, возникших во время загрузки, возвращая объект LoadResult.Error из метода load .
Например, вы можете перехватывать и сообщать об ошибках загрузки в ExamplePagingSource из предыдущего примера, добавив следующее в метод load :
Java (RxJava)
return backend.searchUsers(searchTerm, nextPageNumber)
.subscribeOn(Schedulers.io())
.map(this::toLoadResult)
.onErrorReturn(LoadResult.Error::new);
Java (Guava/LiveData)
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(
backend.searchUsers(query, nextPageNumber), this::toLoadResult,
bgExecutor);
ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(
pageFuture, HttpException.class, LoadResult.Error::new,
bgExecutor);
return Futures.catching(partialLoadResultFuture,
IOException.class, LoadResult.Error::new, bgExecutor);
Для получения дополнительной информации об обработке ошибок Retrofit см. примеры в справочнике API PagingSource .
PagingSource собирает и передает объекты LoadResult.Error в пользовательский интерфейс, чтобы вы могли с ними взаимодействовать. Дополнительную информацию о отображении состояния загрузки в пользовательском интерфейсе см. в разделе «Управление и отображение состояний загрузки» .
Настройте поток PagingData.
Далее вам потребуется поток постраничных данных из реализации PagingSource . Настройте поток данных в вашем ViewModel . Класс Pager предоставляет методы, которые предоставляют реактивный поток объектов PagingData из PagingSource . Библиотека Paging поддерживает использование нескольких типов потоков, включая Flow, LiveData , а также типы Flowable и Observable из RxJava.
При создании экземпляра Pager для настройки реактивного потока необходимо передать экземпляру объект конфигурации PagingConfig и функцию, которая указывает Pager , как получить экземпляр вашей реализации PagingSource :
Java (RxJava)
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
new PagingConfig(/* pageSize = */ 20),
() -> ExamplePagingSource(backend, query));
Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager);
PagingRx.cachedIn(flowable, viewModelScope);
Java (Guava/LiveData)
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
new PagingConfig(/* pageSize = */ 20),
() -> ExamplePagingSource(backend, query));
PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
Оператор cachedIn делает поток данных доступным для совместного использования и кэширует загруженные данные с помощью предоставленного CoroutineScope . В этом примере используется viewModelScope предоставляемый артефактом lifecycle lifecycle-viewmodel-ktx .
Объект Pager вызывает метод load из объекта PagingSource , передавая ему объект LoadParams и получая в ответ объект LoadResult .
Определите адаптер RecyclerView
Вам также потребуется настроить адаптер для приема данных в список RecyclerView . Библиотека Paging предоставляет для этой цели класс PagingDataAdapter .
Определите класс, который наследует PagingDataAdapter . В примере UserAdapter наследует PagingDataAdapter , чтобы предоставить адаптер RecyclerView для элементов списка типа User , используя UserViewHolder в качестве держателя представления :
Kotlin (корутины)
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) :
PagingDataAdapter<User, UserViewHolder>(diffCallback) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): UserViewHolder {
return UserViewHolder(parent)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val item = getItem(position)
// Note that item can be null. ViewHolder must support binding a
// null item as a placeholder.
holder.bind(item)
}
}
Java (RxJava)
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
super(diffCallback);
}
@NonNull
@Override
public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new UserViewHolder(parent);
}
@Override
public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
User item = getItem(position);
// Note that item can be null. ViewHolder must support binding a
// null item as a placeholder.
holder.bind(item);
}
}
Java (Guava/LiveData)
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
super(diffCallback);
}
@NonNull
@Override
public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new UserViewHolder(parent);
}
@Override
public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
User item = getItem(position);
// Note that item can be null. ViewHolder must support binding a
// null item as a placeholder.
holder.bind(item);
}
}
Ваш адаптер также должен определить методы onCreateViewHolder и onBindViewHolder и указать DiffUtil.ItemCallback . Это работает так же, как и обычно при определении адаптеров для списков RecyclerView :
Kotlin (корутины)
object UserComparator : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
// Id is unique.
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
}
Java (RxJava)
class UserComparator extends DiffUtil.ItemCallback<User> {
@Override
public boolean areItemsTheSame(@NonNull User oldItem,
@NonNull User newItem) {
// Id is unique.
return oldItem.id.equals(newItem.id);
}
@Override
public boolean areContentsTheSame(@NonNull User oldItem,
@NonNull User newItem) {
return oldItem.equals(newItem);
}
}
Java (Guava/LiveData)
class UserComparator extends DiffUtil.ItemCallback<User> {
@Override
public boolean areItemsTheSame(@NonNull User oldItem,
@NonNull User newItem) {
// Id is unique.
return oldItem.id.equals(newItem.id);
}
@Override
public boolean areContentsTheSame(@NonNull User oldItem,
@NonNull User newItem) {
return oldItem.equals(newItem);
}
}
Отобразите постраничные данные в пользовательском интерфейсе.
Теперь, когда вы определили PagingSource , создали способ генерации потока PagingData для вашего приложения и определили PagingDataAdapter , вы готовы соединить эти элементы и отображать постраничные данные в вашей активности.
Выполните следующие действия в onCreate вашей активности или в методе onViewCreated фрагмента:
- Создайте экземпляр класса
PagingDataAdapter. - Передайте экземпляр
PagingDataAdapterв списокRecyclerView, в котором вы хотите отображать постраничные данные. - Отслеживайте поток
PagingDataи передавайте каждое сгенерированное значение в методsubmitData()вашего адаптера.
Kotlin (корутины)
val viewModel by viewModels<ExampleViewModel>()
val pagingAdapter = UserAdapter(UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter
// Activities can use lifecycleScope directly; fragments use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
viewModel.flow.collectLatest { pagingData ->
pagingAdapter.submitData(pagingData)
}
}
Java (RxJava)
ExampleViewModel viewModel = new ViewModelProvider(this)
.get(ExampleViewModel.class);
UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
R.id.recycler_view);
recyclerView.adapter = pagingAdapter
viewModel.flowable
// Using AutoDispose to handle subscription lifecycle.
// See: https://github.com/uber/AutoDispose.
.to(autoDisposable(AndroidLifecycleScopeProvider.from(this)))
.subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java (Guava/LiveData)
ExampleViewModel viewModel = new ViewModelProvider(this)
.get(ExampleViewModel.class);
UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
R.id.recycler_view);
recyclerView.adapter = pagingAdapter
// Activities can use getLifecycle() directly; fragments use
// getViewLifecycleOwner().getLifecycle().
viewModel.liveData.observe(this, pagingData ->
pagingAdapter.submitData(getLifecycle(), pagingData));
Теперь список RecyclerView отображает постраничные данные из источника данных и автоматически загружает следующую страницу при необходимости.