PagingSource и Pager
Определяйте способ загрузки страниц
«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 — локальная установка не требуется.
Все уроки этого курса
- Зачем нужна постраничная загрузка
- PagingSource и Pager
- Постраничная загрузка в списках Compose
- RemoteMediator и кэширование