Dominando el Paging 3 en Android: Carga Paginada Eficiente con Jetpack Compose y Room
En este tutorial completo descubrirás cómo integrar la librería Paging 3 de Android junto con Jetpack Compose y bases de datos locales utilizando Room. Diseñaremos una arquitectura moderna capaz de manejar miles de registros sin agotar la memoria ni congelar la interfaz de usuario.
🚀 Introducción a Paging 3 en el Desarrollo Android Moderno
El manejo de grandes volúmenes de datos es uno de los desafíos más comunes en el desarrollo móvil. Si tu aplicación necesita mostrar listas interminables de elementos (como feeds de redes sociales, catálogos de productos o historiales de transacciones), cargar toda la información de golpe desde una API o una base de datos local es una pésima idea. Esto provoca un consumo excesivo de memoria, bloqueos en el hilo principal y, en última instancia, una experiencia de usuario frustrante.
Para solucionar este problema, Google introdujo la librería Paging 3, una herramienta potente y flexible dentro del ecosistema de Jetpack que facilita la carga gradual de datos en fragmentos (páginas). Combinada con Jetpack Compose y Room, Paging 3 nos permite construir interfaces fluidas, reactivas y altamente optimizadas.
🛠️ ¿Qué Vamos a Construir?
A lo largo de este tutorial, implementaremos una aplicación de catálogo de productos que obtiene datos de una API remota, los almacena y caché localmente utilizando Room, y los muestra en una pantalla construida con Jetpack Compose mediante una lista infinita con indicador de carga y manejo de estados de error.
Características Principales:
- Arquitectura limpia basada en capas (Data, Domain, Presentation).
- Integración de Retrofit con PagingSource.
- Estrategia de caché local con Room y
RemoteMediator(opcional pero recomendada). - Renderizado eficiente con LazyColumn en Jetpack Compose.
- Manejo elegante de estados de carga, reintentos y errores.
📦 Paso 1: Configuración de Dependencias
Antes de escribir código, debemos añadir las dependencias necesarias en nuestro archivo build.gradle.kts (nivel de módulo). Asegúrate de tener habilitadas las versiones estables más recientes.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id(
"com.google.devtools.ksp"
) // Requerido para Room
}
dependencies {
// Paging 3
implementation("androidx.paging:paging-runtime:3.2.1")
implementation("androidx.paging:paging-compose:3.2.1")
// Room
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.room:room-paging:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// Jetpack Compose
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
}
🔄 Paso 2: Creación del PagingSource para la Red
El núcleo de Paging 3 es el componente PagingSource. Este se encarga de definir cómo y dónde obtener los datos de forma paginada. Vamos a crear un ProductPagingSource que interactúe con nuestro servicio de Retrofit.
Implementemos el código de la fuente de datos:
import androidx.paging.PagingSource
import androidx.paging.PagingState
class ProductPagingSource(
private val apiService: ProductApiService
) : PagingSource<Int, ProductModel>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ProductModel> {
val page = params.key ?: 1
return try {
val response = apiService.getProducts(page = page, size = params.loadSize)
LoadResult.Page(
data = response.products,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.products.isEmpty()) null else page + 1
)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, ProductModel>): Int? {
return state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1)
?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
}
Explicación de los Métodos Clave:
load(): Se ejecuta cada vez que se necesita una nueva página. RecibeLoadParamsque contiene la clave actual (page) y el tamaño de carga (loadSize). Devuelve un objetoLoadResult.PageoLoadResult.Error.getRefreshKey(): Determina qué página cargar cuando los datos se actualizan (por ejemplo, mediante un gesto de Pull-to-Refresh).
💾 Paso 3: Configuración de Room y Paging
Para aplicaciones robustas, es fundamental ofrecer soporte offline. Room soporta de forma nativa la paginación devolviendo un PagingSource directamente desde las consultas DAO.
Definamos nuestra entidad y el DAO:
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "products")
data.class ProductEntity(
@PrimaryKey val id: Int,
val title: String,
val description: String,
val price: Double,
val imageUrl: String
)
Ahora creamos el DAO con una consulta que retorna un PagingSource:
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface ProductDao {
@Query("SELECT * FROM products")
fun pagingSource(): PagingSource<Int, ProductEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(products: List<ProductEntity>)
@Query("DELETE FROM products")
suspend fun clearAll()
}
📱 Paso 4: Configurando el ViewModel
El ViewModel actúa como puente entre la fuente de datos y la interfaz de usuario. Configuraremos un flujo Flow<PagingData<ProductModel>> utilizando la clase Pager.
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.flow.Flow
class ProductViewModel(
private val apiService: ProductApiService
) : ViewModel() {
val productsFlow: Flow<PagingData<ProductModel>> = Pager(
config = PagingConfig(
pageSize = 20,
prefetchDistance = 5,
enablePlaceholders = false
),
pagingSourceFactory = { ProductPagingSource(apiService) }
).flow.cachedIn(viewModelScope)
}
Parámetros de PagingConfig:
pageSize: Número de elementos cargados en cada página.prefetchDistance: Qué tan cerca del final de la lista debe estar el usuario para que comience a cargar la siguiente página.enablePlaceholders: Si se deben mostrar elementos vacíos mientras se cargan los datos reales.cachedIn(viewModelScope): Mantiene el flujo activo en el ViewModel para evitar recargas innecesarias durante cambios de configuración (como rotaciones de pantalla).
🎨 Paso 5: Interfaz de Usuario con Jetpack Compose
Llegó el momento de mostrar los datos en pantalla utilizando Jetpack Compose. Usaremos la extensión collectAsLazyPagingItems() para consumir el flujo de Paging dentro de un LazyColumn.
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
@Composable
fun ProductScreen(viewModel: ProductViewModel) {
val lazyPagingItems: LazyPagingItems<ProductModel> = viewModel.productsFlow.collectAsLazyPagingItems()
Scaffold(
topBar = {
TopAppBar(title = { Text("Catálogo Paginado") })
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(lazyPagingItems.itemCount) { index ->
val product = lazyPagingItems[index]
if (product != null) {
ProductItem(product = product)
}
}
// Manejo de estados de carga al final de la lista
lazyPagingItems.apply {
when {
loadState.append is LoadState.Loading -> {
item {
Box(modifier = Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
}
loadState.append is LoadState.Error -> {
val e = loadState.append as LoadState.Error
item {
ErrorItem(message = e.error.localizedMessage ?: "Error desconocido") {
retry()
}
}
}
}
}
}
// Manejo de carga inicial
if (lazyPagingItems.loadState.refresh is LoadState.Loading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
}
}
}
@Composable
fun ProductItem(product: ProductModel) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = product.title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(4.dp))
Text(text = product.description, style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.height(8.dp))
Text(text = "$${product.price}", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.primary)
}
}
}
@Composable
fun ErrorItem(message: String, onRetry: () -> Unit) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = message, color = MaterialTheme.colorScheme.error)
Button(onClick = onRetry) {
Text("Reintentar")
}
}
}
🔍 Preguntas Frecuentes (FAQ)
¿Cómo puedo implementar un buscador con Paging 3?
Para implementar una barra de búsqueda, puedes pasar el término de búsqueda como parámetro a tu repositorio y recrear el PagingSource cada vez que el texto cambie utilizando operadores de Kotlin Flow comoflatMapLatest.
¿Es obligatorio usar Room para implementar Paging 3?
No, Paging 3 puede trabajar directamente con fuentes de red (Retrofit), bases de datos (Room) o colecciones estáticas en memoria (Listas).🏁 Conclusión
Has aprendido a implementar con éxito la librería Paging 3 en Android utilizando Jetpack Compose y Room. Esta combinación te garantiza una arquitectura escalable, un rendimiento excelente con grandes volúmenes de datos y una experiencia de usuario fluida y libre de interrupciones.
Tutoriales relacionados
- ¡Poder en tus Manos! Integrando Haptics Avanzados y Vibración en Android Nativointermediate10 min
- ¡Libera la Personalización! Construyendo Vistas Personalizadas Robustas en Android con Canvas y Atributos XMLintermediate20 min
- ¡Poder y Flexibilidad! Construyendo Componentes Reutilizables en Android con Jetpack Compose y Modificadores Personalizadosintermediate25 min
- Almacenamiento Seguro de Datos en Android: SharedPreferences Cifradas y Jetpack DataStoreintermediate20 min
- Carga de Datos Offline en Android: Implementando una Estrategia Robusta con Room y WorkManagerintermediate25 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!