Pojęcia i implementacja w Jetpack Compose
Biblioteka Paging zapewnia zaawansowane funkcje wczytywania i wyświetlania danych podzielonych na strony z większego zbioru danych. Z tego przewodnika dowiesz się, jak za pomocą biblioteki Paging skonfigurować strumień danych podzielonych na strony z sieciowego źródła danych i wyświetlić go w RecyclerView.
Określanie źródła danych
Pierwszym krokiem jest zdefiniowanie implementacji PagingSource, aby zidentyfikować źródło danych. Klasa interfejsu API PagingSource zawiera metodę load, którą możesz zastąpić, aby określić sposób pobierania danych podzielonych na strony z odpowiedniego źródła danych.
Użyj bezpośrednio klasy PagingSource, aby używać współprogramów Kotlin do asynchronicznego wczytywania. Biblioteka Paging udostępnia też klasy obsługujące inne platformy asynchroniczne:
- Aby używać RxJava, zaimplementuj interfejs
RxPagingSource. - Aby użyć
ListenableFuturez biblioteki Guava, zaimplementujListenableFuturePagingSource.
Wybieranie typów kluczy i wartości
PagingSource<Key, Value> ma 2 parametry typu: Key i Value. Klucz określa identyfikator używany do wczytywania danych, a wartość to typ samych danych. Jeśli np. wczytujesz strony User obiektów z sieci, przekazując Int numery stron do Retrofit, wybierz Int jako typ Key i User jako typ Value.
Zdefiniuj PagingSource
W przykładzie poniżej zaimplementowano PagingSource, który wczytuje strony z elementami według numeru strony. Typ Key to Int, a typ Value to 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;
}
}
Typowa implementacja PagingSource przekazuje parametry podane w konstruktorze do metody load, aby wczytać odpowiednie dane dla zapytania. W przykładzie powyżej są to te parametry:
backend: instancja usługi backendu, która udostępnia dane.query: zapytanie wyszukiwania, które ma zostać wysłane do usługi wskazanej przezbackend.
Obiekt LoadParams zawiera informacje o operacji wczytywania, która ma zostać wykonana. Obejmuje to klucz do wczytania i liczbę elementów do wczytania.
Obiekt LoadResult zawiera wynik operacji wczytywania.
LoadResult to klasa zapieczętowana, która przyjmuje jedną z 2 form w zależności od tego, czy wywołanie load zakończyło się powodzeniem:
- Jeśli wczytywanie się powiedzie, zwróć obiekt
LoadResult.Page. - Jeśli wczytywanie się nie powiedzie, zwróć obiekt
LoadResult.Error.
Ilustracja poniżej pokazuje, jak funkcja load w tym przykładzie odbiera klucz dla każdego wczytania i udostępnia klucz dla kolejnego wczytania.
load używa klucza i go aktualizuje.
Implementacja PagingSource musi też implementować metodę getRefreshKey, która przyjmuje obiekt PagingState jako parametr. Zwraca klucz do przekazania do metody load, gdy dane są odświeżane lub unieważniane po początkowym wczytaniu. Biblioteka Paging wywołuje tę metodę automatycznie podczas kolejnych odświeżeń danych.
Obsługuj błędy
Żądania wczytania danych mogą się nie powieść z różnych powodów, zwłaszcza podczas wczytywania przez sieć. Zgłaszaj błędy napotkane podczas wczytywania, zwracając obiekt LoadResult.Error z metody load.
Na przykład błędy wczytywania w ExamplePagingSource z poprzedniego przykładu możesz przechwycić i zgłosić, dodając do metody load ten kod:
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);
Więcej informacji o obsłudze błędów Retrofit znajdziesz w przykładach w PagingSourcedokumentacji interfejsu API.
PagingSource zbiera i przesyła LoadResult.Error obiekty do interfejsu, aby umożliwić Ci podjęcie działań. Więcej informacji o wyświetlaniu stanu wczytywania w interfejsie znajdziesz w artykule Zarządzanie stanami wczytywania i ich prezentowanie.
Konfigurowanie strumienia PagingData
Następnie potrzebujesz strumienia danych podzielonych na strony z implementacji PagingSource.
Skonfiguruj strumień danych w usłudze ViewModel. Klasa Pager udostępnia metody, które udostępniają reaktywny strumień obiektów PagingData z PagingSource. Biblioteka Paging obsługuje kilka typów strumieni, w tym Flow, LiveData oraz typy Flowable i Observable z RxJava.
Podczas tworzenia instancji Pager w celu skonfigurowania strumienia reaktywnego musisz podać instancji obiekt konfiguracji PagingConfig i funkcję, która informuje Pager, jak uzyskać instancję implementacji 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);
Operator cachedIn udostępnia strumień danych i buforuje załadowane dane za pomocą podanego CoroutineScope. W tym przykładzie użyto artefaktu viewModelScope
dostarczonego przez cykl życia lifecycle-viewmodel-ktx.
Obiekt Pager wywołuje metodę load z obiektu PagingSource, przekazując mu obiekt LoadParams i otrzymując w zamian obiekt LoadResult.
Określanie adaptera RecyclerView
Musisz też skonfigurować adapter, aby otrzymywać dane na listę RecyclerView. Biblioteka Paging udostępnia w tym celu klasę PagingDataAdapter.
Określ klasę rozszerzającą PagingDataAdapter. W tym przykładzie UserAdapter
extends PagingDataAdapter udostępnia RecyclerView adapter dla elementów listy
typu User i używa UserViewHolder jako uchwytu widoku:
Kotlin (współprogramy)
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);
}
}
Adapter musi też definiować metody onCreateViewHolder i onBindViewHolder oraz określać DiffUtil.ItemCallback. Działa to tak samo jak zwykle w przypadku definiowania adapterów listy RecyclerView:
Kotlin (współprogramy)
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);
}
}
Wyświetlanie danych podzielonych na strony w interfejsie
Po zdefiniowaniu PagingSource, utworzeniu sposobu generowania strumienia PagingData przez aplikację i zdefiniowaniu PagingDataAdapter możesz połączyć te elementy i wyświetlić w aktywności dane podzielone na strony.
Wykonaj te czynności w metodzie onCreate aktywności lub fragmentu:onViewCreated
- Utwórz instancję klasy
PagingDataAdapter. - Przekaż instancję
PagingDataAdapterdo listyRecyclerView, na której chcesz wyświetlać dane podzielone na strony. - Obserwuj strumień
PagingDatai przekazuj każdą wygenerowaną wartość do metodysubmitData()adaptera.
Kotlin (współprogramy)
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));
Lista RecyclerView wyświetla teraz podzielone na strony dane ze źródła danych i w razie potrzeby automatycznie wczytuje kolejną stronę.