Bienvenido, soy Miguel y hoy les traigo un nuevo post.
Recientemente me entrevistaron para un puesto de desarrollo de software de Android, y me pidieron que codificara en vivo una aplicación simple que demuestre cómo obtener una lista de elementos de un recurso de red y mostrar su información en una lista interminable.
Me dieron una API que admite la paginación y me fui a codificar.
Hubo solo un problema.
Nunca había implementado este comportamiento antes, por lo tanto, pude codificar una aplicación que obtenga resultados de la API, pero no pude implementar la función de desplazamiento sin fin en el período de tiempo permitido.
Entonces, esto significa que ahora tengo un gran tema para una publicación de blog.
¿Qué es el desplazamiento sin fin?
¿Sabes cuando te desplazas por Facebook o Instagram y parece que la lista de publicaciones es tan grande que es posible que nunca llegues al final? Bueno, eso es porque es verdad.
Pero cargar grandes cantidades de datos y enviarlos a nuestra vista de reciclador definitivamente sería un problema.
Atascaría la memoria de las aplicaciones o el almacenamiento local, y sin duda sería perjudicial para el rendimiento.
¿Cómo podemos hacerlo mejor?
Podemos cargar una parte de los datos, luego, cuando hayamos terminado de desplazarnos, podemos cargar otra parte, ¿verdad? Suena bien.
Sin embargo, tendríamos que administrar esto, asegurarnos de no guardar datos inútiles, pero también mantener los datos cargados para el desplazamiento inverso, también tendríamos que administrar todas esas llamadas a la API y cancelarlas si es necesario, aplicar la lógica de subprocesos múltiples y mantener los alienígenas de alcanzar el interruptor de apagado en Alpha centaury …
Bueno, aparte de lo último de los extraterrestres, existe una solución sencilla para gestionar todos esos problemas.
Presentando la biblioteca de paginación de Android
La paginación o «paginación» es algo con lo que los desarrolladores móviles han luchado durante años, pero al igual que las redes y la simultaneidad, las bibliotecas recientes han simplificado el problema, lo que nos permite a los desarrolladores centrarnos en la lógica específica de nuestra aplicación, en lugar de perder nuestro tiempo escribiendo código repetitivo.
Primero, tengamos en cuenta que para usar la biblioteca de paginación de Android, tendríamos que trabajar «Reactively»
La programación reactiva es un concepto que nos permite trabajar de forma asincrónica, y esta entrada de blog requiere algunos conocimientos del paradigma reactivo. Hay algunas formas de trabajar de forma reactiva en Android, las principales son:
RxJava, LiveData y la nueva, aún experimental, Flow API.
La paginación de Android es compatible con los tres y, en este ejemplo, trabajaremos con Flow.
Aquí hay un diagrama que representa el flujo de datos desde el punto final (local o remoto) hasta la interfaz de usuario.
Primero, agregue la dependencia de la biblioteca de paginación:
dependencies { def paging_version = "3.0.0-alpha07" implementation "androidx.paging:paging-runtime:$paging_version" // alternatively - without Android dependencies for tests testImplementation "androidx.paging:paging-common:$paging_version" // optional - RxJava2 support implementation "androidx.paging:paging-rxjava2:$paging_version" // optional - Guava ListenableFuture support implementation "androidx.paging:paging-guava:$paging_version" }
Para obtener datos de un recurso de red, generalmente tenemos una interfaz dedicada que contiene los puntos finales de la API de backend y recupera datos de ella usando OKHttp, retroadaptación o una implementación personalizada. En mi proyecto de muestra, había usado la API de PixaBay.
interface PixaBayService { @GET("api/") suspend fun getPics( @Query("page") page: Int): PhotoList }
Entonces, lo primero que debemos hacer es envolver nuestro servicio con un PagingSource, la fuente tomará el servicio y cualquier otro parámetro que se requiera para la recuperación de datos (es decir, parámetros de consulta, tokens, identificaciones, etc.)
class PixaBayPagingSource( private val service: PixaBayService ) : PagingSource<Int, Photo>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Photo> { TODO - implement load }
la fuente tiene un método para implementar, el método load (), que devuelve LoadResult.
LoadResult es una clase sellada (similar a un Enum con valores asociados) que tiene dos tipos de clases de datos:
- Página. que contiene un fragmento de datos de la fuente y tiene 3 valores:
datos: los datos reales.
prevKey: el índice de la página anterior o nulo si esta es la primera página
nextKey: el índice de la página siguiente o nulo si esta es la última página.
LoadResult.Page( data = photos, prevKey = if (position == PIXABAY_STARTING_PAGE_INDEX) null else position, nextKey = if (photos.isEmpty()) null else position + 1 )
- Error. que representa cualquier tipo de error ocurrido durante el proceso de carga.
Así es como implementamos la clase pagingSource.
private val service: PixaBayService ) : PagingSource<Int, Photo>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Photo> { val position = params.key ?: PIXABAY_STARTING_PAGE_INDEX return try { val response = service.getPics(position) val photos = response.photos LoadResult.Page( data = photos, prevKey = if (position == PIXABAY_STARTING_PAGE_INDEX) null else position, nextKey = if (photos.isEmpty()) null else position + 1 ) } catch (exception: IOException) { return LoadResult.Error(exception) } catch (exception: HttpException) { return LoadResult.Error(exception) } } }
Entonces, como podemos ver, ¡podemos obtener la clave actual como un parámetro de la función de carga de forma gratuita! no hay necesidad de manejar eso.
Entonces, nuestra primera línea establece el número de página en el que estamos, y lo llamamos «posición», si el valor de params.key está vacío, estableceremos un índice inicial basado en el formato de índice API.
val position = params.key?: STARTING_PAGE_INDEX
Luego abrimos un bloque try catch para manejar la llamada de red y crear un LoadResult a partir de la respuesta que obtenemos de la red,
return try {
val response = service.getPics (position)
val photos = response.photos
LoadResult.Page (
data = photos,
prevKey = if (position == STARTING_PAGE_INDEX ) null else position,
nextKey = if (photos.isEmpty ()) null más posición + 1
)
}
Es importante tener en cuenta que esta api específica es compatible con la paginación, pero si no lo hace, debemos administrar eso nosotros mismos.
Y, por último, detecta los errores y devuelve un LoadResult.Error con la excepción.
catch (exception: IOException) { return LoadResult.Error(exception) } catch (exception: HttpException) { return LoadResult.Error(exception) }
Ahora que tenemos una fuente de paginación, necesitamos crear de alguna manera un flujo de datos a partir de ella.
¡Buscapersonas al rescate!
Ahora iríamos a nuestra clase de repositorio.
La clase de repositorio suele ser una capa que abstrae los datos de los servicios (servicio de red o base de datos local) a los viewModels y, en nuestro caso, emite un flujo de datos.
class PixaBayRepository(private val service: PixaBayService) { fun getPhotos(): PhotoList { return service.getPics(1) } }
Ahora, en lugar de exponer los resultados del servicio, necesitamos exponer un flujo de pagingData del tipo de resultados.
Tampoco llamamos directamente al servicio para obtener los datos, sino que creamos una instancia de Pager. esta clase tomará un PageConfig y el servicio, y nos proporcionará un flujo de pagingData del tipo que devuelve el servicio
class PixaBayRepository(private val service: PixaBayService) { companion object { private const val NETWORK_PAGE_SIZE = 10 } /** * Fetch photos, expose them as a stream of data that will emit * every time we get more data from the network. */ fun getPhotos(): Flow<PagingData<Photo>> { Log.d("PixaBayRepository", "New page") return Pager( config = PagingConfig( pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = true ), pagingSourceFactory = { PixaBayPagingSource(service) } ).flow } }
Tenga en cuenta que si no trabaja con flujo o desea trabajar con liveData o RxJava, puede convertir el flujo en datos observables, fluidos o en vivo en este punto. En este punto, tenemos un flujo de datos de paginación que contiene los valores que debería mostrar nuestra lista, ¿y ahora qué? Todo lo que tenemos que hacer es envolver nuestro adaptador de lista con un PagedListAdapter que expone un método suspendido importante: submitList () que toma una instancia de pagingData class PhotosAdapter : PagingDataAdapter<Photo, RecyclerView.ViewHolder>(PHOTO_COMPARATOR) { // Your implementation }
Toma una implementación de la clase DiffUtill.ItemCallback como parámetro, podemos agregar este comparador como un objeto complementario en la PhotosAdapter
clase
companion object { private val PHOTO_COMPARATOR = object : DiffUtil.ItemCallback<Photo>() { override fun areItemsTheSame(oldItem: Photo, newItem: Photo): Boolean = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: Photo, newItem: Photo): Boolean = oldItem == newItem } }
Entonces cubrimos todos los componentes principales del primer diagrama:
¿Pero dónde vamos a poner todas esas piezas?
Bueno, si ha seguido las recomendaciones de Android y las bibliotecas de jetpack, su diseño debería verse así:
Entonces, la transmisión comienza desde la interfaz de usuario, la vista recopilaría el flujo de fotos del viewModel y lo enviaría al adaptador.
viewModel.getPhotos (). collectLatest { adapter.submitData (it) }
Detrás de escena, el método sublimtList utiliza una corrutina para calcular la diferencia entre los datos nuevos y los datos antiguos y actualizar la lista cuando finaliza el cálculo.
Entonces lanzaríamos este bloque en el lifecycleScope
lifecycleScope.launch { viewModel.getPhotos (). collectLatest { adapter.submitData (it) } }
Todo el código de este ejemplo se puede ver en este repositorio de GitHub .
Añadir comentario