Paginacja — technika ładowania danych strona po stronie, stosowana w aplikacjach mobilnych i serwisach internetowych do pracy z dużymi zbiorami rekordów. Według Android Developers Documentation (2025), prawidłowa implementacja paginacji zmniejsza obciążenie API, oszczędza transfer i poprawia doświadczenie użytkownika. Ładowanie stronicowe pozwala aplikacji wyświetlać treści stopniowo, bez oczekiwania na pełne załadowanie wszystkich danych.
Najważniejsze
Paginacja (od ang. pagination — podział na strony) — to technika dzielenia dużego zbioru danych na kolejne porcje (strony). W aplikacjach mobilnych paginacja jest używana przy ładowaniu list wiadomości, kanałów informacyjnych, katalogów produktów, historii zamówień i wszelkich innych kolekcji z potencjalnie nieograniczoną liczbą rekordów.
Bez paginacji aplikacja musi ładować wszystkie dane naraz, co prowadzi do długiego oczekiwania, dużego zużycia transferu i niestabilnej pracy na słabych urządzeniach. Zapytanie API z paginacją zwraca tylko jedną porcję danych i metainformacje do załadowania następnej — w ten sposób aplikacja kontroluje ilość otrzymywanych informacji.
Główne metryki paginacji: rozmiar strony (page size) — liczba rekordów na jednej stronie (zwykle 10-50), oraz numer strony lub kursor — wskaźnik aktualnej pozycji w zbiorze. Wybór rozmiaru strony zależy od typu danych: dla kompaktowych elementów (nazwy) wystarczy 20-30, dla kart z obrazkami — 10-15.
Urządzenia mobilne mają ograniczone zasoby: ilość pamięci RAM, szybkość procesora i limity transferu. Paginacja rozwiązuje trzy kluczowe zadania: zmniejszenie zużycia pamięci (w pamięci przechowywane są tylko widoczne elementy), przyspieszenie pierwszego wyświetlenia (pierwsza porcja ładuje się szybciej niż cały zbiór) i oszczędność transferu (dane są ładowane tylko gdy użytkownik przewija listę).
Istnieją cztery główne rodzaje paginacji, z których każdy rozwiązuje specyficzne zadania. Wybór metody zależy od wymagań dotyczących spójności danych, architektury API, typu przechowywania i dopuszczalnej złożoności implementacji po stronie klienta i serwera.
| Typ | Zasada działania | Stabilność | Szybkość przy dużych wolumenach |
|---|---|---|---|
| Offset | LIMIT + OFFSET w SQL | niska | spada ze wzrostem OFFSET |
| Cursor | WHERE id > last_id | wysoka | stabilna (O(log n)) |
| Keyset | WHERE key > last_key | wysoka | stabilna (O(log n)) |
| Time-based | WHERE created_at < last_time | średnia | stabilna z indeksem |
Paginacja Offset nadaje się do statycznych lub rzadko aktualizowanych zbiorów danych, gdy ważna jest prosta implementacja. Cursor i Keyset — dla dynamicznych danych z częstymi wstawieniami. Time-based — dla chronologicznych kanałów, gdzie rekordy są uporządkowane według czasu utworzenia. Standard GraphQL Relay używa paginacji cursorowej jako jedynej zalecanej metody.
Paginacja Offset — najprostszy rodzaj ładowania stronicowego. Klient przekazuje parametry page i limit (lub offset i limit), serwer stosuje SQL OFFSET i LIMIT. Na przykład page=2, limit=20 zwróci rekordy od 21 do 40. Ta metoda jest intuicyjnie zrozumiała i łatwa do zaimplementowania na dowolnym stosie technologicznym.
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
}
Główna wada paginacji Offset — problem pominiętych i duplikujących się rekordów. Jeśli między dwoma zapytaniami do tabeli zostaną dodane nowe rekordy, OFFSET się przesuwa: użytkownik może zobaczyć ten sam rekord dwa razy lub pominąć nowy. Jest to krytyczne dla kanałów informacyjnych i czatów, gdzie ważna jest spójność.
Inny problem — spadek wydajności przy dużych OFFSET. Baza danych musi skanować i pomijać pierwsze offset rekordów przed zwróceniem wyniku. Przy offset=100000 nawet z LIMIT 20 serwer spędzi zauważalny czas na skanowaniu. PostgreSQL i MySQL wykazują liniowy spadek prędkości ze wzrostem OFFSET.
Paginacja Offset pozostaje najlepszym wyborem dla: panelów administracyjnych (dane zmieniają się rzadko, potrzebna nawigacja po stronach), raportów i logów historycznych (stały wycinek danych), katalogów z filtrowaniem (można przejść do dowolnej strony). Offset jest też najłatwiejszy do zaimplementowania po stronie klienta — RecyclerView z Paging 3 obsługuje go od razu.
Paginacja Keyset używa unikalnego klucza (zwykle klucza głównego) do filtrowania rekordów. Zamiast OFFSET zapytanie używa WHERE id > last_seen_id. Zapewnia to stabilną wydajność niezależnie od liczby rekordów i brak duplikatów przy wstawieniach, ponieważ nowe rekordy zawsze mają większe id.
-- Paginacja Offset (problematyczna)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;
-- Paginacja Keyset (stabilna)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;
Paginacja Time-based (lub kursor czasu) używa znacznika czasu created_at do nawigacji. Klient przekazuje timestamp ostatnio załadowanego rekordu, serwer zwraca rekordy utworzone przed lub po tym znaczniku. Metoda jest popularna w mediach społecznościowych i kanałach informacyjnych, gdzie kolejność rekordów określa czas publikacji.
Cechą paginacji Time-based są możliwe duplikaty, jeśli dwa rekordy zostały utworzone w tej samej milisekundzie. Aby wyeliminować ten problem, łączy się klucz time-based z unikalnym id: WHERE (created_at, id) < (last_time, last_id). Taki złożony kursor gwarantuje unikalność każdego rekordu i dokładną kolejność.
Paginacja Keyset wymaga kolumny z unikalną i monotonicznie rosnącą wartością (autoinkrementowane id, UUID v7). Time-based nadaje się do dowolnych tabel z created_at, ale wymaga dodatkowego przetwarzania duplikatów. Główna różnica: Keyset działa stabilnie przy dowolnych operacjach wstawiania, a Time-based jest wrażliwy na identyczne znaczniki czasu.
Wybór typu paginacji zależy od charakteru danych i wymagań dotyczących doświadczenia użytkownika. Poniżej znajdują się zalecenia dla typowych scenariuszy w rozwoju mobilnym. Uniwersalnego rozwiązania nie ma — każda metoda ma obszar zastosowania, w którym jest optymalna.
Biblioteka Android Paging 3 obsługuje wszystkie typy paginacji przez PagingSource. Dla Offset — PagingSource z kluczem Int (page), dla Cursor — z kluczem String lub Long (cursor). PagingSource automatycznie zarządza ładowaniem, buforowaniem i ponownymi próbami przy błędach.
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
}
}
}
Rozmiar strony wpływa na szybkość ładowania i postrzeganą wydajność. Dla aplikacji mobilnych optymalny zakres to 10-25 elementów na stronę. Mniej niż 10 — zbyt częste zapytania do API i szarpany skrol. Więcej niż 25 — długie ładowanie pierwszej porcji na wolnych sieciach.
Dla obrazków i wideo rozmiar strony zmniejsza się do 5-10, ponieważ każdy element wymaga dodatkowego czasu na ładowanie mediów. Dla list z elementami tekstowymi (komentarze, logi) rozmiar można zwiększyć do 30-50 rekordów. Zaleca się, aby rozmiar strony był konfigurowalny przez API, aby klient mógł dostosować się do różnych warunków sieciowych.
Często zadawane pytania
Paginacja — to ładowanie danych częściami, a nie wszystkich naraz. Jak w książce: czytasz jedną stronę, potem przewracasz na następną. W aplikacji oznacza to, że podczas przewijania listy ładuje się następna porcja danych, a nie cała lista naraz, co oszczędza transfer i pamięć.
Offset liczy rekordy: „pomiń 20, zwróć następne 10„. Jeśli między ładowaniami doda się nowy rekord — numeracja się rozjeżdża. Cursor używa unikalnego identyfikatora ostatniego rekordu: „zwróć 10 rekordów po ID = 100„. Nowe rekordy nie wpływają na pozycję.
Dla aplikacji mobilnych optymalnie 10-25 elementów. Dla list z obrazkami — 5-10, dla kanałów tekstowych — 20-30. Rozmiar zależy od średniego rozmiaru jednego elementu: im cięższy element, tym mniejsza powinna być strona dla szybkiego wyświetlenia.
Użyj biblioteki Paging 3 z Android Jetpack. Zapewnia ona PagingSource do ładowania, PagingData dla strumienia reaktywnego i PagingDataAdapter do automatycznego doładowywania podczas przewijania. Biblioteka obsługuje paginację Offset, Cursor i Keyset przez niestandardowy PagingSource.
Nieskończony skrol — to wzorzec UI, w którym nowa porcja danych ładuje się automatycznie przy zbliżaniu się do końca listy. Paginacja — to mechanizm ładowania danych porcjami, a nieskończony skrol — to jeden ze sposobów jego wyświetlania. Alternatywą jest przycisk „Załaduj więcej„.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również