Cómo recopilar datos paginados

Esta guía se basa en la descripción general de la biblioteca de Paging y explica cómo personalizar la solución de carga de datos de tu app para satisfacer sus necesidades de arquitectura.

Crea una lista observable

Por lo general, el código de IU observa un objeto LiveData<PagedList> (o, si usas RxJava2, un Flowable<PagedList> o Observable<PagedList>), que reside en el ViewModel de tu app. Este objeto observable forma una conexión entre la presentación y el contenido de los datos de la lista de tu app.

Para crear uno de esos objetos observables de PagedList, pasa una instancia de DataSource.Factory a un objeto LivePagedListBuilder o RxPagedListBuilder. Un objeto DataSource carga páginas para un solo PagedList. La clase de fábrica crea instancias nuevas de PagedList en respuesta a actualizaciones de contenido, como invalidaciones de tablas de bases de datos y actualizaciones de red. La biblioteca de persistencias de Room puede proporcionarte objetos DataSource.Factory, aunque también puedes crear los tuyos.

En el siguiente fragmento de código, se muestra cómo crear una instancia de LiveData<PagedList> en la clase ViewModel usando las capacidades de compilación de DataSource.Factory de Room:

ConcertDao

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>
}

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();
}

ConcertViewModel

Kotlin

// The Int type argument corresponds to a PositionalDataSource object.
val myConcertDataSource : DataSource.Factory<Int, Concert> =
       concertDao.concertsByDate()

val concertList = myConcertDataSource.toLiveData(pageSize = 50)

Java

// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
       concertDao.concertsByDate();

LiveData<PagedList<Concert>> concertList =
        LivePagedListBuilder(myConcertDataSource, /* page size */ 50).build();

Define tu propia configuración de paginación

Si, además, quieres configurar una LiveData<PagedList> para casos avanzados, también puedes definir tu propia configuración de paginación. En especial, puedes definir los siguientes atributos:

  • Tamaño de la página: cantidad de elementos en cada página
  • Distancia de carga previa: dado el último elemento visible en la IU de una app, la cantidad de elementos más allá de ese último que la biblioteca de Paging debería intentar recuperar de antemano. Este valor debe ser varias veces mayor que el tamaño de la página.
  • Presencia del marcador de posición:: determina si la IU muestra marcadores de posición para los elementos de la lista que aún no terminaron de cargarse. Para obtener más información sobre los beneficios y las desventajas de usar marcadores de posición, consulta cómo proporcionar marcadores de posición en tu IU.

Si deseas tener más control sobre cuándo la biblioteca de Paging carga una lista desde la base de datos de la app, pasa un objeto Executor personalizado a LivePagedListBuilder, como se muestra en el siguiente fragmento de código:

ConcertViewModel

Kotlin

val myPagingConfig = Config(
        pageSize = 50,
        prefetchDistance = 150,
        enablePlaceholders = true
)

// The Int type argument corresponds to a PositionalDataSource object.
val myConcertDataSource : DataSource.Factory<Int, Concert> =
        concertDao.concertsByDate()

val concertList = myConcertDataSource.toLiveData(
        pagingConfig = myPagingConfig,
        fetchExecutor = myExecutor
)

Java

PagedList.Config myPagingConfig = new PagedList.Config.Builder()
        .setPageSize(50)
        .setPrefetchDistance(150)
        .setEnablePlaceholders(true)
        .build();

// The Integer type argument corresponds to a PositionalDataSource object.
DataSource.Factory<Integer, Concert> myConcertDataSource =
        concertDao.concertsByDate();

LiveData<PagedList<Concert>> concertList =
        new LivePagedListBuilder<>(myConcertDataSource, myPagingConfig)
            .setFetchExecutor(myExecutor)
            .build();

Elige el tipo de fuente de datos correcto

Es importante conectarse a la fuente de datos que mejor maneja la estructura de los datos de origen:

  • Usa PageKeyedDataSource si las páginas que cargas tienen incorporadas las teclas siguiente/anterior. Por ejemplo, si cargas publicaciones de redes sociales desde la red, es posible que debas pasar un token de nextPage de una carga a una otra posterior.
  • Usa ItemKeyedDataSource si necesitas usar datos del elemento N para recuperar el elemento N + 1. Por ejemplo, si quieres recuperar comentarios en conversaciones de una app de debate, es posible que debas pasar el ID del último comentario para obtener el contenido del siguiente.
  • Usa PositionalDataSource si necesitas recuperar páginas de datos desde cualquier ubicación que elijas en tu almacén de datos. Esta clase admite la solicitud de un conjunto de elementos de datos que comiencen desde la ubicación que selecciones. Por ejemplo, la solicitud podría mostrar los 50 elementos de datos y comenzar en la ubicación 1500.

Notifica cuando los datos no sean válidos

Cuando se usa la biblioteca de paginación, corresponde a la capa de datos notificar a las otras capas de tu app cuando una tabla o fila está inactiva. Para hacerlo, llama a invalidate() desde la clase DataSource que elegiste para tu app.

Crea tus propias fuentes de datos

Si usas una solución de datos local personalizada o si cargas datos directamente desde una red, puedes implementar una de las subclases de DataSource. En el siguiente fragmento de código, se muestra una fuente de datos que está desconectada de la hora de inicio de un concierto determinado:

Kotlin

class ConcertTimeDataSource() :
        ItemKeyedDataSource<Date, Concert>() {
    override fun getKey(item: Concert) = item.startTime

    override fun loadInitial(
            params: LoadInitialParams<Date>,
            callback: LoadInitialCallback<Concert>) {
        val items = fetchItems(params.requestedInitialKey,
                params.requestedLoadSize)
        callback.onResult(items)
    }

    override fun loadAfter(
            params: LoadParams<Date>,
            callback: LoadCallback<Concert>) {
        val items = fetchItemsAfter(
            date = params.key,
            limit = params.requestedLoadSize)
        callback.onResult(items)
    }
}

Java

public class ConcertTimeDataSource
        extends ItemKeyedDataSource<Date, Concert> {
    @NonNull
    @Override
    public Date getKey(@NonNull Concert item) {
        return item.getStartTime();
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Date> params,
            @NonNull LoadInitialCallback<Concert> callback) {
        List<Concert> items =
            fetchItems(params.key, params.requestedLoadSize);
        callback.onResult(items);
    }

    @Override
    public void loadAfter(@NonNull LoadParams<Date> params,
            @NonNull LoadCallback<Concert> callback) {
        List<Concert> items =
            fetchItemsAfter(params.key, params.requestedLoadSize);
        callback.onResult(items);
    }

Luego, puedes cargar estos datos personalizados en objetos PagedList creando una subclase concreta de DataSource.Factory. En el siguiente fragmento de código, se muestra cómo generar instancias nuevas de la fuente de datos personalizada definida en el fragmento de código anterior:

Kotlin

class ConcertTimeDataSourceFactory :
        DataSource.Factory<Date, Concert>() {
    val sourceLiveData = MutableLiveData<ConcertTimeDataSource>()
    var latestSource: ConcertDataSource?
    override fun create(): DataSource<Date, Concert> {
        latestSource = ConcertTimeDataSource()
        sourceLiveData.postValue(latestSource)
        return latestSource
    }
}

Java

public class ConcertTimeDataSourceFactory
        extends DataSource.Factory<Date, Concert> {
    private MutableLiveData<ConcertTimeDataSource> sourceLiveData =
            new MutableLiveData<>();

    private ConcertDataSource latestSource;

    @Override
    public DataSource<Date, Concert> create() {
        latestSource = new ConcertTimeDataSource();
        sourceLiveData.postValue(latestSource);
        return latestSource;
    }
}

Considera cómo funcionan las actualizaciones de contenido

A medida que construyes objetos PagedList observables, considera cómo funcionan las actualizaciones de contenido. Si cargas datos directamente desde una base de datos de Room, las actualizaciones se envían automáticamente a la IU de tu app.

Cuando se utiliza una API de red paginada, en general, tienes una interacción de usuario, como "deslizar para actualizar", que sirve como señal para invalidar el DataSource que usaste recientemente. Luego, solicitas una instancia nueva de esa fuente de datos. En el siguiente fragmento de código, se demuestra este comportamiento:

Kotlin

class ConcertActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        // ...
        concertTimeViewModel.refreshState.observe(this, Observer {
            // Shows one possible way of triggering a refresh operation.
            swipeRefreshLayout.isRefreshing =
                    it == MyNetworkState.LOADING
        })
        swipeRefreshLayout.setOnRefreshListener {
            concertTimeViewModel.invalidateDataSource()
        }
    }
}

class ConcertTimeViewModel(firstConcertStartTime: Date) : ViewModel() {
    val dataSourceFactory = ConcertTimeDataSourceFactory(firstConcertStartTime)
    val concertList: LiveData<PagedList<Concert>> =
            dataSourceFactory.toLiveData(
                pageSize = 50,
                fetchExecutor = myExecutor
            )

    fun invalidateDataSource() =
            dataSourceFactory.sourceLiveData.value?.invalidate()
}

Java

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        // ...
        viewModel.getRefreshState()
                .observe(this, new Observer<NetworkState>() {
            // Shows one possible way of triggering a refresh operation.
            @Override
            public void onChanged(@Nullable MyNetworkState networkState) {
                swipeRefreshLayout.isRefreshing =
                        networkState == MyNetworkState.LOADING;
            }
        };

        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshListener() {
            @Override
            public void onRefresh() {
                viewModel.invalidateDataSource();
            }
        });
    }
}

public class ConcertTimeViewModel extends ViewModel {
    private LiveData<PagedList<Concert>> concertList;
    private DataSource<Date, Concert> mostRecentDataSource;

    public ConcertTimeViewModel(Date firstConcertStartTime) {
        ConcertTimeDataSourceFactory dataSourceFactory =
                new ConcertTimeDataSourceFactory(firstConcertStartTime);
        mostRecentDataSource = dataSourceFactory.create();
        concertList = new LivePagedListBuilder<>(dataSourceFactory, 50)
                .setFetchExecutor(myExecutor)
                .build();
    }

    public void invalidateDataSource() {
        mostRecentDataSource.invalidate();
    }
}

Proporciona asignación de datos

La biblioteca de Paging admite transformaciones basadas en elementos y en páginas de elementos cargados por una DataSource.

En el siguiente fragmento de código, se mapea una combinación de nombre y fecha de concierto a una sola string que contiene tanto el nombre como la fecha:

Kotlin

class ConcertViewModel : ViewModel() {
    val concertDescriptions : LiveData<PagedList<String>>
        init {
            val concerts = database.allConcertsFactory()
                    .map { "${it.name} - ${it.date}" }
                    .toLiveData(pageSize = 50)
        }
}

Java

public class ConcertViewModel extends ViewModel {
    private LiveData<PagedList<String>> concertDescriptions;

    public ConcertViewModel(MyDatabase database) {
        DataSource.Factory<Integer, Concert> factory =
                database.allConcertsFactory().map(concert ->
                    concert.getName() + "-" + concert.getDate());
        concertDescriptions = new LivePagedListBuilder<>(
            factory, /* page size */ 50).build();
    }
}

Esto puede ser útil si deseas unir, convertir o preparar elementos después de cargarlos. Debido a que esos procesos se realizan en el ejecutor de la recuperación, puedes hacer un trabajo potencialmente costoso, como leer desde el disco o consultar una base de datos separada.

Envía comentarios

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

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