Пагинация — техника постраничной загрузки данных, используемая в мобильных приложениях и веб-сервисах для работы с большими наборами записей. Согласно Android Developers Documentation (2025), правильная реализация пагинации снижает нагрузку на API, экономит трафик и улучшает пользовательский опыт. Постраничная загрузка позволяет приложению отображать контент постепенно, без ожидания полной загрузки всех данных.
Главное
Пагинация (от англ. pagination — постраничное деление) — это техника разбиения большого набора данных на последовательные порции (страницы). В мобильных приложениях пагинация используется при загрузке списков сообщений, ленты новостей, каталогов товаров, истории заказов и любых других коллекций с потенциально неограниченным числом записей.
Без пагинации приложение вынуждено загружать все данные сразу, что приводит к долгому ожиданию, большому расходу трафика и нестабильной работе на слабых устройствах. API-запрос с пагинацией возвращает только одну порцию данных и мета-информацию для загрузки следующей — таким образом, приложение контролирует объём получаемой информации.
Основные метрики пагинации: размер страницы (page size) — количество записей на одной странице (обычно 10-50), и номер страницы или курсор — указатель на текущую позицию в наборе. Выбор размера страницы зависит от типа данных: для компактных элементов (названия) достаточно 20-30, для карточек с изображениями — 10-15.
Мобильные устройства имеют ограниченные ресурсы: объём оперативной памяти, скорость процессора и лимиты трафика. Пагинация решает три ключевые задачи: снижение потребления памяти (в памяти хранятся только видимые элементы), ускорение первого отображения (первая порция грузится быстрее всего набора) и экономия трафика (данные подгружаются только когда пользователь прокручивает список).
Существует четыре основных вида пагинации, каждый из которых решает специфические задачи. Выбор метода зависит от требований к согласованности данных, архитектуры API, типа хранилища и допустимой сложности реализации на клиенте и сервере.
| Тип | Принцип работы | Стабильность | Скорость на больших объёмах |
|---|---|---|---|
| Offset | LIMIT + OFFSET в SQL | низкая | падает с ростом OFFSET |
| Cursor | WHERE id > last_id | высокая | стабильная (O(log n)) |
| Keyset | WHERE key > last_key | высокая | стабильная (O(log n)) |
| Time-based | WHERE created_at < last_time | средняя | стабильная с индексом |
Offset-пагинация подходит для статических или редко обновляемых наборов данных, когда важна простая реализация. Cursor и Keyset — для динамических данных с частыми вставками. Time-based — для хронологических лент, где записи упорядочены по времени создания. GraphQL-стандарт Relay использует cursor-пагинацию как единственный рекомендуемый метод.
Offset-пагинация — самый простой вид постраничной загрузки. Клиент передаёт параметры page и limit (или offset и limit), сервер применяет SQL OFFSET и LIMIT. Например, page=2, limit=20 вернёт записи с 21 по 40. Этот метод интуитивно понятен и легко реализуется на любом стеке.
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items")
async def get_items(
page: int = Query(default=1, ge=1),
limit: int = Query(default=20, le=100)
):
offset = (page - 1) * limit
items = await fetch_items(offset, limit)
total = await count_items()
return {
"items": items,
"total": total,
"page": page,
"pages": (total + limit - 1) // limit
}
Главный недостаток Offset-пагинации — проблема пропущенных и дублирующихся записей. Если между двумя запросами в таблицу добавляются новые записи, OFFSET смещается: пользователь может увидеть одну запись дважды или пропустить новую. Это критично для лент новостей и чатов, где важна консистентность.
Другая проблема — падение производительности на больших OFFSET. База данных вынуждена сканировать и пропускать первые offset записей перед возвратом результата. При offset=100000 даже с LIMIT 20 сервер потратит заметное время на сканирование. PostgreSQL и MySQL демонстрируют линейное падение скорости с ростом OFFSET.
Offset-пагинация остаётся лучшим выбором для: административных панелей (данные меняются редко, нужна навигация по страницам), отчётов и исторических логов (фиксированный срез данных), каталогов с фильтрацией (можно перейти на любую страницу). Также Offset проще всего реализовать на клиенте — RecyclerView с Paging 3 поддерживает его из коробки.
Keyset-пагинация использует уникальный ключ (обычно первичный ключ) для фильтрации записей. Вместо OFFSET запрос использует WHERE id > last_seen_id. Это обеспечивает стабильную производительность независимо от количества записей и отсутствие дубликатов при вставках, так как новые записи всегда имеют больший id.
-- Offset-пагинация (проблемная)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;
-- Keyset-пагинация (стабильная)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;
Time-based пагинация (или cursor по времени) использует временную метку created_at для навигации. Клиент передаёт timestamp последней загруженной записи, сервер возвращает записи, созданные до или после этой метки. Метод популярен в социальных сетях и лентах новостей, где порядок записей определяется временем публикации.
Особенность Time-based пагинации — возможны дубликаты, если две записи созданы в одну миллисекунду. Для устранения этой проблемы комбинируют time-based ключ с уникальным id: WHERE (created_at, id) < (last_time, last_id). Такой составной курсор гарантирует уникальность каждой записи и точный порядок.
Keyset-пагинация требует столбца с уникальным и монотонно возрастающим значением (автоинкрементный id, UUID v7). Time-based подходит для любых таблиц с created_at, но требует дополнительной обработки дубликатов. Основное различие: Keyset стабильно работает при любых операциях вставки, а Time-based чувствителен к одинаковым временным меткам.
Выбор типа пагинации зависит от характера данных и требований к пользовательскому опыту. Ниже приведены рекомендации для типовых сценариев в мобильной разработке. Универсального решения не существует — каждый метод имеет область применения, в которой он оптимален.
Библиотека Android Paging 3 поддерживает все типы пагинации через PagingSource. Для Offset — PagingSource с ключом Int (page), для Cursor — с ключом String или Long (cursor). PagingSource автоматически управляет загрузкой, кэшированием и повторными попытками при ошибках.
class PostPagingSource(
private val api: PostApi
) : PagingSource<Long, Post>() {
override suspend fun load(
params: LoadParams<Long>
): LoadResult<Long, Post> {
val cursor = params.key ?: Long.MAX_VALUE
return try {
val response = api.getPosts(cursor, params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = response.items.lastOrNull()?.id
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
override fun getRefreshKey(state: PagingState<Long, Post>): Long? {
return state.anchorPosition?.let {
state.closestItemToPosition(it)?.id
}
}
}
Размер страницы влияет на скорость загрузки и восприятие производительности. Для мобильных приложений оптимальный диапазон — 10-25 элементов на страницу. Меньше 10 — слишком частые запросы к API и дёрганный скролл. Больше 25 — долгая загрузка первой порции на медленных сетях.
Для изображений и видео размер страницы уменьшают до 5-10, так как каждый элемент требует дополнительного времени на загрузку медиа. Для списков с текстовыми элементами (комментарии, логи) размер можно увеличить до 30-50 записей. Рекомендуется делать размер страницы настраиваемым через API, чтобы клиент мог адаптироваться под разные условия сети.
Часто задаваемые вопросы
Пагинация — это загрузка данных частями, а не всеми сразу. Как в книге: вы читаете одну страницу, потом переворачиваете на следующую. В приложении это означает, что при прокрутке списка подгружается следующая порция данных, а не весь список целиком, что экономит трафик и память.
Offset считает записи: «пропусти 20, верни следующие 10». Если между загрузками добавилась новая запись — нумерация сбивается. Cursor использует уникальный идентификатор последней записи: «верни 10 записей после ID = 100». Новые записи не влияют на позицию.
Для мобильных приложений оптимально 10-25 элементов. Для списков с изображениями — 5-10, для текстовых лент — 20-30. Размер зависит от среднего размера одного элемента: чем тяжелее элемент, тем меньше должна быть страница для быстрого отображения.
Используйте библиотеку Paging 3 из Android Jetpack. Она предоставляет PagingSource для загрузки, PagingData для реактивного потока и PagingDataAdapter для автоматической подгрузки при прокрутке. Библиотека поддерживает Offset, Cursor и Keyset пагинацию через кастомный PagingSource.
Бесконечный скролл — это UI-паттерн, при котором новая порция данных загружается автоматически при приближении к концу списка. Пагинация — это механизм загрузки данных порциями, а бесконечный скролл — один из способов его отображения. Альтернатива — кнопка «Загрузить ещё».
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также