Cursor Pagination în dezvoltarea mobilă — ce este, principiu și implementare

Autor: IT Sectr Publicat: 2026-03-11 Timp de citire: 9 min

Cursor Pagination — metodă de paginare a datelor care folosește un cursor unic pentru navigarea printr-un set ordonat de înregistrări. Conform GraphQL Specification (2025), paginarea cursor este standardul recomandat pentru API-urile care lucrează cu date dinamice. Cursor Pagination elimină principalele dezavantaje ale abordării Offset: instabilitatea la inserări și scăderea performanței la deplasări mari.

Puncte principale

  • Cursor Pagination — metodă de paginare în care fiecare înregistrare are un identificator-cursor unic pentru navigare.
  • Cursorul — marker unic al poziției în setul de date (de obicei ID, UUID, timestamp), care nu se schimbă la inserări.
  • Stabilitate — înregistrările noi adăugate între cereri nu deplasează cursorul, eliminând duplicatele și omisiunile.
  • Performanță — interogarea WHERE id > cursor folosește eficient indexul, fără a pierde viteza pe seturi mari.
  • Limitare — paginarea cursor nu suportă navigarea după numărul paginii (nu se poate sări la pagina 5).

Ce este Cursor Pagination?

Cursor Pagination (paginarea cursor) — metodă de paginare în care serverul returnează împreună cu datele un indicator special — cursorul. Clientul folosește acest cursor în următoarea cerere pentru a obține următoarea porție de înregistrări. Cursorul este identificatorul unic al ultimului element al paginii curente.

Spre deosebire de Offset-pagination, unde clientul spune „dă-mi pagina 5 cu câte 20 de înregistrări”, paginarea cursor funcționează diferit: „dă-mi 20 de înregistrări după înregistrarea cu ID = 83”. Serverul execută interogarea cu condiția WHERE id > 83 și LIMIT 20. Astfel de abordare garantează că fiecare înregistrare ajunge exact într-o pagină, indiferent de inserări.

Conceptul paginării cursor a câștigat popularitate datorită specificației Relay Connection (GraphQL), care a făcut cursor-based pagination standard pentru API-urile moderne. Relay definește formatul răspunsului: edges (matrice de înregistrări cu cursoare), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Istoria apariției

Cursor-pagination nu este o tehnică nouă — era folosită în bazele de date cu mult înainte de apariția web-ului. În SQL se numește keyset pagination sau seek method. Metoda a devenit populară în API-uri după publicarea specificației Relay în 2015, care a formalizat formatul cursorului ca șir codat base64 pentru uniformitatea transmiterii prin HTTP.

Cum funcționează paginarea cursor

Principiul de bază al paginării cursor — interogarea folosește condiția WHERE pe un câmp indexat pentru poziționare, nu deplasarea. Pentru direcția înainte se aplică WHERE id > last_id, pentru direcția inversă — WHERE id < first_id. Indexul B-tree permite găsirea primei înregistrări după cursor în O(log n), oferind un timp de răspuns stabil.

sql
-- Obține 20 de înregistrări după cursorul '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Obține 20 de înregistrări ÎNAINTE de cursorul '83' (înapoi)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Formatul cursorului

Cursorul poate fi simplu (valoarea ID) sau complex (compus din mai multe câmpuri). Cursoarele simple sunt cheia primară a înregistrării, de exemplu id autoincrement sau UUID. Cursoarele complexe sunt folosite pentru sortarea după câmpuri non-unice, de exemplu (created_at, id), unde id garantează unicitatea la marcaje de timp identice.

Formatul tipic API — cursorul sub formă de șir codat base64. Serverul decodează cursorul, extrage valoarea și construiește interogarea SQL. Codarea Base64 ascunde structura internă a cursorului de client și permite schimbarea formatului fără pierderea compatibilității inverse. Clientul primește cursoarele în câmpul endCursor al răspunsului și le transmite ca șir în următoarea cerere.

Navigarea înainte și înapoi

Paginarea cursor suportă navigarea bidirecțională. Pentru mișcarea înainte (next) se folosește cursorul ultimului element al paginii curente, pentru înapoi (previous) — cursorul primului element. Parametrii after și before din cerere determină direcția: after ia înregistrările după cursor, before — înainte de cursor.

Cursor vs Offset pagination

Alegerea între paginarea cursor și Offset-pagination — una dintre întrebările arhitecturale cheie la proiectarea API-urilor. Fiecare metodă are puncte forte și slabe. Cursor-pagination câștigă în scenariile cu date dinamice, Offset — în scenariile cu navigare arbitrară.

CaracteristicăCursorOffset
Stabilitate la inserăriRidicată (fără duplicate)Scăzută (deplasarea paginilor)
Performanță pe seturi mariO(log n) — stabilăO(n) — scade cu creșterea
Navigare după numărul paginiiNuDa (page=5)
Complexitatea implementăriiMedieScăzută
Suport în RESTcursor/before/afterpage/offset
Suport în GraphQLStandard RelayNerecomandat

De ce Offset pierde la scară mare

Offset-pagination execută scanarea completă a tabelului până la poziția OFFSET. La offset=100000, baza de date citește și omite 100000 de rânduri, chiar dacă LIMIT este 20. MySQL și PostgreSQL nu pot optimiza OFFSET — aceasta este o caracteristică a implementării LIMIT/OFFSET în SQL. Cursor-pagination folosește indexul B-tree, care găsește poziția în O(log n).

O problemă suplimentară a Offset — „omisiunea” înregistrărilor la paginarea înapoi. Dacă utilizatorul a încărcat pagina 5, iar în acest moment s-au adăugat înregistrări noi, la cererea paginii 6 va vedea din nou înregistrarea de la pagina 5 sau va omite înregistrările noi. Cursor-pagination elimină complet acest scenariu: cursorul indică un loc concret în set, iar inserările nu schimbă poziția.

Implementarea Cursor Pagination

Să examinăm implementarea paginării cursor pe backend (Kotlin + Spring) și pe client (Android + Retrofit). Serverul primește parametrii after, before, limit și returnează lista de înregistrări cu cursoare și pageInfo. Răspunsul tipic conține hasNextPage și hasPreviousPage pentru gestionarea UI-ului de paginare.

kotlin
@GetMapping("/posts")
fun getPosts(
    @RequestParam after: Long?,
    @RequestParam(defaultValue = "20") limit: Int
): CursorResponse<Post> {
    val cursor = after ?: Long.MAX_VALUE
    val posts = repository.findByIdLessThanOrderByIdDesc(
        cursor, PageRequest.of(0, limit)
    )
    val endCursor = posts.lastOrNull()?.id
    return CursorResponse(
        data = posts,
        pageInfo = PageInfo(
            hasNextPage = posts.size == limit,
            endCursor = endCursor
        )
    )
}

Implementarea client în Android

Pe partea client, paginarea cursor se implementează prin PagingSource din Paging 3, unde cheia este cursorul (Long). PagingSource.load primește LoadParams.key — cursorul ultimei înregistrări încărcate. LoadResult.Page returnează datele și nextKey — cursorul pentru pagina următoare. Când nextKey = null — paginarea este finalizată.

kotlin
// Retrofit API
interface PostApi {
    @GET("posts")
    suspend fun getPosts(
        @Query("after") after: Long?,
        @Query("limit") limit: Int = 20
    ): CursorResponse<Post>
}

// PagingSource cu cheie cursor
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> = try {
        val response = api.getPosts(
            after = params.key,
            limit = params.loadSize
        )
        val nextKey = response.pageInfo.endCursor
        LoadResult.Page(
            data = response.data,
            prevKey = null,
            nextKey = nextKey
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

Implementarea GraphQL prin Relay

În GraphQL, paginarea cursor se implementează prin modelul Connection Relay. Fiecare tip are Connection (cu pageInfo și edges) și Edge (node + cursor). Cererea transmite parametrii first, after, last, before. Serverul returnează o matrice de edges cu cursoare și pageInfo cu hasNextPage/hasPreviousPage.

Când să folosim Cursor Pagination

Cursor-pagination este recomandată pentru API-urile care lucrează cu date dinamice, unde înregistrările sunt adesea adăugate sau șterse. Exemple clasice: fluxul de știri în rețeaua socială, mesaje în chat, istoricul tranzacțiilor, comentarii la postări. În toate aceste scenarii, consistența și absența duplicatelor sunt importante.

  • Chaturi și mesagerii — fiecare mesaj nou se adaugă în partea de sus a listei. Offset-pagination se dereglează la fiecare mesaj nou.
  • Rețele sociale și fluxuri — postările sunt publicate continuu. Paginarea cursor garantează că utilizatorul nu ratează niciun post.
  • Istoricul comenzilor și tranzacțiilor — datele se schimbă mai rar, dar consistența este critică pentru raportarea financiară.
  • API-uri cu volume mari de date — milioane de înregistrări. Cursor-pagination păstrează performanța acolo unde Offset începe să încetinească.
  • GraphQL API — standardul Relay impune utilizarea cursor-based pagination conform specificației.
  • Aplicații mobile cu scroll infinit — utilizatorul derulează în jos, încărcând porții noi. Abordarea cursor oferă o experiență fluidă fără duplicate.

Când Cursor-pagination nu este potrivită

Există scenarii în care Offset-pagination este mai comodă: panouri administrative, unde este necesară navigarea după numărul paginii; căutare cu paginare, unde rezultatele se pot schimba; rapoarte și analitică, unde este necesar un link fix către pagina 5. În aceste cazuri, avantajele cursorului nu depășesc complexitatea implementării.

Paginarea cursor nu suportă „săritura” la o pagină arbitrară — utilizatorul nu poate face clic pe „Pagina 5” și ajunge la ea. Aceasta este o limitare arhitecturală: pentru calcularea numărului total de pagini este necesară o interogare COUNT separată, care poate fi costisitoare pentru tabele mari. În astfel de cazuri, abordarea hibridă: cursor pentru date + count pentru paginare.

Întrebări frecvente

Ce este cursorul în Cursor Pagination?

Cursorul este identificatorul unic al înregistrării care indică poziția în setul de date. Poate fi simplu (ID-ul înregistrării) sau compus (mai multe câmpuri). Clientul primește cursorul ultimei înregistrări a paginii și îl transmite în următoarea cerere pentru a obține următoarea porție.

Cu ce este mai bună Cursor Pagination decât Offset?

Cursor-pagination nu este supusă deplasării la adăugarea de înregistrări noi — fiecare element ajunge exact într-o pagină. De asemenea, păstrează viteza pe volume mari datorită utilizării indexurilor în loc de scanarea primelor n rânduri. Offset este mai simplu, dar instabil pentru date dinamice.

Se poate implementa paginarea cursor fără GraphQL?

Da, paginarea cursor nu este legată de GraphQL. Poate fi implementată în orice REST API, transmițând cursorul ca parametru de interogare ?after=83&limit=20. Răspunsul trebuie să conțină pageInfo cu endCursor și hasNextPage — aceasta permite clientului să gestioneze încărcarea fără a cunoaște structura internă a cursorului.

Ce cursor să folosim — ID, UUID sau timestamp?

ID autoincrement — alegerea optimă: crește monoton, nu se schimbă, este indexat eficient. UUID v7 (ordonat temporal) este de asemenea potrivit. Timestamp poate da duplicate la același timp, de aceea se combină cu ID: (created_at, id) pentru garantarea unicității cursorului.

Cum aflăm numărul total de pagini la paginarea cursor?

Paginarea cursor nu oferă numărul total de pagini — aceasta este limitarea sa. Dacă este necesară informația despre total, executați o interogare COUNT separată cu aceleași filtre. Pentru tabele mari, utilizați numărarea aproximativă prin EXPLAIN sau totalul din cache provenit din analitică.

Concluzii

  • Cursor Pagination — metodă de paginare cu navigare după identificatorul unic al înregistrării în loc de deplasare.
  • Cursorul garantează stabilitatea setului la inserări: înregistrările noi nu deplasează paginile deja încărcate.
  • Performanța pe volume mari de date rămâne ridicată (O(log n)) datorită utilizării indexurilor B-tree.
  • Cursor-pagination este potrivită pentru date dinamice: chaturi, fluxuri de știri, tranzacții, comentarii.
  • Limitarea principală — lipsa navigării după numărul paginii și imposibilitatea de a sări la o pagină arbitrară.
  • Implementarea folosește WHERE id după cursor, parametrii after/before și pageInfo în răspuns.
  • Standardul — Relay Connection GraphQL, dar REST API cu parametrii cursor este de asemenea larg răspândit.

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.

Discutați proiectul

Citiți și