Пагінація — техніка посторінкового завантаження даних, що використовується в мобільних додатках і веб-сервісах для роботи з великими наборами записів. Згідно з 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 пагінація (або курсор за часом) використовує часову мітку 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також