Пагінація в мобільній розробці — що це, види та принцип роботи

Автор: IT Sectr Опубліковано: 2026-03-11 Час читання: 9 хв

Пагінація — техніка посторінкового завантаження даних, що використовується в мобільних додатках і веб-сервісах для роботи з великими наборами записів. Згідно з Android Developers Documentation (2025), правильна реалізація пагінації знижує навантаження на API, економить трафік і покращує користувацький досвід. Посторінкове завантаження дозволяє додатку відображати контент поступово, без очікування повного завантаження всіх даних.

Головне

  • Пагінація — спосіб завантаження великих наборів даних порціями для оптимізації продуктивності та трафіку.
  • Offset-пагінація використовує зміщення (page/offset) для навігації — простий, але нестабільний метод при частих вставках.
  • Cursor-пагінація використовує унікальний курсор з останнього запису — стабільна при зміні даних між запитами.
  • Keyset-пагінація фільтрує по стовпцю з унікальним індексом — ефективна для великих таблиць без дублікатів.
  • Time-based пагінація групує записи за часовими мітками — зручна для стрічок новин і соціальних мереж.

Що таке пагінація?

Пагінація (від англ. pagination — посторінкове ділення) — це техніка розбиття великого набору даних на послідовні порції (сторінки). У мобільних додатках пагінація використовується при завантаженні списків повідомлень, стрічок новин, каталогів товарів, історії замовлень та будь-яких інших колекцій із потенційно необмеженою кількістю записів.

Без пагінації додаток змушений завантажувати всі дані одразу, що призводить до довгого очікування, великої витрати трафіку та нестабільної роботи на слабких пристроях. API-запит із пагінацією повертає лише одну порцію даних та мета-інформацію для завантаження наступної — таким чином, додаток контролює обсяг отримуваної інформації.

Основні метрики пагінації: розмір сторінки (page size) — кількість записів на одній сторінці (зазвичай 10-50), та номер сторінки або курсор — покажчик на поточну позицію в наборі. Вибір розміру сторінки залежить від типу даних: для компактних елементів (назви) достатньо 20-30, для карток із зображеннями — 10-15.

Навіщо потрібна пагінація в мобільних додатках

Мобільні пристрої мають обмежені ресурси: обсяг оперативної пам'яті, швидкість процесора та ліміти трафіку. Пагінація вирішує три ключові завдання: зниження споживання пам'яті (у пам'яті зберігаються лише видимі елементи), прискорення першого відображення (перша порція завантажується швидше за весь набір) та економія трафіку (дані підвантажуються лише коли користувач прокручує список).

Основні види пагінації

Існує чотири основні види пагінації, кожен із яких вирішує специфічні завдання. Вибір методу залежить від вимог до узгодженості даних, архітектури API, типу сховища та допустимої складності реалізації на клієнті та сервері.

ТипПринцип роботиСтабільністьШвидкість на великих обсягах
OffsetLIMIT + OFFSET у SQLнизькападає зі зростанням OFFSET
CursorWHERE id > last_idвисокастабільна (O(log n))
KeysetWHERE key > last_keyвисокастабільна (O(log n))
Time-basedWHERE created_at < last_timeсереднястабільна з індексом

Коли який тип використовувати

Offset-пагінація підходить для статичних або рідко оновлюваних наборів даних, коли важлива проста реалізація. Cursor і Keyset — для динамічних даних із частими вставками. Time-based — для хронологічних стрічок, де записи впорядковані за часом створення. GraphQL-стандарт Relay використовує cursor-пагінацію як єдиний рекомендований метод.

Offset-пагінація: переваги та недоліки

Offset-пагінація — найпростіший вид посторінкового завантаження. Клієнт передає параметри page та limit (або offset та limit), сервер застосовує SQL OFFSET та LIMIT. Наприклад, page=2, limit=20 поверне записи з 21 по 40. Цей метод інтуїтивно зрозумілий і легко реалізується на будь-якому стеку.

python
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-пагінація залишається найкращим вибором для: адміністративних панелей (дані змінюються рідко, потрібна навігація по сторінках), звітів та історичних логів (фіксований зріз даних), каталогів із фільтрацією (можна перейти на будь-яку сторінку). Offset також найпростіше реалізувати на клієнті — RecyclerView із Paging 3 підтримує його з коробки.

Keyset і Time-based пагінація

Keyset-пагінація використовує унікальний ключ (зазвичай первинний ключ) для фільтрації записів. Замість OFFSET запит використовує WHERE id > last_seen_id. Це забезпечує стабільну продуктивність незалежно від кількості записів та відсутність дублікатів при вставках, оскільки нові записи завжди мають більший id.

sql
-- 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 пагінація

Time-based пагінація (або курсор за часом) використовує часову мітку created_at для навігації. Клієнт передає timestamp останнього завантаженого запису, сервер повертає записи, створені до або після цієї мітки. Метод популярний у соціальних мережах і стрічках новин, де порядок записів визначається часом публікації.

Особливість Time-based пагінації — можливі дублікати, якщо два записи створено в одну мілісекунду. Для усунення цієї проблеми комбінують time-based ключ з унікальним id: WHERE (created_at, id) < (last_time, last_id). Такий складовий курсор гарантує унікальність кожного запису та точний порядок.

Порівняння Keyset і Time-based

Keyset-пагінація потребує стовпця з унікальним і монотонно зростаючим значенням (автоінкрементний id, UUID v7). Time-based підходить для будь-яких таблиць із created_at, але вимагає додаткової обробки дублікатів. Основна відмінність: Keyset стабільно працює при будь-яких операціях вставки, а Time-based чутливий до однакових часових міток.

Як вибрати тип пагінації для проєкту

Вибір типу пагінації залежить від характеру даних і вимог до користувацького досвіду. Нижче наведено рекомендації для типових сценаріїв у мобільній розробці. Універсального рішення не існує — кожен метод має область застосування, у якій він оптимальний.

  • Чат / месенджер — Cursor-пагінація (за id повідомлення). Нові повідомлення додаються зверху, курсор не збивається.
  • Стрічка новин — Time-based пагінація (за created_at). Записи впорядковані за часом, важлива хронологія.
  • Каталог товарів — Offset-пагінація. Користувач може перейти на конкретну сторінку, дані змінюються рідко.
  • Історія замовлень — Cursor-пагінація. Стабільність важлива, оскільки нові замовлення додаються між завантаженнями.
  • Коментарі — Keyset-пагінація. Кожен коментар має унікальний id, великі обсяги без дублікатів.

Реалізація на Android із Paging 3

Бібліотека Android Paging 3 підтримує всі типи пагінації через PagingSource. Для Offset — PagingSource із ключем Int (page), для Cursor — із ключем String або Long (cursor). PagingSource автоматично керує завантаженням, кешуванням і повторними спробами при помилках.

kotlin
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 відрізняється від Cursor пагінації?

Offset рахує записи: «пропусти 20, поверни наступні 10». Якщо між завантаженнями додався новий запис — нумерація збивається. Cursor використовує унікальний ідентифікатор останнього запису: «поверни 10 записів після ID = 100». Нові записи не впливають на позицію.

Який розмір сторінки пагінації оптимальний?

Для мобільних додатків оптимально 10-25 елементів. Для списків із зображеннями — 5-10, для текстових стрічок — 20-30. Розмір залежить від середнього розміру одного елемента: чим важчий елемент, тим меншою має бути сторінка для швидкого відображення.

Як реалізувати пагінацію в RecyclerView?

Використовуйте бібліотеку Paging 3 з Android Jetpack. Вона надає PagingSource для завантаження, PagingData для реактивного потоку та PagingDataAdapter для автоматичного підвантаження при прокручуванні. Бібліотека підтримує Offset, Cursor і Keyset пагінацію через кастомний PagingSource.

Що таке нескінченний скрол і чим він відрізняється від пагінації?

Нескінченний скрол — це UI-патерн, при якому нова порція даних завантажується автоматично при наближенні до кінця списку. Пагінація — це механізм завантаження даних порціями, а нескінченний скрол — один зі способів його відображення. Альтернатива — кнопка «Завантажити ще».

Підсумки

  • Пагінація — техніка завантаження даних порціями, необхідна для мобільних додатків із будь-якими списками.
  • Offset-пагінація проста в реалізації, але страждає від неузгодженості при вставках і падіння продуктивності на великих OFFSET.
  • Cursor-пагінація використовує унікальний ідентифікатор для навігації — стабільна та ефективна на будь-яких обсягах.
  • Keyset-пагінація фільтрує за первинним ключем, забезпечуючи максимальну продуктивність за рахунок використання індексів.
  • Time-based пагінація групує записи за часовими мітками — ідеальна для хронологічних стрічок і соціальних мереж.
  • Вибір методу залежить від характеру даних: для динамічних — Cursor/Keyset, для статичних — Offset, для стрічок — Time-based.
  • Paging 3 на Android і стандартні cursor-рішення на iOS/вебі надають готову інфраструктуру для будь-якого типу пагінації.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також