Coletar dados paginados

Este guia foi desenvolvido com base na Visão geral da biblioteca Paging e discute como personalizar a solução de carregamento de dados do app para atender às necessidades de arquitetura dele.

Criar uma lista observável

Normalmente, o código da IU observa um objeto LiveData<PagedList> (ou, se você estiver usando RxJava2, um objeto Flowable<PagedList> ou Observable<PagedList>), que reside no ViewModel do app. Esse objeto observável forma uma conexão entre a apresentação e o conteúdo dos dados de lista do seu app.

Para criar um desses objetos PagedList observáveis, transmita uma instância de DataSource.Factory para um objeto LivePagedListBuilder ou RxPagedListBuilder. Um objeto DataSource carrega páginas para uma única PagedList. A classe de fábrica cria novas instâncias de PagedList em resposta a atualizações de conteúdo, como invalidações de tabelas de banco de dados e atualizações de rede. A biblioteca de persistência Room pode fornecer objetos DataSource.Factory para você, ou você pode criar objetos próprios.

O snippet de código a seguir mostra como criar uma nova instância de LiveData<PagedList> na classe ViewModel do seu app com o recursos de construção DataSource.Factory da 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();

Definir a própria configuração de paginação

Para configurar LiveData<PagedList> para casos avançados, também é possível definir sua própria configuração de paginação. Você pode definir especialmente os seguintes atributos:

  • Tamanho da página: o número de itens em cada página.
  • Distância da pré-busca: considerando o último item visível na IU de um app, o número de itens após o último item que a biblioteca Paging pode tentar buscar com antecedência. Esse valor precisa ser várias vezes maior que o tamanho da página.
  • Presença de marcador: determina se a IU exibirá marcadores para os itens de listas que ainda não foram carregados. Para ver uma discussão sobre os benefícios e as desvantagens de usar marcadores, consulte Fornecer marcadores na IU.

Se quiser ter mais controle sobre quando a biblioteca Paging carrega uma lista do banco de dados do seu app, transmita um objeto Executor personalizado ao LivePagedListBuilder, como mostrado no snippet de código a seguir.

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

Escolher o tipo de fonte de dados correto

É importante se conectar à fonte de dados que processa melhor a estrutura dos seus dados de origem:

  • Use PageKeyedDataSource se as páginas carregadas incorporam chaves anteriores ou subsequentes. Por exemplo, caso você esteja buscando postagens de mídia social na rede, talvez seja necessário transmitir um token nextPage de um carregamento para um carregamento subsequente.
  • Use ItemKeyedDataSource se precisar usar dados do item N para buscar o item N+1. Por exemplo, se você estiver buscando comentários em sequência em um app de discussões, poderá ser necessário transmitir o ID do último comentário para conseguir o conteúdo do comentário seguinte.
  • Use PositionalDataSource se precisar buscar páginas de dados de qualquer local escolhido no seu armazenamento de dados. Essa classe é compatível com a solicitação de um conjunto de itens de dados a partir de qualquer local selecionado. Por exemplo, a solicitação pode retornar os 50 itens de dados do local 1500.

Notificar quando os dados forem inválidos

Ao usar a biblioteca Paging, a camada de dados é responsável por notificar as outras camadas do seu app quando uma tabela ou linha se torna obsoleta. Para isso, chame invalidate() da classe de DataSource que você escolheu para seu app.

Criar as próprias fontes de dados

Se você usa uma solução de dados local personalizada ou carrega dados diretamente de uma rede, é possível implementar uma das subclasses de DataSource. O snippet de código a seguir mostra uma fonte de dados que foi vinculada ao horário de início de determinado show.

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

Em seguida, carregue esses dados personalizados em objetos PagedList criando uma subclasse concreta de DataSource.Factory. O snippet de código a seguir mostra como gerar novas instâncias da fonte de dados personalizada definida no snippet 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;
    }
}

Considerar como as atualizações de conteúdo funcionam

Ao construir objetos observáveis PagedList, considere como as atualizações de conteúdo funcionam. Se os dados forem carregados diretamente de um banco de dados da Room, as atualizações serão enviadas automaticamente para a IU do seu app.

Ao usar uma API de rede paginada, você normalmente tem uma interação do usuário, como "deslizar para atualizar", que serve como um sinal para invalidar a DataSource usada mais recentemente. Você precisa solicitar uma nova instância dessa fonte de dados. O snippet de código a seguir demonstra esse comportamento:

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

Fornecer mapeamento de dados

A biblioteca Paging é compatível com transformações baseadas em itens e em páginas de itens carregados por uma DataSource.

No snippet de código a seguir, uma combinação do nome e da data do show é mapeada para uma única string contendo o nome e a data.

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

Isso poderá ser útil se você quiser unir, converter ou preparar itens depois que eles forem carregados. Como esse trabalho é feito no executor de busca, é possível realizar tarefas potencialmente caras, como ler o disco ou consultar um banco de dados separado.

Enviar feedback

Envie comentários e ideias usando os recursos abaixo:

Issue tracker
Informe os problemas para que possamos corrigir os bugs.

Outros recursos

Para saber mais sobre a biblioteca Paging, consulte os recursos a seguir.

Exemplos

Codelabs

Vídeos