Wczytywanie i wyświetlanie danych z podziałem na strony

Biblioteka Paging zapewnia zaawansowane funkcje wczytywania i wyświetlania danych podzielonych na strony z większego zbioru danych. Z tego przewodnika dowiesz się, jak używać biblioteki Paging do konfigurowania strumienia danych podzielonych na strony ze źródła danych w sieci i wyświetlania ich na liście leniwej.

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.

Wybieranie typów kluczy i wartości

PagingSource<Key, Value> ma 2 parametry typu: KeyValue. 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 KeyUser 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.

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    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 = nextPageNumber + 1
      )
    } 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)
    }
  }
}

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 przez backend.

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 zamknięta, która przyjmuje jedną z 3 form w zależności od tego, czy wywołanie load się powiodło:

  • Jeśli wczytywanie się powiedzie, zwróć obiekt LoadResult.Page.
  • Jeśli wczytywanie się nie powiedzie, zwróć obiekt LoadResult.Error.
  • Jeśli PagingSource nie jest już ważny i należy go zastąpić nową instancją (np. z powodu zmiany danych źródłowych), zwróć obiekt PagingSource.LoadResult.Invalid

Ilustracja poniżej pokazuje, jak funkcja load w tym przykładzie odbiera klucz dla każdego wczytania i udostępnia klucz dla kolejnego wczytania.

Przy każdym wywołaniu funkcji load klasa ExamplePagingSource pobiera bieżący klucz i zwraca następny klucz do wczytania.
Rysunek 1. Diagram pokazujący, jak load używa klucza i go aktualizuje.

Implementacja PagingSource musi też implementować metodę getRefreshKey, która przyjmuje obiekt PagingState jako parametr. Zwraca klucz, który należy przekazać do metody load, gdy dane są odświeżane lub unieważniane po początkowym wczytaniu. Biblioteka Paging automatycznie wywołuje tę metodę 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:

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)
}

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 PagingDataPagingSource. Biblioteka Paging udostępnia strumień danych jako Flow.

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, jak pokazano w tym przykładzie.

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        // Configure how data is loaded by passing additional properties to
        // PagingConfig, such as pageSize and enabling or disabling placeholders.
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = true
        ),
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

Operator cachedIn udostępnia strumień danych i buforuje załadowane dane za pomocą podanego CoroutineScope. Bez cachedIn nie można ponownie zebrać PagingData. W tym przykładzie używamy viewModelScope dostarczonego przez artefakt lifecycle-viewmodel-ktx cyklu życia.

Obiekt Pager wywołuje metodę load z obiektu PagingSource, przekazując mu obiekt LoadParams i otrzymując w zamian obiekt LoadResult.

Zbieranie i wyświetlanie danych w interfejsie

Aby połączyć strumień podzielony na strony z interfejsem, pobierz przepływ z ViewModel i przekaż go do funkcji kompozycyjnej listy.

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

Użyj collectAsLazyPagingItems, aby przekształcić PagingDataLazyPagingItems. Następnie użyj interfejsu items API w ramach elementu LazyColumn, aby rozmieścić poszczególne elementy.

Pamiętaj, aby podać unikalny, stały identyfikator każdego produktu za pomocą atrybutu itemKey. W poniższym przykładzie użyto it.id (odwołującego się do właściwości User.id), ponieważ pozostaje on stabilny w przypadku instancji User w trakcie aktualizacji danych.

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

Biblioteka Paging używa wartości null jako obiektów zastępczych podczas wczytywania strony, więc jeśli masz włączone obiekty zastępcze, musisz obsługiwać wartości null w bloku treści.

Lista wyświetla teraz dane podzielone na strony, a biblioteka Paging wczytuje dodatkowe strony podczas przewijania przez użytkownika.

Dodatkowe materiały

Więcej informacji o bibliotece Paging znajdziesz w tych materiałach:

Dokumentacja

Wyświetlanie treści