Visão geral da biblioteca Paging 2 Parte do Android Jetpack

Com a biblioteca Paging, é possível carregar e mostrar pequenos blocos de dados por vez. O carregamento de dados parciais sob demanda reduz o uso da largura de banda da rede e dos recursos do sistema.

Este guia oferece vários exemplos conceituais da biblioteca, além de uma visão geral de como ela funciona. Para ver exemplos completos de como essa biblioteca funciona, consulte o codelab e os exemplos da seção Outros recursos.

Configurar

Para importar componentes da biblioteca Paging para seu app Android, adicione as seguintes dependências ao arquivo build.gradle do 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
}

Arquitetura da biblioteca

Esta seção descreve e mostra os principais componentes da Paging Library.

PagedList

O componente principal da biblioteca Paging é a classe PagedList, que carrega blocos de dados ou páginas do seu app. Conforme mais dados são necessários, eles são paginados no objeto PagedList já existente. Se algum dado carregado for alterado, uma nova instância de PagedList será emitida para o armazenador de dados observáveis de um objeto baseado em LiveData ou RxJava2. À medida que os objetos PagedList são gerados, a IU do app apresenta o conteúdo deles, respeitando os ciclos de vida dos controladores de IU.

O snippet de código a seguir mostra como é possível configurar o modelo de visualização de seu app para carregar e apresentar dados usando um armazenador 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();
    }
}

Dados

Cada instância de PagedList carrega um resumo atualizado dos dados do app a partir do objeto DataSource correspondente. Fluxos de dados do back-end ou do banco de dados do seu app para o objeto PagedList.

O exemplo a seguir usa a biblioteca de persistência Room para organizar os dados do seu app. No entanto, caso queira armazená-los de outra forma, também é possível fornecer sua própria fonte de dados.

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 saber mais sobre como carregar dados em objetos PagedList, consulte o guia sobre como Carregar dados paginados.

Interface do usuário

A classe PagedList funciona com um PagedListAdapter para carregar itens em uma RecyclerView. Essas classes trabalham juntas para buscar e exibir o conteúdo à medida que ele é carregado, fazendo a pré-busca de conteúdo fora da visualização e animando as mudanças de conteúdo.

Para saber mais, consulte o guia sobre como Exibir listas paginadas.

Compatibilidade com diferentes arquiteturas de dados

A Paging Library é compatível com as arquiteturas de dados a seguir:

  • Veiculada somente de um servidor de back-end.
  • Armazenada somente em um banco de dados do dispositivo.
  • Uma combinação das outras origens, usando o banco de dados do dispositivo como cache.

A figura 1 mostra como é o fluxo de dados cada um desses cenários de arquitetura. No caso de uma solução somente de rede ou de banco de dados, os dados fluem diretamente para o modelo de IU do seu app. Caso você esteja usando uma abordagem combinada, os dados fluem do servidor de back-end para um banco de dados do dispositivo e depois para o modelo de IU do app. De vez em quando, o endpoint de cada fluxo fica sem dados para carregar e solicita mais dados do componente que os forneceu. Por exemplo, quando um banco de dados do dispositivo fica sem dados, ele solicita mais do servidor.

Diagramas de fluxos de dados
Figura 1. Como os dados fluem por cada uma das arquiteturas compatíveis com a biblioteca Paging

O restante desta seção oferece recomendações para a configuração de cada caso de uso de fluxo de dados.

Somente rede

Para exibir dados de um servidor de back-end, use a versão síncrona da API Retrofit para carregar informações no seu próprio objeto DataSource personalizado.

Somente banco de dados

Configure seu RecyclerView para observar o armazenamento local, de preferência usando a biblioteca de persistência Room. Dessa forma, sempre que os dados forem inseridos ou modificados no banco de dados do app, essas mudanças serão refletidas automaticamente no RecyclerView que exibe esses dados.

Rede e banco de dados

Depois de começar a observar o banco de dados, será possível detectar quando ele estiver sem dados usando PagedList.BoundaryCallback. Você pode buscar mais itens da sua rede e inseri-los no banco de dados. Caso a IU esteja observando o banco de dados, é só isso que você precisa fazer.

Solucionar erros de rede

Ao usar uma rede para buscar ou paginar os dados exibidos usando a biblioteca Paging, é importante não tratar a rede como "disponível" "ou" "indisponível" o tempo todo, porque muitas conexões são intermitentes ou lentas:

  • Um servidor específico pode não responder a uma solicitação de rede.
  • O dispositivo pode estar conectado a uma rede lenta ou fraca.

Em vez disso, seu app precisa verificar se há falhas em cada solicitação e recuperá-las da melhor forma possível nos casos em que a rede não está disponível. Por exemplo, você pode oferecer um botão "Tentar novamente" a ser selecionado pelos usuários quando a etapa de atualização de dados não funcionar. Caso ocorra um erro durante a etapa de paginação de dados, é melhor repetir as solicitações de paginação de forma automática.

Atualizar seu app existente

Caso seu app já consuma dados de um banco de dados ou de uma fonte de back-end, é possível fazer upgrade diretamente para a função oferecida pela biblioteca Paging. Esta seção mostra como fazer upgrade de um app com design comum.

Soluções personalizadas de paginação

Se você usar a funcionalidade personalizada para carregar pequenos subconjuntos de dados da fonte de dados do seu app, substitua essa lógica pela classe PagedList. As instâncias de PagedList fornecem conexões integradas a fontes de dados comuns. Essas instâncias também fornecem adaptadores para objetos RecyclerView que você pode incluir na IU do seu app.

Dados carregados usando listas, em vez de páginas

Se você usar uma lista na memória como estrutura de dados de apoio para o adaptador da UI, considere a possibilidade de usar uma classe PagedList se o número de itens na lista ficar grande. As instâncias de PagedList podem usar LiveData<PagedList> ou Observable<List> para transmitir atualizações de dados para a IU do app, minimizando os tempos de carregamento e o uso de memória. Melhor ainda, substituir um objeto List por um PagedList no seu app não exige nenhuma mudança na estrutura de IU ou na lógica de atualização de dados do seu app.

Associar um cursor de dados a uma visualização em lista usando o CursorAdapter

Seu app pode usar um CursorAdapter para associar dados de um Cursor a uma ListView. Nesse caso, você geralmente precisa migrar de uma ListView para uma RecyclerView como o contêiner de IU da lista do seu app e depois substituir o componente Cursor por Room ou PositionalDataSource, caso as instâncias de Cursor acessem um banco de dados SQLite.

Em algumas situações, como ao trabalhar com instâncias de Spinner, você fornece apenas o próprio adaptador. Em seguida, uma biblioteca recebe os dados que são carregados nesse adaptador e os exibe para você. Nessas situações, mude o tipo de dados do adaptador para LiveData<PagedList> e envolva essa lista em um objeto ArrayAdapter antes de tentar fazer com que uma classe de biblioteca infle esses itens em uma IU.

Carregar conteúdo de forma assíncrona usando o AsyncListUtil

Se você estiver usando objetos AsyncListUtil para carregar e exibir grupos de informações de forma assíncrona, a biblioteca Paging permitirá que você carregue dados com mais facilidade:

  • Seus dados não precisam ser de posicionamento. A biblioteca Paging permite carregar dados diretamente do seu back-end usando as chaves fornecidas pela rede.
  • Seus dados podem ser extremamente grandes. Usando a biblioteca Paging, é possível carregar dados em páginas até que não haja mais dados restantes.
  • Você pode observar seus dados com mais facilidade. A biblioteca Paging pode apresentar os dados que o ViewModel do seu app mantém em uma estrutura de dados observável.

Exemplos de banco de dados

Os snippets de código a seguir mostram várias maneiras possíveis de fazer com que todas as partes funcionem juntas.

Observar dados paginados usando o LiveData

O snippet de código a seguir mostra todas as partes funcionando juntas. À medida que eventos de shows são adicionados, removidos ou modificados no banco de dados, o conteúdo em RecyclerView é atualizado de forma automática e 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);
        }
    };
}

Observar dados paginados usando RxJava2

Se preferir usar RxJava2 em vez de LiveData, você pode criar um objeto Observable ou 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();
    }
}

Em seguida, você pode iniciar e parar a observação dos dados usando o código no snippet a seguir:

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

O código para ConcertDao e ConcertAdapter é o mesmo para uma solução baseada em RxJava2 e para uma solução baseada em LiveData.

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