Descripción general de la biblioteca de Paging 2 Parte de Android Jetpack

La biblioteca de Paging 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.

Configuración

Si quieres importar componentes de Paging a tu app para Android, agrega las siguientes dependencias al archivo build.gradle de tu app:

Groovy

dependencies {
  def paging_version = "2.1.2"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  implementation("androidx.paging:paging-runtime:$paging_version") // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation("androidx.paging:paging-common:$paging_version") // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation("androidx.paging:paging-rxjava2:$paging_version") // For Kotlin use paging-rxjava2-ktx
}

Arquitectura de biblioteca

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

PagedList

El principal componente de la biblioteca de Paging es la clase PagedList, que carga fragmentos de los 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 contenedor de datos observable desde un objeto LiveData o uno 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 tus 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 contenedor de 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 un resumen actualizado 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 persistencias Room para organizar los datos de tu app. Sin embargo, si deseas almacenar los 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 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 un RecyclerView. Estas clases trabajan juntas para recuperar y mostrar contenido a medida que se carga, y proporcionan una carga previa del contenido fuera de la vista y animan los 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, solicita más datos al componente que los proporcionó. 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: manera en que fluyen los datos por medio de cada una de las arquitecturas que admite la biblioteca de Paging

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. De preferencia, usa la biblioteca de persistencias Room. De esa manera, cada vez que se insertan o se modifican datos en la base de datos de la app, estos cambios se reflejan automáticamente 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 recuperar 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 recuperar o paginar los datos que muestras por medio de la biblioteca de Paging, 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 Paging. 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.

Uso de listas en lugar de páginas para la carga de datos

Si usas una lista en la memoria como la estructura de datos de copia de seguridad para el adaptador de la IU, considera observar las actualizaciones de datos por medio de una clase PagedList en caso de que el número de elementos de la lista se vuelva grande. Las instancias de PagedList pueden usar LiveData<PagedList> o 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 ni en la lógica de actualización de datos de tu app.

Usa CursorAdapter para asociar un cursor de datos con una vista de lista

Es posible que tu app use CursorAdapter para asociar datos de un Cursor con un ListView. En ese caso, generalmente necesitas migrar de una ListView a una RecyclerView como el contenedor de IU de la lista de la app y, luego, reemplazar el componente Cursor con Room o PositionalDataSource, según 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, une esta lista en un objeto ArrayAdapter antes de intentar que una clase de biblioteca los aumente estos elementos en una IU.

Usa AsyncListUtil para cargar contenido de forma asíncrona

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

  • No es necesario que los datos sean posicionales. La biblioteca de Paging 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 Paging, puedes cargar datos en las páginas hasta que no queden más por cargar.
  • Los datos se pueden observar con más facilidad. La biblioteca de Paging 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.

Usa LiveData para observar datos paginados

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 eficiente y automáticamente:

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)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.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 =
                new ViewModelProvider(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);
        }
    };
}

Usa RxJava2 para observar datos paginados

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 del siguiente fragmento:

Kotlin

class ConcertActivity : AppCompatActivity() {
    private val adapter = ConcertAdapter()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        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 = new ViewModelProvider(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 basada en LiveData.

Envía comentarios

Usa estos recursos para compartir tus comentarios y tus ideas con nosotros:

Herramienta de seguimiento de errores
Informa los problemas para que podamos corregir los errores.

Recursos adicionales

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

Ejemplos

Codelabs

Videos