Como gerar automaticamente seus provedores com Flutter Riverpod Generator
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
eStreamProvider
) - gerenciar o estado do aplicativo local (com
StateProvider
,StateNotifierProvider
eChangeNotifierProvider
)
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).
Conteudo
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
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
comoAutoDisposeProviderRef<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 classeRepositório
. Em vez disso, crie uma função global separada que retorne uma instância desseRepositó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
eChangeNotifier
não são suportados pelo novo gerador, portanto você ainda não pode converter o código existente usandoStateNotifierProvider
eChangeNotifierProvider
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
eMoviesRepositoryRef
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:
Provedor
FutureProvider
StreamProvider
NotifierProvider
(novo no Riverpod 2.0)AsyncNotifierProvide
r (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:
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? 😎