Paging 3 для Kotlin: полное руководство с примерами
1. Что это и зачем нужна
Paging 3 — это библиотека от Google для Android, написанная на Kotlin, которая решает одну из самых частых задач в мобильной разработке: постраничную загрузку данных (pagination). Когда у вас есть список из тысяч или миллионов элементов (например, лента новостей, сообщения в чате, товары в каталоге), загружать их все сразу невозможно — это приведёт к переполнению памяти, зависанию UI и огромному расходу трафика. Paging 3 автоматизирует процесс подгрузки данных порциями (страницами) по мере прокрутки списка пользователем.
Библиотека построена на современных принципах: она использует корутины Kotlin для асинхронной работы, Flow для реактивного обновления данных и полностью интегрируется с Jetpack Compose и RecyclerView. В отличие от предыдущих версий (Paging 2), Paging 3 предлагает более чистую архитектуру, меньше бойлерплейта и встроенную поддержку обработки ошибок, обновления данных и кэширования.
Основная ценность Paging 3 — вы пишете логику загрузки одной страницы (например, запрос к API), а библиотека сама решает, когда запрашивать следующую, как хранить данные в памяти, как уведомлять адаптер об изменениях и как обрабатывать состояния загрузки (загрузка, ошибка, пустой список). Это экономит часы разработки и делает приложение стабильным даже при работе с огромными наборами данных.
2. Установка
Paging 3 — это библиотека для Android/Kotlin, поэтому она устанавливается через систему сборки Gradle, а не через pip или npm. Добавьте зависимость в файл build.gradle.kts (модуль app):
// build.gradle.kts (Module: app)
dependencies {
// Основная библиотека Paging 3
implementation("androidx.paging:paging-runtime-ktx:3.3.5")
// Если используете Jetpack Compose (опционально)
implementation("androidx.paging:paging-compose:3.3.5")
// Если используете RxJava (опционально)
implementation("androidx.paging:paging-rxjava3:3.3.5")
// Для тестирования (опционально)
testImplementation("androidx.paging:paging-common:3.3.5")
}
После синхронизации Gradle библиотека готова к использованию. Минимальная версия SDK — 21 (Android 5.0).
3. Быстрый старт — минимальный рабочий пример
Создадим простейший экран, который загружает список чисел от 1 до 1000 по 20 элементов на страницу. Пример состоит из трёх частей: источник данных (PagingSource), ViewModel и UI (RecyclerView).
Шаг 1. PagingSource — источник данных
class NumberPagingSource : PagingSource<Int, Int>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Int> {
return try {
// Определяем ключ следующей страницы (если null — первая загрузка)
val currentPage = params.key ?: 0
val pageSize = params.loadSize // обычно 20
// Генерируем данные для текущей страницы
val start = currentPage * pageSize
val end = minOf(start + pageSize, 1000)
val numbers = (start until end).toList()
// Возвращаем результат с ключами для следующей и предыдущей страницы
LoadResult.Page(
data = numbers,
prevKey = if (currentPage > 0) currentPage - 1 else null,
nextKey = if (end < 1000) currentPage + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
// Возвращает ключ для перезагрузки (обычно null или первый ключ)
override fun getRefreshKey(state: PagingState<Int, Int>): Int? = null
}
Шаг 2. ViewModel
class MainViewModel : ViewModel() {
val pager = Pager(
config = PagingConfig(pageSize = 20, enablePlaceholders = false),
pagingSourceFactory = { NumberPagingSource() }
).flow.cachedIn(viewModelScope)
}
Шаг 3. Activity с RecyclerView
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val adapter = NumberAdapter()
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(this)
// Подписываемся на Flow из ViewModel
lifecycleScope.launch {
viewModel.pager.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
// Простой адаптер
class NumberAdapter : PagingDataAdapter<Int, NumberAdapter.ViewHolder>(diffCallback) {
companion object {
val diffCallback = object : DiffUtil.ItemCallback<Int>() {
override fun areItemsTheSame(oldItem: Int, newItem: Int) = oldItem == newItem
override fun areContentsTheSame(oldItem: Int, newItem: Int) = oldItem == newItem
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView: TextView = itemView.findViewById(android.R.id.text1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.textView.text = getItem(position)?.toString() ?: "Загрузка..."
}
}
Готово! При запуске вы увидите список чисел, который подгружается по мере прокрутки.
4. Основные методы/функции/классы
4.1 PagingSource
Сигнатура: abstract class PagingSource<Key, Value>
Что делает: Базовый класс для источника данных. Определяет, как загружать страницы и как управлять ключами пагинации.
class MySource : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
val page = params.key ?: 1
val response = api.getUsers(page, params.loadSize)
return LoadResult.Page(
data = response.users,
prevKey = if (page > 1) page - 1 else null,
nextKey = if (response.hasNext) page + 1 else null
)
}
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchor ->
state.closestItemToPosition(anchor)?.id
}
}
}
4.2 Pager
Сигнатура: class Pager<Key, Value>(config: PagingConfig, pagingSourceFactory: () -> PagingSource<Key, Value>)
Что делает: Фабрика для создания Flow<PagingData>. Принимает конфигурацию и фабрику источника данных.
val pager = Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 5,
enablePlaceholders = false,
initialLoadSize = 40
),
pagingSourceFactory = { MyPagingSource() }
).flow.cachedIn(viewModelScope)
4.3 PagingConfig
Сигнатура: class PagingConfig(pageSize: Int, prefetchDistance: Int = pageSize, enablePlaceholders: Boolean = true, initialLoadSize: Int = pageSize * 3, maxSize: Int = Int.MAX_VALUE, jumpThreshold: Int = Int.MAX_VALUE)
Что делает: Настройки пагинации: размер страницы, расстояние до предзагрузки, максимальный размер кэша.
val config = PagingConfig(
pageSize = 50,
prefetchDistance = 10,
enablePlaceholders = false,
initialLoadSize = 100,
maxSize = 500
)
4.4 PagingDataAdapter
Сигнатура: abstract class PagingDataAdapter<T, VH : RecyclerView.ViewHolder>(diffCallback: DiffUtil.ItemCallback<T>)
Что делает: Адаптер RecyclerView, который автоматически обрабатывает постраничные данные. Вызывает submitData() для обновления списка.
class UserAdapter : PagingDataAdapter<User, UserAdapter.ViewHolder>(USER_COMPARATOR) {
companion object {
private val USER_COMPARATOR = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(old: User, new: User) = old.id == new.id
override fun areContentsTheSame(old: User, new: User) = old == new
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
4.5 submitData (метод PagingDataAdapter)
Сигнатура: suspend fun submitData(pagingData: PagingData<T>)
Что делает: Принимает новый экземпляр PagingData и обновляет список. Должен вызываться из корутины.
lifecycleScope.launch {
viewModel.pager.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
4.6 LoadResult.Page и LoadResult.Error
Сигнатура: sealed class LoadResult<Key, Value> с подклассами Page и Error
Что делает: Возвращаемое значение метода load(). Page содержит данные и ключи, Error — исключение.
return try {
val data = fetchData(params.key)
LoadResult.Page(
data = data,
prevKey = null,
nextKey = data.lastOrNull()?.id
)
} catch (e: IOException) {
LoadResult.Error(e)
}
4.7 PagingState
Сигнатура: class PagingState<Key, Value>(pages: List<Page<Key, Value>>, anchorPosition: Int?, config: PagingConfig, placeholdersEnabled: Boolean)
Что делает: Предоставляет состояние всех загруженных страниц. Используется в getRefreshKey() для определения ключа перезагрузки.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
return state.anchorPosition?.let { anchor ->
state.closestItemToPosition(anchor)?.id
}
}
4.8 cachedIn (расширение Flow)
Сигнатура: fun <T> Flow<PagingData<T>>.cachedIn(scope: CoroutineScope): Flow<PagingData<T>>
Что делает: Кэширует загруженные страницы в указанном CoroutineScope. Предотвращает повторную загрузку при повороте экрана.
class MyViewModel : ViewModel() {
val flow = Pager(...).flow.cachedIn(viewModelScope)
}
4.9 LoadParams
Сигнатура: class LoadParams<Key>(key: Key?, loadSize: Int, placeholdersEnabled: Boolean)
Что делает: Параметры, передаваемые в метод load(): ключ страницы, размер загрузки и флаг плейсхолдеров.
override suspend fun load(params: LoadParams<String>): LoadResult<String, Item> {
val cursor = params.key ?: ""
val response = api.getItems(cursor, params.loadSize)
return LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = response.nextCursor
)
}
4.10 RemoteMediator (для сетевого кэширования)
Сигнатура: abstract class RemoteMediator<Key, Value>
Что делает: Позволяет комбинировать загрузку из сети и локальной базы данных (Room). Автоматически синхронизирует данные.
@OptIn(ExperimentalPagingApi::class)
class MyRemoteMediator(
private val api: Api,
private val db: AppDatabase
) : RemoteMediator<Int, User>() {
override suspend fun load(loadType: LoadType, state: PagingState<Int, User>): MediatorResult {
return try {
val page = when (loadType) {
LoadType.REFRESH -> 1
LoadType.PREPEND -> return MediatorResult.Success(endOfPaginationReached = true)
LoadType.APPEND -> {
val lastItem = state.lastItemOrNull()
if (lastItem == null) 1 else (lastItem.id / state.config.pageSize) + 1
}
}
val response = api.getUsers(page, state.config.pageSize)
db.runInTransaction {
if (loadType == LoadType.REFRESH) db.userDao().clearAll()
db.userDao().insertAll(response.users)
}
MediatorResult.Success(endOfPaginationReached = response.users.isEmpty())
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
}
5. Реальный практический пример (100-200 строк кода)
Создадим приложение для загрузки списка репозиториев GitHub с поиском. Используем Retrofit, Room, Paging 3 и RemoteMediator.
1. Модели данных и API
// Модель для сети
data class Repo( Также в библиотеке
Работа с S3 и облачными хранилищами в C++: библиотека AWS SDK для C++
Swagger/OpenAPI генерация для JavaScript (Node.js) — инструменты и библиотеки