Como gerar automaticamente seus provedores com Flutter Riverpod Generator

Tempo de leitura: 12 minutes

Riverpod é uma poderosa estrutura de cache reativo e vinculação de dados para Flutter.

Isso nos dá muitos tipos diferentes de provedores que podemos usar para:

  • acessar dependências em nosso código (com Provider)
  • armazenar em cache dados assíncronos da rede (com FutureProvider e StreamProvider)
  • gerenciar o estado do aplicativo local (com StateProvider, StateNotifierProvider e ChangeNotifierProvider)

Mas escrever muitos provedores manualmente pode ser propenso a erros, e escolher qual provedor usar nem sempre é fácil. 🥵

E se eu lhe dissesse que você não precisa mais?

E se eu lhe dissesse que você não precisa mais?

E se você pudesse simplesmente anotar seu código com @riverpod e deixar o build_runner gerar todos os provedores instantaneamente?

Acontece que é para isso que serve o novo pacote riverpod_generator (e pode tornar nossa vida muito mais fácil).

 

O que iremos cobrir

Há muito o que abordar, então dividirei isso em dois artigos.

Neste primeiro artigo, aprenderemos como gerar provedores a partir de funções usando a nova sintaxe @riverpod.

Como parte disso, mostrarei como:

  • declare provedores com a sintaxe @riverpod
  • converta FutureProvider para a nova sintaxe
  • passar argumentos para um provedor, superando as limitações do antigo modificador family

E no próximo artigo aprenderemos como gerar provedores a partir de classes e veremos como substituir completamente StateNotifierProvider e StateProvider pelas novas classes Notifier e AsyncNotifier.

Você pode encontrar o segundo artigo aqui (Em breve): Como usar o Notifier e o AsyncNotifier com o novo Flutter Riverpod Generator.

Também abordaremos algumas vantagens e desvantagens, para que você possa decidir se deve usar a nova sintaxe em seus próprios aplicativos.

Preparar? Vamos! 👇

 

Devemos escrever os provedores à mão?

Essa é uma boa pergunta.

Por um lado, você pode ter provedores simples como este:

// a provider for the Dio client to be used by the rest of the app
final dioProvider = Provider<Dio>((ref) {
  return Dio();
});

Por outro lado, alguns provedores possuem dependências e podem aceitar um argumento usando o modificador family:

// a provider to fetch the movie data for a given movie id
final movieProvider = FutureProvider.autoDispose
    .family<TMDBMovie, int>((ref, movieId) {
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
});

E se você tiver um StateNotifierProvider com o modificador family, a sintaxe se tornará ainda mais complexa, pois será necessário especificar três anotações de tipo:

final emailPasswordSignInControllerProvider = StateNotifierProvider.autoDispose
    .family<
        EmailPasswordSignInController, // the StateNotifier subclass
        EmailPasswordSignInState, // the type of the underlying state class
        EmailPasswordSignInFormType // the argument type passed to the family
    >((ref, formType) {
  return EmailPasswordSignInController(
    authRepository: ref.watch(authRepositoryProvider),
    formType: formType,
  );
});

Embora o analisador estático possa nos ajudar a descobrir quantos tipos precisamos, o código acima não é muito legível.

Existe uma maneira mais simples? 🧐

 

A anotação @riverpod

Vamos considerar este FutureProvider mais uma vez:

// a provider to fetch the movie data for a given movie id
final movieProvider = FutureProvider.autoDispose
    .family<TMDBMovie, int>((ref, movieId) {
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
});

A essência deste provedor é que podemos usá-lo para buscar um filme chamando este método:

// declared inside a MoviesRepository class
Future<TMDBMovie> movie({required int movieId});

Mas e se, em vez de criar o provedor acima, pudéssemos escrever algo assim?

@riverpod
Future<TMDBMovie> movie(
  MovieRef ref, {
  required int movieId,
}) {
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
}

Isso está de acordo com a forma como definimos uma função:

  • tipo de retorno primeiro
  • então nome da função
  • então lista de argumentos
  • então corpo funcional
Uma definição de função. 1: tipo de retorno, 2: nome da função, 3: lista de argumentos, 4: corpo da função
Uma definição de função. 1: tipo de retorno, 2: nome da função, 3: lista de argumentos, 4: corpo da função

Isso é mais intuitivo do que declarar FutureProvider.family<TMDBMovie, int>, com o tipo de retorno próximo ao tipo de argumento.

 

Uma nova sintaxe do Riverpod?

Quando Remi apresentou a nova sintaxe do Riverpod durante o Flutter Vikings, fiquei um pouco confuso.

Mas depois de experimentá-lo em alguns dos meus projetos, passei a gostar de sua simplicidade.

A nova API é muito mais simplificada e traz duas melhorias significativas de usabilidade:

  • você não precisa mais se preocupar com qual provedor usar
  • você pode passar argumentos nomeados ou posicionais para um provedor como desejar (assim como faria com qualquer função)

Este é um grande avanço para o próprio Riverpod, e aprender a nova API tornará sua vida muito mais fácil.

Então deixe-me mostrar como tudo funciona.

Em vez de começar do zero, pegaremos alguns provedores de um aplicativo existente e os converteremos para a nova sintaxe. No final, compartilharei um repositório de exemplo com o código-fonte completo.

 

Primeiros passos com riverpod_generator

Conforme explicado na página riverpod_generator em pub.dev, precisamos adicionar estes pacotes a pubspec.yaml:

dependencies:
  # ou flutter_riverpod/hooks_riverpod conforme https://riverpod.dev/docs/getting_started
  riverpod:
  # o pacote de anotações contendo @riverpod
  riverpod_annotation:

dev_dependencies:
  # uma ferramenta para executar geradores de código
  build_runner:
  # o gerador de código
  riverpod_generator:
  # riverpod_lint torna mais fácil trabalhar com Riverpod
  riverpod_lint:
  # importe custom_lint também, pois riverpod_lint depende disso
  custom_lint:

 

Iniciando o gerador de código no modo “watch”

Então, precisamos executar este comando no terminal:

flutter pub run build_runner watch -d

O sinalizador -d é opcional e é igual a --delete-conflicting-outputs. Como o nome indica, ele garante a substituição de quaisquer saídas conflitantes de compilações anteriores (que normalmente é o que queremos).

Isso observará todos os arquivos Dart em nosso projeto e atualizará automaticamente o código gerado quando fizermos alterações.

Então, vamos começar a criar alguns provedores. 👇

 

Criando o primeiro provedor anotado

Como primeiro passo, vamos considerar este provedor simples:

// dio_provider.dart
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// a provider for the Dio client to be used by the rest of the app
final dioProvider = Provider<Dio>((ref) {
  return Dio();
});

Veja como devemos modificar este arquivo para usar a nova sintaxe:

import 'package:dio/dio.dart';
// 1. import the riverpod_annotation package
import 'package:riverpod_annotation/riverpod_annotation.dart';

// 2. add a part file
part 'dio_provider.g.dart';

// 3. use the @riverpod annotation
@riverpod
// 4. update the declaration
Dio dio(DioRef ref) {
  return Dio();
}

Assim que salvarmos este arquivo, build_runner começará a trabalhar e produzirá dio_provider.g.dart na mesma pasta:

Os novos arquivos .g.dart são gerados junto com os existentes, então você não precisa alterar a estrutura de pastas.

E se abrirmos o arquivo gerado, é isso que vemos:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'dio_provider.dart';

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$dioHash() => r'26723d20a4ee2d05c3b01acad1196ed96cece567';

/// See also [dio].
@ProviderFor(dio)
final dioProvider = AutoDisposeProvider<Dio>.internal(
  dio,
  name: r'dioProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$dioHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef DioRef = AutoDisposeProviderRef<Dio>;
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

O mais relevante é que este arquivo:

  • contém o dioProvider que precisamos (com propriedades adicionais que podemos usar para depuração)
  • define um tipo DioRef como AutoDisposeProviderRef<Dio>

Isso significa que só precisamos escrever este código:

part 'dio_provider.g.dart';

@riverpod
Dio dio(DioRef ref) {
  return Dio();
}

E riverpod_generator criará o dioProvider correspondente e o tipo DioRef que passamos como argumento para nossa função.

Todos os provedores criados com riverpod_generator usam o modificador autoDispose por padrão.

 

Criando um provedor para uma classe Repository

Agora que temos um dioProvider, vamos tentar usá-lo em algum lugar.

Por exemplo, suponha que temos uma classe MoviesRepository que define alguns métodos para buscar dados de filmes:

class MoviesRepository {
  MoviesRepository({required this.client, required this.apiKey});
  final Dio client;
  final String apiKey;

  // procura por filmes que correspondam a uma determinada consulta (paginado)
  Future<List<TMDBMovie>> searchMovies({required int page, String query = ''});
  // obtém os filmes "em execução" (paginados)
  Future<List<TMDBMovie>> nowPlayingMovies({required int page});
  //obtém o filme para um determinado id
  Future<TMDBMovie> movie({required int movieId});
}

Para criar um provedor para este repositório, podemos escrever isto:

part 'movies_repository.g.dart';

@riverpod
MoviesRepository moviesRepository(MoviesRepositoryRef ref) => MoviesRepository(
      client: ref.watch(dioProvider), //o provedor que definimos acima
      apiKey: Env.tmdbApiKey, // uma constante definida em outro lugar
    );

Como resultado, riverpod_generator criará um moviesRepositoryProvider e o tipo MoviesRepositoryRef para nós.

Ao criar um provedor para um Repositório, não adicione a anotação @riverpod à própria classe Repositório. Em vez disso, crie uma função global separada que retorne uma instância desse Repositório e anote isso. Aprenderemos mais sobre como usar @riverpod com aulas no próximo artigo.

Criando e lendo um FutureProvider anotado

Como vimos, dado um FutureProvider como este:

// um provedor para buscar os dados do filme para um determinado ID de filme
final movieProvider = FutureProvider.autoDispose
    .family<TMDBMovie, int>((ref, movieId) {
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
});

Podemos convertê-lo para usar a anotação @riverpod:

@riverpod
Future<TMDBMovie> movie(
  MovieRef ref, {
  required int movieId,
}) {
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
}

E assista dentro do nosso widget:

class MovieDetailsScreen extends ConsumerWidget {
  const MovieDetailsScreen({super.key, required this.movieId});
  final int movieId;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // movieId é um argumento *nomeado*
    final movieAsync = ref.watch(movieProvider(movieId: movieId));
    return movieAsync.when(
      error: (e, st) => Text(e.toString()),
      loading: () => CircularProgressIndicator(),
      data: (movie) => SomeMovieWidget(movie),
    );
  }
}

Esta é a parte mais importante:

// movieId é um argumento *nomeado*
final movieAsync = ref.watch(movieProvider(movieId: movieId));

Como podemos ver, movieId é um argumento nomeado porque o definimos como tal na função movie:

@riverpod
Future<TMDBMovie> movie(
  MovieRef ref, {
  required int movieId,
}) {
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
}

Isso significa que não estamos mais restritos a definir um provedor family com apenas um argumento posicional.

Na verdade, nem nos importamos se estamos usando um family.

Tudo o que fazemos é definir uma função com um objeto “ref” e quantos argumentos nomeados ou posicionais quisermos, e riverpod_generator cuida do resto.

 

Como as famílias geradas são implementadas?

Se estivermos curiosos e dermos uma olhada em como o movieProvider foi gerado, podemos encontrar o seguinte:

typedef MovieRef = AutoDisposeFutureProviderRef<TMDBMovie>;

@ProviderFor(movie)
const movieProvider = MovieFamily();

class MovieFamily extends Family<AsyncValue<TMDBMovie>> {
  const MovieFamily();

  MovieProvider call({
    required int movieId,
  }) {
    return MovieProvider(
      movieId: movieId,
    );
  }
  ...
}

Isso usa classes que podem ser chamadas – um recurso interessante da linguagem Dart que nos permite chamar movieProvider(movieId: movieId) em vez de movieProvider.call(movieId: movieId).

Isso funciona com StreamProvider também?

Como vimos, usar @riverpod facilita a geração de um FutureProvider.

E desde o Riverpod Generator 2.0.0, os streams também são suportados.

Na verdade, se tivermos um método que retorne um Stream, podemos criar o provedor correspondente assim:

@riverpod
Stream<int> values(ValuesRef ref) {
  return Stream.fromIterable([1, 2, 3]);
}

Isso é possível graças à nova classe StreamNotifier que foi introduzida no Riverpod 2.3.

Streams e StreamProvider são bastante úteis se usarmos um banco de dados em tempo real, como Cloud Firestore, ou se estivermos nos comunicando com um back-end personalizado que suporta web sockets, então é bom que agora eles sejam suportados pelo Riverpod Generator. 👍

StateNotifier e ChangeNotifier não são suportados pelo novo gerador, portanto você ainda não pode converter o código existente usando StateNotifierProvider e ChangeNotifierProvider para a nova sintaxe.

 

 

Misturando a sintaxe antiga e a nova

Vamos revisitar esta função mais uma vez:

@riverpod
Future<TMDBMovie> movie(
  MovieRef ref, {
  required int movieId,
}) {
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
}

Observe como dentro dele chamamos ref.watch(moviesRepositoryProvider).

Mas podemos usar um provedor baseado na sintaxe antiga dentro de um provedor gerado automaticamente?

Acontece que o novo pacote Riverpod Lint introduz uma nova regra de lint chamada avoid_manual_providers_as_generated_provider_depenency. E se não seguirmos esta regra, receberemos este aviso:

Os provedores gerados devem depender apenas de outros provedores gerados. Não fazer isso pode violar regras como “provider_dependencies”

Portanto, se planejamos migrar nosso código, é melhor começar pelos provedores que não dependem de outros provedores e percorrer a árvore de provedores até que todos os provedores sejam atualizados. 👍

 

Usando autoDispose vs keepAlive

Um requisito comum é destruir o estado de um provedor quando ele não for mais usado.

Com a sintaxe antiga, isso era feito com o modificador autoDispose (que estava desabilitado por padrão).

Se usarmos a nova sintaxe @riverpod, autoDispose agora está habilitado por padrão e foi renomeado para keepAlive.

Isso significa que podemos escrever isto:

// keepAlive is false by default
@riverpod
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
  ...
}

Isso significa que podemos escrever isto:

// keepAlive: false is the same as using autoDispose
@Riverpod(keepAlive: false)
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
  ...
}

E o movieProvider gerado será descartado quando não for mais usado.

Por outro lado, se definirmos keepAlive como true, o provedor permanecerá “vivo”:

// keepAlive: true is the same as *NOT* using autoDispose
@Riverpod(keepAlive: true)
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
  ...
}

Observe que se você deseja obter um KeepAliveLink para implementar algum comportamento de cache personalizado, ainda poderá fazê-lo dentro do provedor:

@riverpod
Future<TMDBMovie> movie(MovieRef ref, {required int movieId}) {
  // obtenha o [KeepAliveLink]
  final link = ref.keepAlive();
  // iniciar um cronômetro de 60 segundos
  final timer = Timer(const Duration(seconds: 60), () {
    // descarta no tempo limite
    link.close();
  });
  // certifique-se de cancelar o cronômetro quando o estado do provedor for descartado
  ref.onDispose(() => timer.cancel());
  return ref
      .watch(moviesRepositoryProvider)
      .movie(movieId: movieId);
}

Gerador Riverpod: compensações

Agora que aprendemos sobre a nova sintaxe e como funciona o gerador, vamos resumir os prós e os contras.

Vantagem: gere automaticamente o tipo certo de provedor

A maior vantagem é que não precisamos mais descobrir que tipo de provedor precisamos (Provider vs FutureProvider vs StreamProvider etc.), uma vez que o gerador de código descobrirá isso a partir da assinatura da função.

A nova sintaxe @riverpod também facilita a declaração de provedores complexos que aceitam um ou mais argumentos (como FutureProvider.family que vimos acima).

Outro bônus é que o código gerado cria um novo tipo especializado para cada objeto “ref”, e isso pode ser facilmente inferido a partir do nome da função:

  • moviesRepository()moviesRepositoryProvider e MoviesRepositoryRef
  • movie()movieProvider e MovieRef

Isso torna os erros de tipo de tempo de execução menos prováveis, pois nosso código não será compilado se não usarmos os tipos corretos em primeiro lugar.

Vantagem: autoDispose por padrão

Com a nova sintaxe, todos os provedores gerados usam autoDispose por padrão.

Esta é uma escolha sensata, uma vez que não devemos nos apegar ao estado dos provedores que não estão mais em uso.

E como expliquei em meu guia do Riverpod 2.0, podemos ajustar o comportamento de descarte chamando ref.keepAlive() e até mesmo implementar uma estratégia de cache baseada em tempo limite, se necessário.

 

Vantagem: recarga dinâmica com estado para provedores

A documentação do pacote diz o seguinte:

Ao modificar o código-fonte de um provedor, no hot-reload o Riverpod irá reexecutar esse provedor e somente esse provedor.

Essa é uma melhoria bem-vinda. 🙂

Desvantagem: geração de código

Todas as desvantagens do Riverpod Generator se resumem a uma coisa: geração de código.

Mesmo o provedor mais simples produz 15 linhas de código gerado em um arquivo separado, o que pode retardar o processo de construção e sobrecarregar nosso projeto com arquivos extras.

Se adicionarmos os arquivos gerados ao controle de versão, eles aparecerão em Pull Requests sempre que forem alterados:

Se não quisermos isso, podemos adicionar *.g.dart a .gitignore para excluir todos os arquivos gerados de nosso repositório.

Isto tem duas implicações:

  • outros membros da equipe precisam estar sempre executando flutter pub run build_runner watch durante o desenvolvimento
  • Os fluxos de trabalho de construção de CI precisam executar o gerador de código antes de compilar o aplicativo (levando a compilações mais longas que custam mais minutos de construção)

Na prática, observei que flutter pub run build_runner watch é rápido (pelo menos em projetos pequenos) e produz atualizações em menos de um segundo após a primeira compilação:

[INFO] ------------------------------------------------------------------------
[INFO] Starting Build
[INFO] Updating asset graph completed, took 0ms
[INFO] Running build completed, took 309ms
[INFO] Caching finalized dependency graph completed, took 12ms
[INFO] Succeeded after 323ms with 4 outputs (16 actions)

Isso está alinhado com os tempos de resposta do hot-reload e torna o fluxo de trabalho de desenvolvimento muito tranquilo. 👍

No entanto, você precisará de uma máquina de desenvolvimento robusta se quiser usar o build_runner em projetos maiores.

E como os minutos de compilação do CI não são gratuitos, recomendo adicionar todos os arquivos gerados ao controle de versão (junto com os arquivos .lock para garantir que todos executem as mesmas versões de pacote).

 

Desvantagem: nem todos os tipos de provedores são suportados ainda

Dos oito tipos diferentes de provedores, riverpod_generator oferece suporte apenas ao seguinte:

  1. Provedor
  2. FutureProvider
  3. StreamProvider
  4. NotifierProvider (novo no Riverpod 2.0)
  5. AsyncNotifierProvider (novo no Riverpod 2.0)

Provedores legados como StateProvider, StateNotifierProvider e ChangeNotifierProvider não são suportados, e já expliquei como eles podem ser substituídos em meu artigo sobre como usar Notifier e AsyncNotifier com o novo Flutter Riverpod Generator.

E com a introdução do pacote Riverpod Lint, adotar a nova sintaxe @riverpod se torna muito mais fácil.

Portanto, quer seu aplicativo use um banco de dados em tempo real e dependa muito de streams ou se comunique com a API REST usando futuros, você já pode se beneficiar do novo gerador.

 

Exemplos de aplicativos com código-fonte

Até agora, vimos como criar provedores com a nova sintaxe @riverpod.

E se você está se perguntando como tudo isso se encaixa em aplicativos do mundo real, tenho boas notícias para você.

Na verdade, dois dos meus aplicativos Flutter de código aberto já usam o novo Riverpod Generator. 👇

1. Aplicativo de filmes TMDB

O primeiro é um aplicativo de filmes baseado nas APIs TMDB:

TMDB Movies app with Riverpod
Aplicativo TMDB Movies com Riverpod

 

Este aplicativo inclui suporte para:

  • rolagem infinita com paginação
  • puxe para atualizar
  • funcionalidade de pesquisa

Todos esses recursos são construídos nativamente com Riverpod (sem pacotes externos).

E como o aplicativo já usa Freezed para serialização JSON, adicionar o pacote riverpod_generator pareceu uma ótima opção.

O código-fonte inclui coisas que não abordamos aqui, como cancelar solicitações de rede com CancelToken do pacote dio.

Este aplicativo ainda é Trabalho em progresso e tentarei adicionar mais recursos no futuro.

Mas você já pode conferir aqui: 👇

Aplicativo TMDB Movies com Riverpod

 

Conclusão

Como vimos, o pacote riverpod_generator tem muito a oferecer. Aqui estão alguns motivos para usá-lo:

  • gerar automaticamente o tipo certo de provedor
  • muito mais fácil criar provedores com argumentos, superando as limitações da “antiga” sintaxe modificadora family
  • maior segurança de tipo e menos erros de tipo em tempo de execução
  • autoDispose por padrão

No entanto, alguns tipos de provedores legados não são suportados.

E como o novo pacote depende da geração de código, você precisa:

  • lidar com arquivos adicionais gerados automaticamente no projeto
  • decida se os arquivos gerados devem ser adicionados ao git e planeje adequadamente

Se você está em dúvida sobre a geração de código, considere esta opinião de Remi Rousselet:

O código gerado não é “padrão”. Você realmente não se importa com o código gerado. Na verdade, não foi feito para ser lido ou editado. Está lá para o compilador, não para desenvolvedores. Na verdade, você pode ocultá-lo do seu IDE Explorer e normalmente não confirma os arquivos gerados.

No geral, a vantagem mais significativa é a maior produtividade do desenvolvedor.

Usar a nova sintaxe significa que você precisa aprender e usar uma API menor e mais familiar. E isso torna o Riverpod mais acessível para desenvolvedores que ficavam confusos com as APIs antigas.

Mas só para ficar claro: riverpod_generator é um pacote opcional construído sobre o riverpod e a sintaxe “antiga” não irá desaparecer tão cedo.

E como a nova sintaxe do Riverpod é compatível com a antiga, você pode adotá-la de forma incremental ao migrar provedores em sua base de código.

Então, por que não tentar? 😎