Android Academy · Lekcja

PagingSource i Pager

Zdefiniuj sposób wczytywania stron

Lekcja 2 z 413 kroki

PagingSource i Pager to bezpłatna lekcja Android Academy na CoddyKit. To lekcja 2 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Android Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Android Academy zawiera 4 lekcji w sumie.

Definiowanie sposobu ładowania stron

Aby stronicować dane z sieciowego API, należy napisać element PagingSource. Odpowiada on na dwa pytania dotyczące Paging 3:

  • Jak załadować stronę dla danego klucza?
  • Od którego miejsca wznowić działanie po odświeżeniu przez użytkownika?

Następnie należy opakować go w element Pager, który przekształca go w obiekt Flow<PagingData>.

Parametry typów PagingSource

PagingSource<Key, Value> przyjmuje dwa parametry typu:

  • Key - identyfikuje stronę. W przypadku API opartego na numerach stron jest to Int, a w przypadku API opartego na kursorach jest to token typu String.
  • Value - typ elementu, na przykład Article.
import androidx.paging.PagingSource
import androidx.paging.PagingState

class ArticlePagingSource(
    private val api: ArticleApi
) : PagingSource<Int, Article>() {
    // implement load() and getRefreshKey()
}

Implementowanie load()

load() jest funkcją suspend. Otrzymuje params.key (stronę do pobrania) i zwraca obiekt LoadResult.

W przypadku powodzenia należy zwrócić LoadResult.Page zawierający elementy oraz poprzedni i następny klucz. Wartość null klucza oznacza, że w danym kierunku nie ma już strony.

override suspend fun load(
    params: LoadParams<Int>
): LoadResult<Int, Article> {
    val page = params.key ?: 1   // first load has a null key
    return try {
        val response = api.getArticles(page = page, size = params.loadSize)
        LoadResult.Page(
            data = response.items,
            prevKey = if (page == 1) null else page - 1,
            nextKey = if (response.items.isEmpty()) null else page + 1
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

Dlaczego prevKey i nextKey mają znaczenie

Paging używa nextKey, aby ładować kolejne strony podczas przewijania w dół, oraz prevKey, aby ładować poprzednie strony podczas przewijania w górę (co jest przydatne przy rozpoczynaniu w środku listy).

Zwrócenie null jako nextKey informuje Paging, że nie ma więcej stron, więc przestaje wysyłać żądania. Pominięcie tego może powodować nieskończone żądania zwracające puste wyniki.

// Stop forward paging when the server returns an empty page
nextKey = if (response.items.isEmpty()) null else page + 1

// Stop backward paging at the first page
prevKey = if (page == 1) null else page - 1

Implementowanie getRefreshKey()

Podczas odświeżania listy (gestu przeciągnięcia w celu odświeżenia lub unieważnienia) Paging musi wiedzieć, którą stronę załadować ponownie, aby użytkownik pozostał mniej więcej w tym samym miejscu.

getRefreshKey() używa bieżącej wartości anchorPosition - elementu znajdującego się najbliżej obszaru widoku - aby wybrać odpowiedni klucz.

override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
    return state.anchorPosition?.let { anchor ->
        val closestPage = state.closestPageToPosition(anchor)
        closestPage?.prevKey?.plus(1)
            ?: closestPage?.nextKey?.minus(1)
    }
}

Eleganckie obsługiwanie błędów

Wywołanie sieciowe należy umieścić w bloku try/catch, a w przypadku błędu zwrócić LoadResult.Error(e). Paging udostępni ten błąd w interfejsie użytkownika jako LoadState.Error, dzięki czemu można wyświetlić przycisk ponowienia.

Nie wolno dopuścić, aby wyjątek wydostał się z load() - należy go przechwycić i przekształcić w LoadResult.Error.

return try {
    val response = api.getArticles(page = page, size = params.loadSize)
    LoadResult.Page(
        data = response.items,
        prevKey = if (page == 1) null else page - 1,
        nextKey = if (response.items.isEmpty()) null else page + 1
    )
} catch (e: IOException) {        // no network
    LoadResult.Error(e)
} catch (e: HttpException) {       // non-2xx response
    LoadResult.Error(e)
}

Tworzenie elementu Pager

Pager łączy element PagingConfig z fabryką tworzącą nowy element PagingSource. Jego właściwość .flow to obiekt Flow<PagingData>, który zbiera interfejs użytkownika.

Lambda fabryki musi za każdym razem tworzyć nowe źródło, ponieważ Paging unieważnia i odtwarza źródło podczas odświeżania.

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow

class ArticleRepository(private val api: ArticleApi) {
    fun articleStream(): Flow<PagingData<Article>> = Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 5),
        pagingSourceFactory = { ArticlePagingSource(api) }
    ).flow
}

cachedIn w ViewModelach

Zbieranie obiektu PagingData jest operacją jednorazową; ponowne zebranie uruchamia ładowanie od początku. Aby zachować dane po zmianach konfiguracji i umożliwić współdzielenie danych przez wielu odbiorców, należy buforować przepływ w viewModelScope za pomocą cachedIn.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn

class ArticleViewModel(
    repo: ArticleRepository
) : ViewModel() {
    val articles = repo.articleStream()
        .cachedIn(viewModelScope)
}

loadSize a pageSize

Należy pamiętać, że load() odczytuje params.loadSize, a nie bezpośrednio skonfigurowaną wartość pageSize.

Przy pierwszym ładowaniu Paging może zażądać większego początkowego fragmentu (kontrolowanego przez initialLoadSize w PagingConfig, domyślnie równego trzykrotności rozmiaru strony). Należy zawsze przekazywać do API wartość params.loadSize, aby żądanie odpowiadało oczekiwanej przez Paging liczbie elementów.

// Correct: respect the size Paging asks for
val response = api.getArticles(page = page, size = params.loadSize)

// PagingConfig can tune the first load:
PagingConfig(pageSize = 20, initialLoadSize = 40)

API oparte na kursorach

Nie każde API używa numerów stron. Niektóre zwracają kursor lub token wskazujący następną stronę. Schemat jest identyczny - wystarczy zmienić typ Key na String i użyć tokenu z odpowiedzi.

class CursorArticleSource(
    private val api: ArticleApi
) : PagingSource<String, Article>() {
    override suspend fun load(
        params: LoadParams<String>
    ): LoadResult<String, Article> {
        val cursor = params.key   // null on first load
        return try {
            val res = api.getArticles(cursor = cursor, size = params.loadSize)
            LoadResult.Page(
                data = res.items,
                prevKey = null,            // forward-only cursor
                nextKey = res.nextCursor   // null when exhausted
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<String, Article>) = null
}

Łączenie wszystkich elementów

Mają już Państwo kompletną warstwę danych: element PagingSource ładowany stronami, element Pager strumieniujący strony oraz ViewModel buforujący przepływ.

Warstwa interfejsu użytkownika po prostu zbiera viewModel.articles - wyrenderujemy je w następnej lekcji za pomocą Compose LazyColumn.

// Data layer summary
// 1. ArticlePagingSource : PagingSource<Int, Article>
// 2. Pager(config, factory).flow  -> Flow<PagingData<Article>>
// 3. ViewModel: repo.articleStream().cachedIn(viewModelScope)
// UI just collects viewModel.articles

Szybkie sprawdzenie

Co sygnalizuje zwrócenie nextKey = null z funkcji load() w obiekcie PagingSource opartym na numerach stron?

Podsumowanie: PagingSource i Pager

Utworzyli Państwo warstwę danych do stronicowania:

  • PagingSource<Key, Value> implementuje funkcje load() i getRefreshKey()
  • load() zwraca LoadResult.Page z wartościami prevKey/nextKey albo LoadResult.Error
  • Klucz o wartości null zatrzymuje stronicowanie w danym kierunku
  • Pager(config, factory).flow tworzy obiekt Flow<PagingData>
  • cachedIn(viewModelScope) zachowuje dane po zmianach konfiguracji

Następnie: renderowanie tego przepływu na liście Compose.

Bezpłatny start

Ucz się Kotlin dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
36
Lekcje
152

Często zadawane pytania

Czy lekcja „PagingSource i Pager” jest bezpłatna?

Tak — pełny tekst „PagingSource i Pager” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Android Academy, przejdź na CoddyKit PRO. Kurs Android Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „PagingSource i Pager”?

Zdefiniuj sposób wczytywania stron Ćwiczysz Android Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Android Academy?

Nie wymagamy żadnego doświadczenia. Android Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 4.

Ile czasu zajmuje lekcja „PagingSource i Pager”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Android Academy?

Tak. Każda lekcja Android Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Dlaczego warto stosować Paging
  2. PagingSource i Pager
  3. Paging na listach Compose
  4. RemoteMediator i buforowanie
← Powrót do Android Academy