Пагинација — техника учитавања података страницу по страницу, која се користи у мобилним апликацијама и веб сервисима за рад са великим скуповима записа. Према Android Developers Documentation (2025), правилна имплементација пагинације смањује оптерећење API-ја, штеди саобраћај и побољшава корисничко искуство. Учитавање по страницама омогућава апликацији да приказује садржај постепено, без чекања на потпуно учитавање свих података.
Главно
Пагинација (од енгл. pagination — подела на странице) — техника разбијања великог скупа података на узастопне порције (странице). У мобилним апликацијама, пагинација се користи при учитавању листи порука, вести, каталога производа, историје поруџбина и било којих других колекција са потенцијално неограниченим бројем записа.
Без пагинације, апликација мора да учита све податке одједном, што доводи до дугог чекања, велике потрошње саобраћаја и нестабилног рада на слабијим уређајима. API захтев са пагинацијом враћа само једну порцију података и мета-информације за учитавање следеће — на тај начин апликација контролише количину примљених информација.
Главне метрике пагинације: величина странице (page size) — број записа на једној страници (обично 10-50), и број странице или курсор — показивач на тренутну позицију у скупу. Избор величине странице зависи од типа података: за компактне елементе (називе) довољно је 20-30, за картице са сликама — 10-15.
Мобилни уређаји имају ограничене ресурсе: количина RAM меморије, брзина процесора и ограничења саобраћаја. Пагинација решава три кључна задатка: смањење потрошње меморије (у меморији се чувају само видљиви елементи), убрзавање првог приказа (прва порција се учитава брже од целог скупа) и штедња саобраћаја (подаци се учитавају само када корисник помера листу).
Постоје четири основне врсте пагинације, од којих свака решава специфичне задатке. Избор метода зависи од захтева за конзистентношћу података, архитектуре 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође