Descripción general de la biblioteca de paginación Parte de Android Jetpack

La biblioteca de paginación te ayuda a cargar y mostrar pequeños fragmentos de datos a la vez. La carga de datos parciales a pedido reduce el uso del ancho de banda de la red y los recursos del sistema.

En esta guía, se proporcionan varios ejemplos conceptuales de la biblioteca, junto con una descripción general de cómo funciona. Para ver ejemplos completos del funcionamiento de esta biblioteca, prueba el codelab y los ejemplos de la sección de recursos adicionales.

Arquitectura de biblioteca

En esta sección, se describen y se muestran los componentes principales de la biblioteca de paginación.

PagedList

El componente clave de la biblioteca de paginación es la clase PagedList, que carga fragmentos de datos o páginas de tu app. A medida que se necesitan más datos, se paginan en el objeto PagedList existente. Si algún dato cargado cambia, se emite una nueva instancia de PagedList al propietario de datos observable desde un objeto LiveData o basado en RxJava2. A medida que se generan los objetos PagedList, la IU de la app presenta su contenido y respeta los ciclos de vida de los controladores de IU.

En el siguiente fragmento de código, se muestra cómo puedes configurar el modelo de vista de tu app para cargar y presentar datos mediante un propietario LiveData de objetos PagedList:

Kotlin

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }
    

Java

    public class ConcertViewModel extends ViewModel {
        private ConcertDao concertDao;
        public final LiveData<PagedList<Concert>> concertList;

        // Creates a PagedList object with 50 items per page.
        public ConcertViewModel(ConcertDao concertDao) {
            this.concertDao = concertDao;
            concertList = new LivePagedListBuilder<>(
                    concertDao.concertsByDate(), 50).build();
        }
    }
    

Datos

Cada instancia de PagedList carga una instantánea actualizada de los datos de tu app desde el objeto DataSource correspondiente. Los datos fluyen desde el backend o la base de datos de la app al objeto PagedList.

En el ejemplo siguiente, se usa la biblioteca de Room Persistence para organizar los datos de tu app. Sin embargo, si deseas almacenar tus datos por otros medios, también puedes proporcionar tu propia fuente de datos.

Kotlin

    @Dao
    interface ConcertDao {
        // The Int type parameter tells Room to use a PositionalDataSource object.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        fun concertsByDate(): DataSource.Factory<Int, Concert>
    }
    

Java

    @Dao
    public interface ConcertDao {
        // The Integer type parameter tells Room to use a
        // PositionalDataSource object.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        DataSource.Factory<Integer, Concert> concertsByDate();
    }
    

Para obtener más información sobre cómo puedes cargar datos en objetos PagedList, consulta la guía sobre cómo cargar datos paginados.

IU

La clase PagedList funciona con un PagedListAdapter para cargar elementos en una RecyclerView. Estas clases trabajan juntas para buscar y mostrar contenido a medida que se carga, ya que precargan contenido fuera de la vista y animan cambios de contenido.

Para obtener más información, consulta la guía sobre cómo mostrar listas paginadas.

Admite diferentes arquitecturas de datos

La biblioteca de paginación admite las siguientes arquitecturas de datos:

  • Publicada solo desde un servidor de backend.
  • Almacenada solo en una base de datos en el dispositivo.
  • Una combinación de las otras fuentes usando la base de datos en el dispositivo como caché.

En la figura 1, se muestra cómo fluyen los datos en cada uno de estos escenarios de arquitectura. En el caso de una solución solo de red o de base de datos, los datos fluyen directamente al modelo de IU de tu app. Si usas un enfoque combinado, los datos fluyen desde el servidor de backend a una base de datos en el dispositivo y, luego, al modelo de IU de tu app. De vez en cuando, el extremo de cada flujo de datos se queda sin datos para cargar. En ese momento, se solicitan más datos del componente que proporcionó los datos. Por ejemplo, cuando una base de datos en el dispositivo se queda sin datos, solicita más al servidor.

Diagramas de flujos de datos
Figura 1: Cómo fluyen los datos por medio de cada una de las arquitecturas que admite la biblioteca de paginación

En el resto de esta sección, se proporcionan recomendaciones para configurar cada caso de uso del flujo de datos.

Solo de red

Para mostrar datos de un servidor de backend, usa la versión síncrona de la API de Retrofit a fin de cargar información en tu propio objeto DataSource personalizado.

Solo de base de datos

Configura tu RecyclerView para observar el almacenamiento local. Preferentemente, usa la biblioteca de Room Persistence. De esa manera, cada vez que se insertan o se modifican datos en la base de datos de la app, estos cambios se reflejan de forma automática en la RecyclerView que muestra los datos.

Red y base de datos

Después de comenzar a observar la base de datos, puedes escuchar cuándo la base de datos no tiene datos utilizando PagedList.BoundaryCallback. Luego, puedes buscar más elementos de la red e insertarlos en la base de datos. Si tu IU está observando la base de datos, eso es todo lo que tienes que hacer.

Maneja errores de red

Cuando usas una red para buscar o paginar los datos que muestras usando la biblioteca de paginación, es importante no tratar la red como "disponible" o "no disponible" todo el tiempo, ya que muchas conexiones son intermitentes o inestables:

  • Es posible que un servidor en particular no responda a una solicitud de red.
  • El dispositivo puede estar conectado a una red lenta o débil.

En cambio, tu app debe verificar cada solicitud para ver si hubo fallas y recuperarse de la mejor manera posible en los casos en que la red no esté disponible. Por ejemplo, puedes proporcionar un botón de "reintentar" para que los usuarios seleccionen si el paso de actualización de datos no funciona. Si se produce un error en el paso de paginación de datos, es mejor volver a intentar las solicitudes de paginación automáticamente.

Actualiza tu app existente

Si tu app ya consume datos de una base de datos o una fuente de backend, es posible actualizar directamente a la funcionalidad que proporciona la biblioteca de paginación. En esta sección, se muestra cómo actualizar una app que tiene un diseño común existente.

Soluciones de paginación personalizadas

Si utilizas una funcionalidad personalizada para cargar pequeños subconjuntos de datos desde la fuente de datos de la app, puedes reemplazar esta lógica con la de la clase PagedList. Las instancias de PagedList ofrecen conexiones integradas a fuentes de datos comunes. Estas instancias también proporcionan adaptadores para objetos RecyclerView que puedes incluir en la IU de tu app.

Datos cargados usando listas en lugar de páginas

Si usas una lista en la memoria como la estructura de datos de copia de seguridad para el adaptador de la IU, considera realizar las actualizaciones de datos usando una clase PagedList en caso de que el número de elementos en la lista sea grande. Las instancias de PagedList pueden usar LiveData<PagedList> u Observable<List> para pasar actualizaciones de datos a la IU de tu app, lo que minimiza los tiempos de carga y el uso de memoria. Mejor aún, el reemplazo de un objeto List con un objeto PagedList en tu app no requiere ningún cambio en la estructura de la IU o la lógica de actualización de datos de tu app.

Asocia un cursor de datos con una vista de lista usando CursorAdapter

Es posible que tu app use un CursorAdapter para asociar datos de un Cursor con una ListView. En ese caso, generalmente necesitas migrar de una ListView a una RecyclerView como el contenedor de la IU de la lista de la app y, luego, reemplazar el componente Cursor con Room o PositionalDataSource, dependiendo de si las instancias de Cursor acceden a una base de datos SQLite.

En algunas situaciones, como cuando trabajas con instancias de Spinner, solo proporcionas el adaptador. Luego, una biblioteca toma los datos cargados en ese adaptador y los muestra por ti. En esas situaciones, cambia el tipo de datos del adaptador a LiveData<PagedList> y, luego, envuelve esa lista en un objeto ArrayAdapter antes de intentar que una clase de biblioteca los aumente en una IU.

Carga contenido de forma asíncrona utilizando AsyncListUtil

Si usas objetos AsyncListUtil para cargar y mostrar grupos de información de manera asíncrona, la biblioteca de paginación te permite cargar datos más fácilmente:

  • No es necesario que los datos sean posicionales. La biblioteca de paginación te permite cargar datos directamente desde tu backend mediante las claves que proporciona la red.
  • Los datos pueden ser infinitamente grandes. Con la biblioteca de paginación, puedes cargar datos en las páginas hasta que no queden datos.
  • Los datos se pueden observar con más facilidad. La biblioteca de paginación puede presentar los datos que tiene el ViewModel de tu app en una estructura de datos observable.

Ejemplos de bases de datos

En los siguientes fragmentos de código, se muestran varias formas posibles de hacer que todas las piezas funcionen juntas.

Cómo observar datos paginados usando LiveData

En el siguiente fragmento de código, se muestran todas las piezas en funcionamiento. A medida que se agregan, quitan o cambian eventos de concierto en la base de datos, el contenido de RecyclerView se actualiza de manera automática y eficiente:

Kotlin

    @Dao
    interface ConcertDao {
        // The Int type parameter tells Room to use a PositionalDataSource
        // object, with position-based loading under the hood.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        fun concertsByDate(): DataSource.Factory<Int, Concert>
    }

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: LiveData<PagedList<Concert>> =
                concertDao.concertsByDate().toLiveData(pageSize = 50)
    }

    class ConcertActivity : AppCompatActivity() {
        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            val recyclerView = findViewById(R.id.concert_list)
            val adapter = ConcertAdapter()
            viewModel.livePagedList.observe(this, PagedList(adapter::submitList))
            recyclerView.setAdapter(adapter)
        }
    }

    class ConcertAdapter() :
            PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
        fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
            val concert: Concert? = getItem(position)

            // Note that "concert" is a placeholder if it's null.
            holder.bindTo(concert)
        }

        companion object {
            private val DIFF_CALLBACK = object :
                    DiffUtil.ItemCallback<Concert>() {
                // Concert details may have changed if reloaded from the database,
                // but ID is fixed.
                override fun areItemsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert.id == newConcert.id

                override fun areContentsTheSame(oldConcert: Concert,
                        newConcert: Concert) = oldConcert == newConcert
            }
        }
    }
    

Java

    @Dao
    public interface ConcertDao {
        // The Integer type parameter tells Room to use a PositionalDataSource
        // object, with position-based loading under the hood.
        @Query("SELECT * FROM concerts ORDER BY date DESC")
        DataSource.Factory<Integer, Concert> concertsByDate();
    }

    public class ConcertViewModel extends ViewModel {
        private ConcertDao concertDao;
        public final LiveData<PagedList<Concert>> concertList;

        public ConcertViewModel(ConcertDao concertDao) {
            this.concertDao = concertDao;
            concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50).build();
        }
    }

    public class ConcertActivity extends AppCompatActivity {
        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ConcertViewModel viewModel =
                    ViewModelProviders.of(this).get(ConcertViewModel.class);
            RecyclerView recyclerView = findViewById(R.id.concert_list);
            ConcertAdapter adapter = new ConcertAdapter();
            viewModel.concertList.observe(this, adapter::submitList);
            recyclerView.setAdapter(adapter);
        }
    }

    public class ConcertAdapter
            extends PagedListAdapter<Concert, ConcertViewHolder> {
        protected ConcertAdapter() {
            super(DIFF_CALLBACK);
        }

        @Override
        public void onBindViewHolder(@NonNull ConcertViewHolder holder,
                int position) {
            Concert concert = getItem(position);
            if (concert != null) {
                holder.bindTo(concert);
            } else {
                // Null defines a placeholder item - PagedListAdapter automatically
                // invalidates this row when the actual object is loaded from the
                // database.
                holder.clear();
            }
        }

        private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
                new DiffUtil.ItemCallback<Concert>() {
            // Concert details may have changed if reloaded from the database,
            // but ID is fixed.
            @Override
            public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
                return oldConcert.getId() == newConcert.getId();
            }

            @Override
            public boolean areContentsTheSame(Concert oldConcert,
                    Concert newConcert) {
                return oldConcert.equals(newConcert);
            }
        };
    }
    

Cómo observar datos paginados usando RxJava2

Si prefieres usar RxJava2 en lugar de LiveData, puedes crear un objeto Observable o Flowable:

Kotlin

    class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
        val concertList: Observable<PagedList<Concert>> =
                concertDao.concertsByDate().toObservable(pageSize = 50)
    }
    

Java

    public class ConcertViewModel extends ViewModel {
        private ConcertDao concertDao;
        public final Observable<PagedList<Concert>> concertList;

        public ConcertViewModel(ConcertDao concertDao) {
            this.concertDao = concertDao;

            concertList = new RxPagedListBuilder<>(
                    concertDao.concertsByDate(), /* page size */ 50)
                            .buildObservable();
        }
    }
    

Luego, puedes comenzar a observar los datos y dejar de hacerlo utilizando el código en el siguiente fragmento:

Kotlin

    class ConcertActivity : AppCompatActivity() {
        private val adapter: ConcertAdapter()
        private lateinit var viewModel: ConcertViewModel

        private val disposable = CompositeDisposable()

        public override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val recyclerView = findViewById(R.id.concert_list)
            viewModel = ViewModelProviders.of(this)
                    .get<ConcertViewModel>()
            recyclerView.setAdapter(adapter)
        }

        override fun onStart() {
            super.onStart()
            disposable.add(viewModel.concertList
                    .subscribe(adapter::submitList)))
        }

        override fun onStop() {
            super.onStop()
            disposable.clear()
        }
    }
    

Java

    public class ConcertActivity extends AppCompatActivity {
        private ConcertAdapter adapter = new ConcertAdapter();
        private ConcertViewModel viewModel;

        private CompositeDisposable disposable = new CompositeDisposable();

        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            RecyclerView recyclerView = findViewById(R.id.concert_list);

            viewModel = ViewModelProviders.of(this).get(ConcertViewModel.class);
            recyclerView.setAdapter(adapter);
        }

        @Override
        protected void onStart() {
            super.onStart();
            disposable.add(viewModel.concertList
                    .subscribe(adapter.submitList(flowableList)
            ));
        }

        @Override
        protected void onStop() {
            super.onStop();
            disposable.clear();
        }
    }
    

El código de ConcertDao y ConcertAdapter es el mismo para una solución basada en RxJava2 que para una solución basada en LiveData.

Envía comentarios

Comparte tus comentarios e ideas con nosotros por medio de estos recursos:

Seguimiento de problemas
Informa los problemas para que podamos solucionar los errores.

Recursos adicionales

Para obtener más información sobre la biblioteca de paginación, consulta los siguientes recursos.

Ejemplos

Codelabs

Videos