Coletar dados paginados

Este documento se baseia 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 interface observa um objeto LiveData<PagedList> (ou, se você estiver usando o 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 da lista do 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 um único 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 do Room pode fornecer objetos DataSource.Factory para você, ou você pode criar os seus.

O snippet de código abaixo mostra como criar uma nova instância de LiveData<PagedList> na classe ViewModel do app usando os recursos de criação de DataSource.Factory do 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 ainda mais um LiveData<PagedList> para casos avançados, você também pode definir sua própria configuração de paginação. Em particular, é possível definir 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 de posição: determina se a interface exibirá marcadores de posição para os itens de lista que ainda não foram carregados. Para uma discussão sobre os benefícios e as desvantagens de usar marcadores, aprenda a 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 incorporarem as chaves seguinte ou anterior. Por exemplo, se você estiver buscando postagens de mídias sociais na rede, talvez precise transmitir um token nextPage de um carregamento para o seguinte.
  • 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 acessar o conteúdo do comentário seguinte.
  • Use PositionalDataSource se precisar buscar páginas de dados de qualquer local escolhido no repositório de dados. Essa classe permite solicitar um conjunto de itens de dados que começa em qualquer local selecionado. Por exemplo, a solicitação pode retornar os 50 itens de dados que começam com o 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 fazer isso, chame invalidate() da classe DataSource que você escolheu para o 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, pode implementar uma das subclasses 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, é possível carregar esses dados personalizados em objetos PagedList criando uma subclasse concreta de DataSource.Factory. O snippet de código abaixo 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 criar objetos PagedList observáveis, considere como as atualizações de conteúdo funcionam. Se os dados forem carregados diretamente de um banco de dados do 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 o DataSource usado mais recentemente. Em seguida, você solicita uma nova instância dessa fonte de dados. O snippet de código abaixo 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.

Amostras

Codelabs

Vídeos