Paginarea — tehnica de încărcare a datelor pe pagini, utilizată în aplicațiile mobile și serviciile web pentru lucrul cu seturi mari de înregistrări. Conform Android Developers Documentation (2025), implementarea corectă a paginării reduce încărcarea API, economisește trafic și îmbunătățește experiența utilizatorului. Încărcarea pe pagini permite aplicației să afișeze conținutul treptat, fără a aștepta încărcarea completă a tuturor datelor.
Principalele puncte
Paginarea (din engl. pagination — divizare în pagini) — este tehnica de împărțire a unui set mare de date în porțiuni succesive (pagini). În aplicațiile mobile, paginarea este utilizată la încărcarea listelor de mesaje, fluxurilor de știri, cataloagelor de produse, istoricului comenzilor și oricăror alte colecții cu un număr potențial nelimitat de înregistrări.
Fără paginare, aplicația este nevoită să încarce toate datele deodată, ceea ce duce la așteptări lungi, consum mare de trafic și funcționare instabilă pe dispozitivele slabe. Cererea API cu paginare returnează doar o porțiune de date și meta-informații pentru încărcarea următoarei — astfel, aplicația controlează volumul de informații primite.
Principalii indicatori ai paginării: dimensiunea paginii (page size) — numărul de înregistrări pe o pagină (de obicei 10-50), și numărul paginii sau cursorul — indicatorul poziției curente în set. Alegerea dimensiunii paginii depinde de tipul datelor: pentru elemente compacte (denumiri) sunt suficiente 20-30, pentru carduri cu imagini — 10-15.
Dispozitivele mobile au resurse limitate: volumul memoriei RAM, viteza procesorului și limitele de trafic. Paginarea rezolvă trei sarcini cheie: reducerea consumului de memorie (în memorie se păstrează doar elementele vizibile), accelerarea primei afișări (prima porțiune se încarcă mai repede decât întregul set) și economisirea traficului (datele se încarcă doar când utilizatorul derulează lista).
Există patru tipuri principale de paginare, fiecare rezolvând sarcini specifice. Alegerea metodei depinde de cerințele privind coerența datelor, arhitectura API, tipul de stocare și complexitatea admisibilă a implementării pe client și server.
| Tip | Principiul de funcționare | Stabilitate | Viteza la volume mari |
|---|---|---|---|
| Offset | LIMIT + OFFSET în SQL | scăzută | scade odată cu creșterea OFFSET |
| Cursor | WHERE id > last_id | ridicată | stabilă (O(log n)) |
| Keyset | WHERE key > last_key | ridicată | stabilă (O(log n)) |
| Time-based | WHERE created_at < last_time | medie | stabilă cu index |
Paginarea Offset este potrivită pentru seturi de date statice sau rareori actualizate, când implementarea simplă este importantă. Cursor și Keyset — pentru date dinamice cu inserări frecvente. Time-based — pentru fluxuri cronologice, unde înregistrările sunt ordonate după momentul creării. Standardul GraphQL Relay utilizează paginarea cursor ca singura metodă recomandată.
Paginarea Offset — cel mai simplu tip de încărcare pe pagini. Clientul transmite parametrii page și limit (sau offset și limit), serverul aplică SQL OFFSET și LIMIT. De exemplu, page=2, limit=20 va returna înregistrările 21-40. Această metodă este intuitivă și ușor de implementat pe orice stivă tehnologică.
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
}
Principalul dezavantaj al paginării Offset — problema înregistrărilor omise și duplicate. Dacă între două cereri se adaugă înregistrări noi în tabel, OFFSET se deplasează: utilizatorul poate vedea aceeași înregistrare de două ori sau poate rata una nouă. Acest lucru este critic pentru fluxurile de știri și chat-uri, unde coerența este importantă.
O altă problemă — scăderea performanței la OFFSET-uri mari. Baza de date trebuie să scaneze și să sară peste primele offset înregistrări înainte de a returna rezultatul. La offset=100000 chiar și cu LIMIT 20, serverul va petrece timp considerabil scanând. PostgreSQL și MySQL demonstrează o scădere liniară a vitezei odată cu creșterea OFFSET.
Paginarea Offset rămâne cea mai bună alegere pentru: panouri administrative (datele se schimbă rar, este necesară navigarea între pagini), rapoarte și jurnale istorice (secțiune fixă de date), cataloage cu filtrare (se poate naviga la orice pagină). Offset este, de asemenea, cel mai ușor de implementat pe client — RecyclerView cu Paging 3 îl suportă implicit.
Paginarea Keyset utilizează o cheie unică (de obicei cheia primară) pentru filtrarea înregistrărilor. În loc de OFFSET, cererea folosește WHERE id > last_seen_id. Aceasta asigură o performanță stabilă indiferent de numărul de înregistrări și absența duplicatelor la inserări, deoarece înregistrările noi au întotdeauna un id mai mare.
-- Paginare Offset (problematică)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;
-- Paginare Keyset (stabilă)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;
Paginarea Time-based (sau cursorul temporal) utilizează marcajul de timp created_at pentru navigare. Clientul trimite timestamp-ul ultimei înregistrări încărcate, serverul returnează înregistrările create înainte sau după acest marcaj. Metoda este populară în rețelele sociale și fluxurile de știri, unde ordinea înregistrărilor este determinată de momentul publicării.
Particularitatea paginării Time-based — posibile duplicate dacă două înregistrări sunt create în aceeași milisecundă. Pentru a elimina această problemă, se combină cheia time-based cu id-ul unic: WHERE (created_at, id) < (last_time, last_id). Un astfel de cursor compus garantează unicitatea fiecărei înregistrări și ordinea exactă.
Paginarea Keyset necesită o coloană cu o valoare unică și monoton crescătoare (id auto-incrementat, UUID v7). Time-based este potrivită pentru orice tabel cu created_at, dar necesită procesarea suplimentară a duplicatelor. Diferența principală: Keyset funcționează stabil la orice operații de inserare, iar Time-based este sensibilă la marcaje de timp identice.
Alegerea tipului de paginare depinde de natura datelor și de cerințele privind experiența utilizatorului. Mai jos sunt recomandări pentru scenarii tipice în dezvoltarea mobilă. Nu există o soluție universală — fiecare metodă are un domeniu de aplicare în care este optimă.
Biblioteca Android Paging 3 suportă toate tipurile de paginare prin PagingSource. Pentru Offset — PagingSource cu cheie Int (page), pentru Cursor — cu cheie String sau Long (cursor). PagingSource gestionează automat încărcarea, cache-ul și reîncercările la erori.
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
}
}
}
Dimensiunea paginii influențează viteza de încărcare și percepția performanței. Pentru aplicațiile mobile, intervalul optim este 10-25 de elemente pe pagină. Mai puțin de 10 — cereri prea frecvente la API și derulare sacadată. Mai mult de 25 — încărcarea lungă a primei porțiuni pe rețele lente.
Pentru imagini și video, dimensiunea paginii se reduce la 5-10, deoarece fiecare element necesită timp suplimentar pentru încărcarea media. Pentru liste cu elemente text (comentarii, jurnale), dimensiunea poate fi mărită la 30-50 de înregistrări. Se recomandă ca dimensiunea paginii să fie configurabilă prin API, astfel încât clientul să se poată adapta la diferite condiții de rețea.
Întrebări frecvente
Paginarea — încărcarea datelor în porțiuni, nu pe toate deodată. Ca într-o carte: citești o pagină, apoi treci la următoarea. În aplicație, aceasta înseamnă că la derularea listei se încarcă următoarea porțiune de date, nu întreaga listă deodată, ceea ce economisește trafic și memorie.
Offset numără înregistrările: „sari peste 20, returnează următoarele 10„. Dacă între încărcări se adaugă o înregistrare nouă — numerotarea se deplasează. Cursor utilizează identificatorul unic al ultimei înregistrări: „returnează 10 înregistrări după ID = 100„. Înregistrările noi nu afectează poziția.
Pentru aplicațiile mobile, optim este 10-25 de elemente. Pentru liste cu imagini — 5-10, pentru fluxuri text — 20-30. Dimensiunea depinde de dimensiunea medie a unui element: cu cât elementul este mai greu, cu atât pagina trebuie să fie mai mică pentru o afișare rapidă.
Utilizați biblioteca Paging 3 din Android Jetpack. Aceasta oferă PagingSource pentru încărcare, PagingData pentru flux reactiv și PagingDataAdapter pentru încărcarea automată la derulare. Biblioteca suportă paginarea Offset, Cursor și Keyset prin PagingSource personalizat.
Derularea infinită — un model UI în care o nouă porțiune de date se încarcă automat la apropierea de sfârșitul listei. Paginarea — este mecanismul de încărcare a datelor în porțiuni, iar derularea infinită este una dintre modalitățile de afișare a acesteia. Alternativa — butonul „Încarcă mai mult„.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și