tutoriales.com

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.

Intermedio10 min de lectura16 views
Reportar error

🚀 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.

💡 Consejo: Paging 3 está diseñado desde cero utilizando Kotlin Flows y corrutinas, lo que encaja perfectamente con las arquitecturas modernas basadas en MVVM y flujos reactivos.

🛠️ ¿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")
}
📌 Nota: Usamos KSP (Kotlin Symbol Processing) en lugar de KAPT para compilar las anotaciones de Room, ya que es significativamente más rápido en proyectos modernos con Kotlin.

🔄 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.

Repositorio PagingSource Retrofit API Servidor Web Inicia carga Página X HTTP GET JSON / Datos Lista de Elementos PagingData Solicitud Respuesta

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. Recibe LoadParams que contiene la clave actual (page) y el tamaño de carga (loadSize). Devuelve un objeto LoadResult.Page o LoadResult.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()
}
⚠️ Advertencia: Al usar Room con Paging 3, asegúrate de manejar las transacciones correctamente si implementas un `RemoteMediator` para sincronizar red y base de datos local.

📱 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.

Scaffold (Estructura Base) Content (Contenedor Principal) LazyColumn (Lista Optimizada) LazyPagingItems (Gestión de Datos) ProductItemCard: Producto #1 ProductItemCard: Producto #2 ProductItemCard: Producto #3 Cargando más elementos... (Footer State)
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 como flatMapLatest.
¿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.

🔥 Importante: Recuerda siempre gestionar adecuadamente los estados de error y los reintentos en tu interfaz de usuario para garantizar una experiencia resiliente ante fallos de red.

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!