Biblioteka stronicowania daje zaawansowane możliwości wczytywania i wyświetlania danych z podziałem na strony z większego zbioru danych. W tym przewodniku pokazujemy, jak za pomocą biblioteki stronicowania skonfigurować strumień danych z podziałem na strony ze źródła danych sieciowych i wyświetlać je w elemencie RecyclerView
.
Zdefiniuj źródło danych
Pierwszym krokiem jest zdefiniowanie implementacji PagingSource
umożliwiającej identyfikację źródła danych. Klasa interfejsu API PagingSource
zawiera metodę load()
, którą zastępujesz, aby wskazać sposób pobierania danych z podziałem na strony z odpowiedniego źródła danych.
Aby używać współprogramów Kotlin do ładowania asynchronicznego, użyj bezpośrednio klasy PagingSource
. Biblioteka stronicowania udostępnia również klasy obsługujące inne platformy asynchroniczne:
- Aby użyć kodu RxJava, zastosuj kod
RxPagingSource
. - Aby użyć polecenia
ListenableFuture
z Gujawy, zaimplementujListenableFuturePagingSource
.
Wybierz typy klucza i wartości
Funkcja PagingSource<Key, Value>
ma 2 parametry: Key
i Value
. Klucz określa identyfikator używany do wczytywania danych, a wartością jest typ samych danych. Jeśli na przykład wczytujesz z sieci strony obiektów User
, przekazując ich numery Int
do Retrofit, wybierz Int
jako typ Key
i typ User
jako typ Value
.
Zdefiniuj stronę PagingSource
W poniższym przykładzie zaimplementowano komponent PagingSource
, który wczytuje strony elementów według numeru strony. Typ Key
to Int
, a typ Value
to User
.
Kotlin
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) } } }
Java
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
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 te parametry to:
backend
: instancja usługi backendu, która dostarcza dane.query
: zapytanie, 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 zabezpieczona klasa, która ma jedną z 2 form w zależności od tego, czy wywołanie load()
zakończyło się powodzeniem:
- Jeśli wczytanie się udało, zwróć obiekt
LoadResult.Page
. - Jeśli wczytywanie się nie udało, zwróć obiekt
LoadResult.Error
.
Na rysunku poniżej widać, jak funkcja load()
w tym przykładzie otrzymuje klucz przy każdym obciążeniu i dostarcza klucz do kolejnego wczytywania.
Implementacja PagingSource
musi też implementować metodę getRefreshKey()
, która jako parametr przyjmuje obiekt PagingState
. Zwraca klucz, aby przekazać do metody load()
, gdy dane zostaną odświeżone lub unieważnione po wstępnym wczytaniu. Biblioteka stronicowania wywołuje tę metodę automatycznie przy kolejnych odświeżeniach danych.
Obsługa błędów
Żądania wczytania danych mogą z różnych powodów kończyć się niepowodzeniem, zwłaszcza podczas wczytywania danych przez sieć. Raportuj błędy napotkane podczas wczytywania przez zwrócenie obiektu LoadResult.Error
z metody load()
.
Możesz na przykład wychwycić i zgłosić błędy wczytywania w tabeli ExamplePagingSource
z poprzedniego przykładu, dodając do metody load()
ten kod:
Kotlin
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) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
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 dokumentacji interfejsu API PagingSource
.
PagingSource
zbiera i dostarcza obiekty LoadResult.Error
do interfejsu, aby umożliwić Ci podjęcie działań na ich podstawie. Więcej informacji o przedstawianiu stanu wczytywania w interfejsie znajdziesz w artykule Zarządzanie stanami wczytywania i ich prezentowanie.
Skonfiguruj strumień danych PagingData
Następnie potrzebujesz strumienia danych z podziałem na strony z implementacji PagingSource
.
Skonfiguruj strumień danych w urządzeniu ViewModel
. Klasa Pager
udostępnia metody, które udostępniają reaktywny strumień obiektów PagingData
z PagingSource
. Biblioteka stronicowania obsługuje kilka typów strumieni, w tym Flow
, LiveData
oraz Flowable
i Observable
z RxJava.
Gdy tworzysz instancję Pager
, aby skonfigurować strumień reaktywny, musisz udostępnić w niej obiekt konfiguracji PagingConfig
oraz funkcję, która poinformuje Pager
, jak pobrać instancję swojej implementacji PagingSource
:
Kotlin
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)
Java
// 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
// 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()
umożliwia udostępnianie strumienia danych i zapisuje wczytane dane w pamięci podręcznej z podanym parametrem CoroutineScope
. W tym przykładzie korzystamy z viewModelScope
dostarczonego przez artefakt lifecycle-viewmodel-ktx
cyklu życia.
Obiekt Pager
wywołuje metodę load()
z obiektu PagingSource
, dostarczając go z obiektem LoadParams
, a następnie odbierając obiekt LoadResult
.
Definiowanie adaptera RecyclerView
Musisz też skonfigurować adapter, aby przekazywać dane na listę RecyclerView
. Do tego celu służy biblioteka stronicowania, która udostępnia klasę PagingDataAdapter
.
Zdefiniuj klasę rozszerzającą zakres PagingDataAdapter
. W tym przykładzie UserAdapter
rozszerza zakres PagingDataAdapter
, aby udostępnić adapter RecyclerView
dla elementów listy typu User
i używać UserViewHolder
jako właściciela widoku:
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
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
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ć właściwość DiffUtil.ItemCallback
.
Działa to tak samo jak w przypadku definiowania adapterów list 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
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
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 z podziałem na strony w interfejsie
Po zdefiniowaniu obiektu PagingSource
, utworzeniu dla aplikacji sposobu generowania strumienia PagingData
i zdefiniowaniu PagingDataAdapter
możesz teraz połączyć te elementy i wyświetlać w swojej aktywności dane z podziałem na strony.
Wykonaj te czynności w metodzie onCreate
swojej aktywności lub jej fragmentu onViewCreated
:
- Utwórz instancję klasy
PagingDataAdapter
. - Przekaż wystąpienie
PagingDataAdapter
do listyRecyclerView
, w której chcesz wyświetlać dane z podziałem na strony. - Obserwuj strumień
PagingData
i przekaż każdą wygenerowaną wartość do metodysubmitData()
adaptera.
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
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
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 dane stronowane ze źródła danych i w razie potrzeby automatycznie wczytuje kolejną stronę.
Dodatkowe materiały
Więcej informacji o bibliotece stronicowania znajdziesz w tych dodatkowych materiałach:
Ćwiczenia z programowania
Próbki
- Przykład strony w sekcji Architektura Android
- Strony z komponentami architektury Androida z przykładem sieci
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy JavaScript jest wyłączony
- Strona z sieci i bazy danych
- Migrate to Paging 3 (Migracja do strony 3)
- Omówienie biblioteki stronicowania