Библиотека подкачки предоставляет мощные возможности для загрузки и отображения постраничных данных из более крупного набора данных. В этом руководстве показано, как использовать библиотеку подкачки для настройки потока выгружаемых данных из сетевого источника данных и отображения его в RecyclerView
.
Определить источник данных
Первым шагом является определение реализации PagingSource
для идентификации источника данных. Класс API PagingSource
включает метод load()
, который вы переопределяете, чтобы указать, как получить постраничные данные из соответствующего источника данных.
Используйте класс PagingSource
напрямую, чтобы использовать сопрограммы Kotlin для асинхронной загрузки. Библиотека подкачки также предоставляет классы для поддержки других асинхронных фреймворков:
- Чтобы использовать RxJava, вместо этого реализуйте
RxPagingSource
. - Чтобы использовать
ListenableFuture
из Guava, вместо этого реализуйтеListenableFuturePagingSource
.
Выберите типы ключей и значений
PagingSource<Key, Value>
имеет два параметра типа: Key
и Value
. Ключ определяет идентификатор, используемый для загрузки данных, а значение — это тип самих данных. Например, если вы загружаете страницы объектов User
из сети, передавая номера страниц Int
в Retrofit , выберите Int
в качестве типа Key
и User
в качестве типа Value
.
Определите источник подкачки
В следующем примере реализуется PagingSource
, который загружает страницы элементов по номеру страницы. Тип Key
— Int
, а тип Value
— User
.
Котлин
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { try { // Start refresh at page 1 if undefined. val nextPageNumber = params.key ?: 1 val response = backend.searchUsers(query, nextPageNumber) return LoadResult.Page( data = response.users, prevKey = null, // Only paging forward. nextKey = response.nextPageNumber ) } catch (e: Exception) { // Handle errors in this block and return LoadResult.Error for // expected errors (such as a network failure). } } override fun getRefreshKey(state: PagingState<Int, User>): Int? { // 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. return state.anchorPosition?.let { anchorPosition -> val anchorPage = state.closestPageToPosition(anchorPosition) anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) } } }
Ява
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; } }
Ява
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()
, когда данные обновляются или становятся недействительными после первоначальной загрузки. Библиотека подкачки автоматически вызывает этот метод при последующих обновлениях данных.
Обработка ошибок
Запросы на загрузку данных могут завершиться неудачно по ряду причин, особенно при загрузке по сети. Сообщайте об ошибках, обнаруженных во время загрузки, возвращая объект LoadResult.Error
из метода load()
.
Например, вы можете выявлять и сообщать об ошибках загрузки в ExamplePagingSource
из предыдущего примера, добавив в метод load()
следующее:
Котлин
catch (e: IOException) { // IOException for network failures. return LoadResult.Error(e) } catch (e: HttpException) { // HttpException for any non-2xx HTTP status codes. return LoadResult.Error(e) }
Ява
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Ява
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
. Библиотека подкачки поддерживает использование нескольких типов потоков, включая Flow
, LiveData
, а также типы Flowable
и Observable
из RxJava.
Когда вы создаете экземпляр Pager
для настройки реактивного потока, вы должны предоставить экземпляру объект конфигурации PagingConfig
и функцию, которая сообщает Pager
, как получить экземпляр вашей реализации PagingSource
:
Котлин
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Ява
// 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);
Ява
// 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-viewmodel-ktx
.
Объект Pager
вызывает метод load()
из объекта PagingSource
, предоставляя ему объект LoadParams
и получая взамен объект LoadResult
.
Определить адаптер RecyclerView
Вам также необходимо настроить адаптер для получения данных в список RecyclerView
. Для этой цели библиотека Paging предоставляет класс PagingDataAdapter
.
Определите класс, расширяющий PagingDataAdapter
. В этом примере UserAdapter
расширяет PagingDataAdapter
, предоставляя адаптер RecyclerView
для элементов списка типа User
и используя UserViewHolder
в качестве держателя представления :
Котлин
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) } }
Ява
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); } }
Ява
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
:
Котлин
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 } }
Ява
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); } }
Ява
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()
вашего адаптера.
Котлин
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) } }
Ява
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));
Ява
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
теперь отображает постраничные данные из источника данных и при необходимости автоматически загружает другую страницу.
Дополнительные ресурсы
Чтобы узнать больше о библиотеке подкачки, см. следующие дополнительные ресурсы:
Кодлабы
Образцы
- Пример подкачки компонентов архитектуры Android
- Пример подкачки компонентов архитектуры Android с помощью сети
Рекомендуется для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Страница из сети и базы данных
- Перейти на страницу 3
- Обзор библиотеки подкачки