Riverpod e Hooks: Desbloqueando o poder da paginação

Tempo de leitura: 7 minutes

Este artigo aborda como usar Hooks, um método de gerenciamento de estado de widget, e Riverpod, uma biblioteca de gerenciamento de estado Flutter, para desenvolver paginação eficaz em seus aplicativos Flutter. Com a capacidade de apresentar enormes quantidades de dados sequencialmente e aprimorar a experiência do usuário, a paginação é um recurso essencial em muitos aplicativos. Neste tutorial, demonstraremos como construir paginação usando Riverpod e Hooks, permitindo projetar interfaces de usuário dinâmicas e adaptáveis para seus projetos Flutter.

 

Paginação

Um conceito fundamental na programação frontend é a paginação, que nos permite carregar uma quantidade finita de dados simultaneamente – tanto o usuário quanto o servidor se beneficiam disso. Ao evitar que o usuário tenha que esperar que todo o material seja carregado de uma vez, isso melhora sua experiência. Ao entregar apenas os dados necessários, reduz a carga do servidor.

Tipos de paginação

A paginação não é uma solução universalmente aplicável. Está disponível em vários sabores, cada um com um sabor único. Uma prévia dos tipos de paginação que analisaremos nas seções a seguir é fornecida abaixo:

1. Paginação de página/deslocamento:

Este método tradicional de paginação depende da capacidade do servidor de determinar limitações e compensações de dados. O frontend especifica apenas o número ou deslocamento da página desejada; o back-end executa tarefas de processamento intensivo.

2. Paginação baseada em cursor/conjunto de teclas/busca/tempo:

Essa paginação requer mais comunicação entre o desenvolvimento front-end e back-end. Informamos ao servidor o itemItem mais recente que o usuário visualizou. Pode ser um campo crítico para o seu conjunto de dados, como um ID de item (na paginação do Cursor) ou um carimbo de data/hora (na paginação baseada em tempo).

Usando Flutter

No desenvolvimento de front-end recente, existem dois tipos principais de paginação: um onde os dados do itemItem visualizado mais recentemente são enviados para o back-end e outro onde os detalhes da página ou deslocamento são calculados e enviados. Como o back-end controla a consulta e a estrutura de dados, normalmente decide qual método utilizar.

1. Paginação de página/deslocamento:

Quando a paginação de página/deslocamento é usada, a estrutura da solicitação frequentemente se assemelha ao objeto JSON no Flutter mostrado abaixo:

Exemplo

{ "page": 1,
    "perpage": 10,
    "search": "...other filter"
}

 

2. Paginação usando o cursor:

Esta paginação introduz uma nova estrutura de solicitação:

Exemplo

{
   "cursor": "Item-12",
   "search":....// outro filtro
}

Um cursor que geralmente aponta para o último item obtido em uma solicitação anterior serve como ponto focal desta solicitação. Além disso, filtros de pesquisa podem estar presentes. Em resposta, o back-end retorna objetos JSON como:

Código:

{  
  "data": [
    {"id": "Item-13", "name": "Item 13"},
    {"id": "Item-14", "name": "Item 14"},
    {"id": "Item-15", "name": "Item 15"},
    {"id": "Item-16", "name": "Item 16"},
    {"id": "Item-17", "name": "Item 17"},
    {"id": "Item-18", "name": "Item 18"},
    {"id": "Item-19", "name": "Item 19"},
    {"id": "Item-20", "name": "Item 20"},
    {"id": "Item-21", "name": "Item 21"},
    {"id": "Item-22", "name": "Item 22"}
  ],
  "hasMore": true // algumas APIs podem não retornar isso e algumas podem devolvê-lo
}

Execute o seguinte comando para adicionar o pacote necessário antes de iniciar o código.

dart pub add dev:json_serializable json_annotation dev:build_runner flutter_hooks hooks_riverpod flutter_riverpod riverpod

Transformar em modelo

Usar a função toJson/fromJson para transformar a resposta JSON em modelos Dart para implementar a paginação em seu aplicativo Dart de forma eficiente é crucial. Veja como usar a biblioteca json_serializable para definir modelos Dart.

@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<t> {
  final MetaData meta;
  final List<t> data;

  PaginationResponse({required this.meta, required this.data});

  factory PaginationResponse.fromJson(
          Map<string, dynamic=""> json) =>
      _$PaginationResponseFromJson(json);


  Map<string, dynamic=""> toJson = >   _$PaginationResponseFromJson(this);
}

 

 

Isolamento lógico de paginação para reutilização

É uma boa ideia isolar a lógica de paginação em uma classe distinta para utilizá-la em diferentes partes do seu aplicativo. Como resultado, seu código se torna mais flexível e abstrato. Vamos expandir ainda mais essa ideia, começando por dar maior generalidade ao modelo PaginationResponse:

@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<t> {
  final MetaData meta;
  final List<t> data;

  PaginationResponse({required this.meta, required this.data});

  factory PaginationResponse.fromJson(
          Map<string, dynamic=""> json, T Function(Object?) dataFromJson) =>
      _$PaginationResponseFromJson(json, dataFromJson);
}


@JsonSerializable()
class MetaData {
  final int page;
  final int perPage;
  final int totalPage;

  MetaData(
      {required this.page, required this.perPage, required this.totalPage});

  factory MetaData.fromJson(Map<string, dynamic=""> json) =>
      _$MetaDataFromJson(json);

  Map<string, dynamic=""> toJson() => _$MetaDataToJson(this);
}

 

Controlador abstrato

Poderíamos desenvolver uma classe mixin chamada PaginationController para implementar um controlador de paginação reutilizável usando Riverpod na biblioteca de gerenciamento de estado Flutter. Onde a paginação for necessária, este controlador fornecerá métodos para lidar com a lógica e poderá ser incorporado aos notificadores Riverpod.

Código

mixin PaginationController<t> on AsyncNotifierBase<paginationresponse<t>> {
  FutureOr<paginationresponse<t>> loadData(PaginationRequest query);
  Future<void> loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading<paginationresponse<t>>().copyWithPrevious(oldState);
    state = await AsyncValue.guard<paginationresponse<t>>(() async {
      final res = await loadData(oldState.requireValue.nextPage());
      res.data.insertAll(0, state.requireValue.data);
      return res;
    });
  }
  bool canLoadMore() {
    if (state.isLoading) return false;
    if (!state.hasValue) return false;
    if (state.requireValue.isCompleted) return false;
    return true;
  }
}

Os seguintes recursos são fornecidos por este mixin PaginationController:

1. loadData: Ao mesclar esta classe, você deve implementar a função abstrata loadData. A busca e análise de dados reais devem ser tratadas por ele.

2. loadMore: Este método recebe novos dados, adiciona-os aos dados existentes e inicia um estado de carregamento. É responsável por carregar mais dados.

3. canLoadMore: Este método avalia se dados adicionais são carregados. Determina se um procedimento de carregamento está em andamento e se todos os dados foram carregados.

4. isCompleted: com base nos dados do modelo PaginationResponse, o getter determina se a paginação foi concluída.

 

Código

bool get isCompleted => meta.page >= meta.totalPage;

5. nextPage(): A próxima página é retornada por meio do método nextPage(), que é encontrado na resposta de paginação.

PaginationRequest nextPage() =>
     PaginationRequest(perPage: meta.perPage, page: meta.page + 1);

Portanto, o código final é:

// model/pagination_request.dart
@JsonSerializable()
class PaginationRequest {
  final int page;
  final int perPage;


  PaginationRequest({
    required this.page,
    required this.perPage,
  });


  factory PaginationRequest.fromJson(Map<string, dynamic=""> json) =>
      _$PaginationRequestFromJson(json);


  Map<string, dynamic=""> toJson() => _$PaginationRequestToJson(this);
}

//model/pagination_response.dart

@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<t> {
  final MetaData meta;
  final List<t> data;


  bool get isCompleted => meta.page >= meta.totalPage;
  PaginationRequest nextPage() =>
      PaginationRequest(perPage: meta.perPage, page: meta.page + 1);


  PaginationResponse({required this.meta, required this.data});


  factory PaginationResponse.fromJson(
          Map<string, dynamic=""> json, T Function(Object?) dataFromJson) =>
      _$PaginationResponseFromJson(json, dataFromJson);
}


@JsonSerializable()
class MetaData {
  final int page;
  final int perPage;
  final int totalPage;


  MetaData(
      {required this.page, required this.perPage, required this.totalPage});


  factory MetaData.fromJson(Map<string, dynamic=""> json) =>
      _$MetaDataFromJson(json);


  Map<string, dynamic=""> toJson() => _$MetaDataToJson(this);
}

//pagination_controller.dart

mixin PaginationController<t> on AsyncNotifierBase<paginationresponse<t>> {
  FutureOr<paginationresponse<t>> loadData(PaginationRequest query);


  Future<void> loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading<paginationresponse<t>>().copyWithPrevious(oldState);
    state = await AsyncValue.guard<paginationresponse<t>>(() async {
      final res = await loadData(oldState.requireValue.nextPage());
      res.data.insertAll(0, state.requireValue.data);
      return res;
    });
  }


  bool canLoadMore() {
    if (state.isLoading) return false;
    if (!state.hasValue) return false;
    if (state.requireValue.isCompleted) return false;
    return true;
  }
}

 

 

O controlador de paginação está sendo utilizado

Vamos usar o controlador base que acabamos de criar para começar a usá-lo em nossa aplicação. Para incorporar o controlador ao seu código, siga estas etapas:

  • A mágica acontece na classe ItemsController. Ele usa o mixin PaginationController que criamos e estende AutoDisposeAsyncNotifier.
  • Especificamos a estratégia inicial de busca de dados no método de construção. Aqui, 30 itens são exibidos na primeira página. Para atender às suas necessidades, você pode alterar isso.
  • A tarefa de obter dados do apiClient está na função loadData. Você pode alternar isso com a lógica usada para recuperar dados de um caso de uso, repositório, serviço ou diretamente por meio de uma chamada de API.
  • A função fetchItems serve como uma ilustração de como o apiClient pode ser usado para recuperar dados. Ele utiliza Dio para enviar uma solicitação HTTP e retorna um PaginationResponse<item Item>.

Código

FutureOr<paginationresponse<item>> fetchItems(PaginationRequest? query) async {
   final res = await dio.get<map>("/items", queryParameters: query?.toJson());
   return PaginationResponse<item>.fromJson(
       res.data!.cast(), (v) => Item.fromJson(v! as Map<string, dynamic="">));
 }

 

Implantação de Flutter Hooks

Depois que a lógica de paginação for criada com sucesso, a próxima etapa crítica é descobrir quando usar o método loadMore. Para comportamento de rolagem infinita, deve ser ativado quando o usuário chega perto do final da página ou clica no botão “Página/Carregar mais”. Como gerenciar cliques em botões é muito simples, exploraremos a ideia de rolagem ilimitada neste post. Usar um widget ScrollController ou NotificationListener permitirá rolagem ilimitada. Mostraremos como utilizar flutter_hooks para encapsular essa lógica em um gancho usePagination exclusivo para tornar isso ainda mais simples. A boa notícia é que será necessária apenas uma pequena quantidade de código, tornando a implementação simples.

Código

import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';


ScrollController usePagination(
    VoidCallback fetchData, bool Function() isLoadMore) {
  final scrollController = useScrollController();


  void scrollListener() {
    if (isLoadMore() &&
        scrollController.position.pixels >=
            scrollController.position.maxScrollExtent) {
      fetchData();
    }
  }


  useEffect(() {
    scrollController.addListener(scrollListener);
    return null;
  }, [scrollController]);


  return scrollController;
}

 

 

Paginação com cursor

O código completo de paginação do cursor é fornecido abaixo, junto com uma descrição das alterações feitas:

Código

import 'package:json_annotation/json_annotation.dart';
part "pagination_request.g.dart";


@JsonSerializable()
class PaginationRequest {
  final String? cursor;


  PaginationRequest({
    this.cursor,
  });


  factory PaginationRequest.fromJson(Map<string, dynamic=""> json) =>
      _$PaginationRequestFromJson(json);


  Map<string, dynamic=""> toJson() => _$PaginationRequestToJson(this);
}


@JsonSerializable(genericArgumentFactories: true, createToJson: false)
class PaginationResponse<t> {
  final List<t> data;


  bool get isCompleted =>
      data.isEmpty; // if we get empty data that mean we completed


  PaginationResponse({required this.data});


  factory PaginationResponse.fromJson(
          Map<string, dynamic=""> json, T Function(Object?) dataFromJson) =>
      _$PaginationResponseFromJson(json, dataFromJson);
}

Adaptamos a implementação isCompleted, excluímos os metadados e movemos o método nextPage para o controlador abaixo.

Código

mixin PaginationController<t> on AsyncNotifierBase<paginationresponse<t>> {
  FutureOr<paginationresponse<t>> loadData(PaginationRequest query);
  PaginationRequest nextPage(PaginationResponse<t> current);
  Future<void> loadMore() async {
    final oldState = state;
    if (oldState.requireValue.isCompleted) return;
    state = AsyncLoading<paginationresponse<t>>().copyWithPrevious(oldState);
    state = await AsyncValue.guard<paginationresponse<t>>(() async {
      final res = await loadData(nextPage(oldState.requireValue));
      res.data.insertAll(0, state.requireValue.data);
      return res;
    });
  }


  bool canLoadMore() {
    if (state.isLoading) return false;
    if (!state.hasValue) return false;
    if (state.requireValue.isCompleted) return false;
    return true;
  }
}

O método abstrato nextPage é uma mudança significativa, pois precisamos usar o controlador final para implementá-lo.

@JsonSerializable()
class Item {
  final String id;
  final String name;
  Item(this.id, this.name);


  factory Item.fromJson(Map<string, dynamic=""> json) => _$ItemFromJson(json);
  Map<string, dynamic=""> toJson() => _$ItemToJson(this);
}


final itemsController = AsyncNotifierProvider.autoDispose<itemscontroller, paginationresponse<item="">>(ItemsController.new);


class ItemsController extends AutoDisposeAsyncNotifier<paginationresponse<item>>
    with PaginationController<item> {
  @override
  Future<paginationresponse<item>> build() async {
    return await loadData(PaginationRequest(cursor: null));
  }


  @override
  FutureOr<paginationresponse<item>> loadData(PaginationRequest query) {
    return ref.read(itemRepoProvider).getItemByCursorPaginate(query);
  }


  @override
  PaginationRequest nextPage(PaginationResponse<item> current) =>
      PaginationRequest(cursor: current.data.last.id);
}

O método nextPage foi movido para o controlador para fornecer acesso às propriedades do item, uma vez que outras camadas podem não estar familiarizadas com a estrutura do item.

O componente da interface do usuário não mudará.

Lembre-se de que dependendo de como seu back-end lida com a paginação, cada caso de uso pode exigir modificações e ajustes distintos.

 

Conclusão

Concluindo, usar RiverPod com Hooks para implementar paginação em seu aplicativo Flutter é uma virada de jogo. Garante um gerenciamento de dados eficaz e uma experiência de usuário tranquila. Para melhorar o desempenho do seu aplicativo e a satisfação do usuário, seja ousado e contrate desenvolvedores Flutter com experiência nessas estratégias se precisar de ajuda. Codificar é divertido!

Perguntas frequentes (FAQ)

1. Defina a paginação no Flutter.

Flutter tem um recurso de paginação que permite aos usuários visualizar big data em pedaços menores. Embora carregar todos os recursos de uma vez seja difícil ou lento, como em feeds de redes sociais, resultados de pesquisa ou listas de produtos, a paginação é frequentemente usada.

2. Quais tipos de paginação são discutidos neste artigo?

Os dois principais tipos de paginação são abordados neste artigo:

Paginação de página/deslocamento: O usuário especifica o número de página ou deslocamento desejado neste método tradicional, deixando a recuperação de dados para o back-end.
Cursor/Keyset/Seek/Time-Based Pagination: Com esse tipo, os dados sobre a visualização de item mais recente do usuário são enviados do front-end para o back-end.

3. Isolar a lógica de paginação em uma classe diferente é uma boa ideia?

A capacidade de reutilização e manutenção do código são melhoradas separando a lógica de paginação em uma classe distinta. Ele permite dissociar a funcionalidade e aplicá-la em vários componentes do aplicativo, permitindo uma base de código mais simples e modular.