Android Academy · Урок

PagingSource и Pager

Определяйте способ загрузки страниц

Урок 2 из 413 шагов

«PagingSource и Pager» — бесплатный урок Android Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Android Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Android Academy содержит 4 уроков всего.

Определение способа загрузки страниц

Чтобы загружать данные постранично из сетевого API, создайте PagingSource. Он отвечает на два вопроса для Paging 3:

  • как загрузить страницу с заданным ключом;
  • откуда продолжить загрузку, если пользователь обновит список.

Затем оберните его в Pager, который преобразует его в Flow<PagingData>.

Параметры типов PagingSource

PagingSource<Key, Value> принимает два параметра типа:

  • Key — идентифицирует страницу. Для API со страницами, обозначенными номерами, это Int, а для API с курсорами — токен типа String.
  • Value — тип элемента, например Article.
import androidx.paging.PagingSource
import androidx.paging.PagingState

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

Реализация load()

load() — это функция suspend. Она получает params.key (страницу для загрузки) и возвращает LoadResult.

При успешной загрузке верните LoadResult.Page с элементами, а также предыдущим и следующим ключами. Значение null для ключа означает, что в соответствующем направлении страниц больше нет.

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

Зачем нужны prevKey и nextKey

Paging использует nextKey, чтобы загружать данные дальше по мере прокрутки вниз, и prevKey, чтобы загружать предыдущие страницы — это полезно, если загрузка начинается с середины списка.

Возврат значения null для nextKey сообщает Paging, что страниц больше нет, поэтому новые запросы прекращаются. Если забыть об этом, могут возникнуть бесконечные пустые запросы.

// 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

Реализация getRefreshKey()

При обновлении списка — например, с помощью жеста обновления или после его недействительности — Paging должен знать, какую страницу загрузить заново, чтобы пользователь примерно остался на прежнем месте.

getRefreshKey() использует текущую позицию anchorPosition — элемента, ближайшего к области просмотра, — чтобы выбрать подходящий ключ.

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

Корректная обработка ошибок

Оберните сетевой вызов в try/catch и при сбое верните LoadResult.Error(e). Paging передаст это в интерфейс как LoadState.Error, чтобы вы могли показать кнопку повтора.

Никогда не позволяйте исключению выйти из load() — перехватите его и преобразуйте в 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)
}

Создание Pager

Pager связывает ваш PagingConfig с фабрикой, которая создает новый PagingSource. Его свойство .flow — это Flow<PagingData>, который собирает интерфейс.

Лямбда фабрики должна каждый раз создавать новый источник, поскольку Paging делает источник недействительным и создает его заново при обновлении.

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 для ViewModels

Сбор PagingData выполняется только один раз: повторный сбор запускает загрузку заново. Чтобы пережить изменения конфигурации и позволить нескольким сборщикам совместно использовать данные, сохраните поток в viewModelScope с помощью 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 и pageSize

Обратите внимание: load() читает params.loadSize, а не настроенный вами напрямую pageSize.

При самой первой загрузке Paging может запросить больший начальный фрагмент. Его размер задается параметром initialLoadSize в PagingConfig и по умолчанию равен трехкратному размеру страницы. Всегда передавайте в API значение params.loadSize, чтобы размер запроса соответствовал ожидаемому Paging результату.

// 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 с курсорами

Не каждый API использует номера страниц. Некоторые возвращают курсор или токен, указывающий на следующую страницу. Подход остается тем же — просто измените тип Key на String и используйте токен из ответа.

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
}

Собираем все вместе

Теперь у вас есть полноценный слой данных: PagingSource, загружающий одну страницу, Pager, передающий страницы в потоке, и ViewModel, кэширующий этот поток.

Слой интерфейса просто собирает viewModel.articles — в следующем уроке мы отобразим его с помощью 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

Быстрая проверка

Что означает возврат nextKey = null из load() в PagingSource со страницами, обозначенными номерами?

Итоги: PagingSource и Pager

Вы создали слой данных для постраничной загрузки:

  • PagingSource<Key, Value> реализует load() и getRefreshKey();
  • load() возвращает LoadResult.Page с prevKey/nextKey или LoadResult.Error;
  • ключ со значением null останавливает постраничную загрузку в соответствующем направлении;
  • Pager(config, factory).flow создает Flow<PagingData>;
  • cachedIn(viewModelScope) сохраняет данные при изменениях конфигурации.

Далее: отображение этого потока в списке Compose.

Можно начать бесплатно

Изучай Kotlin с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
36
Уроки
152

Часто задаваемые вопросы

Урок «PagingSource и Pager» бесплатный?

Да — полный текст урока «PagingSource и Pager» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Android Academy, подпишись на CoddyKit PRO. Курс Android Academy содержит 4 уроков всего.

Чему я научусь в уроке «PagingSource и Pager»?

Определяйте способ загрузки страниц Ты практикуешь Android Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Android Academy?

Предыдущий опыт не требуется. Android Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «PagingSource и Pager»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Android Academy?

Да. Каждый урок Android Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Зачем нужна постраничная загрузка
  2. PagingSource и Pager
  3. Постраничная загрузка в списках Compose
  4. RemoteMediator и кэширование
← Назад к Android Academy