La paginación es una técnica de carga de datos por páginas, utilizada en aplicaciones móviles y servicios web para trabajar con grandes conjuntos de registros. Según la Documentación de Android Developers (2025), una implementación correcta de la paginación reduce la carga en la API, ahorra tráfico y mejora la experiencia del usuario. La carga por páginas permite que la aplicación muestre contenido gradualmente, sin esperar a que se carguen todos los datos.
Puntos clave
La paginación (del latín pagination — división en páginas) es una técnica que divide un gran conjunto de datos en porciones secuenciales (páginas). En aplicaciones móviles, la paginación se utiliza al cargar listas de mensajes, feeds de noticias, catálogos de productos, historial de pedidos y cualquier otra colección con un número potencialmente ilimitado de registros.
Sin paginación, la aplicación se ve obligada a cargar todos los datos a la vez, lo que provoca largas esperas, alto consumo de tráfico y un rendimiento inestable en dispositivos débiles. Una solicitud a la API con paginación devuelve solo una porción de datos e información meta para cargar la siguiente — de este modo, la aplicación controla la cantidad de información que recibe.
Las métricas principales de la paginación son: tamaño de página (page size) — cantidad de registros por página (generalmente 10–50), y número de página o cursor — un indicador de la posición actual en el conjunto. La elección del tamaño de página depende del tipo de datos: para elementos compactos (nombres) bastan 20–30, para tarjetas con imágenes — 10–15.
Los dispositivos móviles tienen recursos limitados: memoria RAM, velocidad de procesador y límites de tráfico. La paginación resuelve tres tareas clave: reducción del consumo de memoria (solo se almacenan en memoria los elementos visibles), aceleración de la primera visualización (la primera porción se carga más rápido que todo el conjunto) y ahorro de tráfico (los datos se cargan solo cuando el usuario desplaza la lista).
Existen cuatro tipos principales de paginación, cada uno resuelve tareas específicas. La elección del método depende de los requisitos de consistencia de datos, la arquitectura de la API, el tipo de almacenamiento y la complejidad aceptable de implementación en el cliente y el servidor.
| Tipo | Principio de funcionamiento | Estabilidad | Velocidad en grandes volúmenes |
|---|---|---|---|
| Offset | LIMIT + OFFSET en SQL | baja | decrece al aumentar OFFSET |
| Cursor | WHERE id > last_id | alta | estable (O(log n)) |
| Keyset | WHERE key > last_key | alta | estable (O(log n)) |
| Time-based | WHERE created_at < last_time | media | estable con índice |
La paginación Offset es adecuada para conjuntos de datos estáticos o que se actualizan con poca frecuencia, cuando la implementación simple es importante. Cursor y Keyset son para datos dinámicos con inserciones frecuentes. Time-based es para feeds cronológicos donde los registros se ordenan por hora de creación. El estándar GraphQL Relay utiliza la paginación por cursor como el único método recomendado.
La paginación Offset es el tipo más simple de carga por páginas. El cliente envía los parámetros page y limit (u offset y limit), y el servidor aplica SQL OFFSET y LIMIT. Por ejemplo, page=2, limit=20 devuelve los registros del 21 al 40. Este método es intuitivo y fácil de implementar en cualquier stack.
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
}
El principal inconveniente de la paginación Offset es el problema de registros perdidos y duplicados. Si se añaden nuevos registros a la tabla entre dos solicitudes, el OFFSET se desplaza: el usuario puede ver el mismo registro dos veces o perderse uno nuevo. Esto es crítico para feeds de noticias y chats donde la consistencia es importante.
Otro problema es la degradación del rendimiento con OFFSET grandes. La base de datos tiene que escanear y saltarse los primeros offset registros antes de devolver el resultado. Con offset=100000 incluso con LIMIT 20, el servidor dedicará un tiempo notable al escaneo. PostgreSQL y MySQL muestran una caída lineal de velocidad a medida que crece OFFSET.
La paginación Offset sigue siendo la mejor opción para: paneles de administración (los datos cambian raramente, se necesita navegación por páginas), informes y registros históricos (instantánea fija de datos), catálogos con filtros (se puede ir a cualquier página). Offset también es el más simple de implementar en el cliente — RecyclerView con Paging 3 lo soporta de serie.
La paginación Keyset utiliza una clave única (generalmente la clave primaria) para filtrar registros. En lugar de OFFSET, la consulta usa WHERE id > last_seen_id. Esto garantiza un rendimiento estable independientemente del número de registros y ausencia de duplicados en las inserciones, ya que los nuevos registros siempre tienen un id mayor.
-- Paginación Offset (problemática)
SELECT * FROM posts
ORDER BY id
LIMIT 20 OFFSET 100;
-- Paginación Keyset (estable)
SELECT * FROM posts
WHERE id > 100
ORDER BY id
LIMIT 20;
La paginación basada en tiempo (o cursor por tiempo) utiliza la marca de tiempo created_at para la navegación. El cliente envía la marca de tiempo del último registro cargado, y el servidor devuelve los registros creados antes o después de esa marca. Este método es popular en redes sociales y feeds de noticias donde el orden de los registros se determina por la hora de publicación.
Una característica de la paginación basada en tiempo es la posible aparición de duplicados si dos registros se crean en el mismo milisegundo. Para eliminar este problema, se combina una clave basada en tiempo con un id único: WHERE (created_at, id) < (last_time, last_id). Este cursor compuesto garantiza la unicidad de cada registro y un orden preciso.
La paginación Keyset requiere una columna con un valor único y monótonamente creciente (id autoincremental, UUID v7). La basada en tiempo es adecuada para cualquier tabla con created_at, pero requiere un manejo adicional de duplicados. La diferencia principal: Keyset funciona de forma estable ante cualquier operación de inserción, mientras que la basada en tiempo es sensible a marcas de tiempo idénticas.
La elección del tipo de paginación depende de la naturaleza de los datos y los requisitos de experiencia de usuario. A continuación se presentan recomendaciones para escenarios típicos en el desarrollo móvil. No existe una solución universal — cada método tiene un ámbito donde es óptimo.
La biblioteca Android Paging 3 soporta todos los tipos de paginación a través de PagingSource. Para Offset — PagingSource con clave Int (page), para Cursor — con clave String o Long (cursor). PagingSource gestiona automáticamente la carga, el almacenamiento en caché y los reintentos en caso de error.
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
}
}
}
El tamaño de página afecta la velocidad de carga y la percepción del rendimiento. Para aplicaciones móviles, el rango óptimo es 10–25 elementos por página. Menos de 10 genera demasiadas solicitudes a la API y un scroll entrecortado. Más de 25 provoca una carga lenta de la primera porción en redes lentas.
Para imágenes y vídeos, el tamaño de página se reduce a 5–10, ya que cada elemento requiere tiempo adicional para cargar el contenido multimedia. Para listas de texto (comentarios, registros), el tamaño puede aumentarse a 30–50 registros. Se recomienda hacer que el tamaño de página sea configurable a través de la API para que el cliente pueda adaptarse a diferentes condiciones de red.
Preguntas frecuentes
La paginación es la carga de datos en partes, no todo a la vez. Como en un libro: lees una página y luego pasas a la siguiente. En una aplicación, esto significa que al desplazarte por una lista, se carga el siguiente lote de datos, no toda la lista completa, lo que ahorra tráfico y memoria.
Offset cuenta registros: ”omite 20, devuelve los siguientes 10.” Si se añade un nuevo registro entre las cargas, la numeración se desvía. Cursor utiliza el identificador único del último registro: “devuelve 10 registros después del ID = 100.” Los nuevos registros no afectan la posición.
Para aplicaciones móviles, lo óptimo son 10–25 elementos. Para listas con imágenes — 5–10, para feeds de texto — 20–30. El tamaño depende del tamaño medio de cada elemento: cuanto más pesado sea el elemento, más pequeña debe ser la página para una visualización rápida.
Usa la biblioteca Paging 3 de Android Jetpack. Proporciona PagingSource para la carga, PagingData para flujos reactivos y PagingDataAdapter para la carga automática al desplazarse. La biblioteca soporta paginación Offset, Cursor y Keyset mediante un PagingSource personalizado.
El scroll infinito es un patrón de UI en el que se carga automáticamente un nuevo lote de datos al acercarse al final de la lista. La paginación es el mecanismo de carga de datos por lotes, y el scroll infinito es una forma de mostrarlo. Una alternativa es el botón “Cargar más”.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también