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 de cómo funciona esta biblioteca, prueba el codelab y las muestras 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 componente clave de la biblioteca de Paging es la clase PagedList, que carga fragmentos de los datos de tu app o páginas. A medida que se necesitan más datos, se paginan en el objeto PagedList existente. Si cambia algún dato cargado, se emite una nueva instancia de PagedList al contenedor de datos observable desde un objeto basado en LiveData o RxJava2. A medida que se generan los objetos PagedList, la IU de tu app presenta su contenido, respetando los ciclos de vida de los controladores de la 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 una instantánea actualizada de los datos de tu app desde su objeto DataSource correspondiente. Los datos fluyen desde el backend o la base de datos de tu app hacia el objeto PagedList.

En el siguiente ejemplo, se usa la biblioteca de persistencias Room para organizar los datos de tu app, pero, si quieres almacenarlos con otro medio, también puedes proporcionar tu propia fábrica de fuentes 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 funcionan juntas para recuperar y mostrar el contenido a medida que se carga, recuperar previamente el contenido que no está a la vista y animar 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.

Flujo de datos en las arquitecturas compatibles de la biblioteca de Paging
Figura 1. Cómo fluyen los datos a través 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 para cargar información en tu propio objeto DataSource personalizado.

Solo de base de datos

Configura tu RecyclerView para observar el almacenamiento local, preferentemente con la biblioteca de persistencias Room. De esa manera, cada vez que se inserten o modifiquen datos en la base de datos de tu app, estos cambios se reflejarán automáticamente en el RecyclerView que muestra estos datos.

Red y base de datos

Después de que comiences a observar la base de datos, puedes escuchar cuando se quede sin datos con PagedList.BoundaryCallback. Luego, puedes recuperar más elementos de tu red y, luego, insertarlos en la base de datos. Si tu IU observa la base de datos, eso es todo lo que necesitas 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 usas una funcionalidad personalizada para cargar pequeños subconjuntos de datos desde la fuente de datos de tu app, puedes reemplazar esta lógica por la de la clase PagedList. Las instancias de PagedList ofrecen conexiones integradas a fuentes de datos comunes. Estas instancias también proporcionan adaptadores para los objetos RecyclerView que podrías 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 respaldo para el adaptador de tu IU, considera observar las actualizaciones de datos con una clase PagedList si la cantidad de elementos de la lista puede ser 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, reemplazar un objeto List por un objeto PagedList en tu app no requiere ningún cambio en la estructura de la IU de la app ni en la lógica de actualización de datos.

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

Es posible que tu app use un CursorAdapter para asociar datos de un Cursor con un ListView. En ese caso, por lo general, debes migrar de un ListView a un RecyclerView como contenedor de la IU de la lista de tu app y, luego, reemplazar el componente Cursor por 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 que se cargan en ese adaptador y los muestra. En estas situaciones, cambia el tipo de datos de tu adaptador a LiveData<PagedList> y, luego, une esta lista en un objeto ArrayAdapter antes de intentar que una clase de biblioteca 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 forma asíncrona, la biblioteca de Paging simplifica la carga de datos:

  • 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 conciertos en la base de datos, el contenido de RecyclerView se actualiza de forma 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)
        // 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 solución basada en LiveData.

Enviar 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