Como usar Notifier e AsyncNotifier com o novo Flutter Riverpod Generator
Escrever aplicativos Flutter usando Riverpod ficou muito mais fácil com a introdução do pacote riverpod_generator.
Usando a nova sintaxe Riverpod, usamos a anotação @riverpod
e deixamos build_runner
gerar todos os provedores instantaneamente.
Já abordei todos os princípios básicos neste artigo:
Como gerar automaticamente seus provedores com Flutter Riverpod Generator
E neste artigo, iremos adiante e aprenderemos sobre as classes Notifier
, AsyncNotifier
e StreamNotifier
que foram adicionadas ao Riverpod 2.3.
Essas classes pretendem substituir StateNotifier
e trazer alguns novos benefícios:
- mais fácil de executar inicialização complexa e assíncrona
- API mais ergonômica: não é mais necessário passar
ref
- não é mais necessário declarar os provedores manualmente (se usarmos o Riverpod Generator)
Ao final, você saberá como criar classes de estado personalizadas com o mínimo de esforço e gerar rapidamente provedores complexos usando riverpod_generator.
Preparar? Vamos! 🔥
Este artigo pressupõe que você já esteja familiarizado com o Riverpod. Se você é novo no Riverpod, leia: Flutter Riverpod 2.x: O Guia Definitivo
Conteudo
O que iremos cobrir
Para tornar este tutorial mais fácil de seguir, usaremos dois exemplos.
1. Contador Simples
O primeiro exemplo será um contador simples baseado em StateProvider
.
Iremos convertê-lo para o novo Notifier
e aprender sobre sua sintaxe.
Depois disso, adicionaremos Riverpod Generator
à mistura e veremos como gerar o NotifierProvider
correspondente automaticamente.
2. Controlador de autenticação
A seguir, estudaremos um exemplo mais complexo com alguma lógica assíncrona baseada em StateNotifier
.
Iremos convertê-lo para usar a nova classe AsyncNotifier
e aprender algumas nuances em torno da inicialização assíncrona.
E também converteremos isso para usar o Riverpod Generator e gerar o AsyncNotifierProvider
correspondente.
Por fim, resumiremos os benefícios do Notifier
e do AsyncNotifier
, para que você possa escolher se deseja usá-los em seus aplicativos.
E também compartilharei alguns códigos-fonte mostrando como tudo se encaixa.
Vamos mergulhar! 👇
Um contador simples com StateProvider
Como primeiro passo, vamos considerar um StateProvider
simples, junto com um CounterWidget
que o utiliza:
// 1. declare um [StateProvider] final counterProvider = StateProvider<int>((ref) { return 0; }); // 2. crie uma subclasse [ConsumerWidget] class CounterWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // 3. observe o provedor e reconstrua quando o valor mudar final counter = ref.watch(counterProvider); return ElevatedButton( // 4. use o valor child: Text('Value: $counter'), // 5. alterar o estado dentro de um retorno de chamada de botão onPressed: () => ref.read(counterProvider.notifier).state++, ); } }
Nada sofisticado aqui:
- podemos observar o valor do contador no método
build
- podemos incrementá-lo no retorno de chamada do botão
Como podemos ver, StateProvider
é fácil de declarar:
final counterProvider = StateProvider<int>((ref) { return 0; });
Isto é ideal para armazenar e atualizar variáveis simples como o contador acima.
Mas StateProvider
não funciona bem se o seu estado precisar de alguma lógica de validação ou se você precisar representar objetos mais complexos.
E embora StateNotifier
seja uma alternativa adequada para casos mais avançados, agora é recomendado usar a nova classe Notifier
. 👇
Como funciona o Notificador?
Veja como podemos declarar uma classe Counter
baseada na classe Notifier
.
// counter.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; class Counter extends Notifier<int> { @override int build() { return 0; } void increment() { state++; }
Duas coisas a serem observadas:
- temos um método
build
que retorna umint
(o valor inicial) - podemos (opcionalmente) adicionar um método para incrementar o state (nosso valor do contador)
Se quisermos criar um provedor para esta classe, podemos fazer isto:
final counterProvider = NotifierProvider<Counter, int>(() { return Counter(); });
Alternativamente, podemos usar Counter.new
como um construtor destacável:
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
Usando o NotifierProvider no widget
Acontece que podemos usar counterProvider
em nosso CounterWidget
sem nenhuma alteração, desde que importemos o arquivo counter.dart
:
import 'counter.dart'; class CounterWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // 1. observe o provedor e reconstrua quando o valor mudar final counter = ref.watch(counterProvider); return ElevatedButton( // 2. use o valor child: Text('Value: $counter'), // 3. alterar o estado dentro de um retorno de chamada de botão onPressed: () => ref.read(counterProvider.notifier).state++, ); } }
E como também temos um método de increment
, podemos fazer isso se desejarmos:
onPressed: () => ref.read(counterProvider.notifier).increment(),
O método increment
torna nosso código mais expressivo. Mas é opcional, pois ainda podemos modificar o estado diretamente, se quisermos.
StateProvider x NotifierProvider
Até agora, aprendemos que StateProvider
funciona bem quando precisamos modificar variáveis simples.
Mas se nosso estado (e a lógica para atualizá-lo) for mais complexo, Notifier
e NotifierProvider
são uma boa alternativa e ainda fácil de implementar:
// counter.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; class Counter extends Notifier<int> { @override int build() { return 0; } void increment() { state++; } } final counterProvider = NotifierProvider<Counter, int>(Counter.new);
E se quisermos podemos automatizar a geração do provedor. 👇
Notificador com Gerador Riverpod
Veja como podemos declarar a mesma classe Counter
usando a nova sintaxe @riverpod
:
import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'counter.g.dart'; @riverpod class Counter extends _$Counter { @override int build() { return 0; } void increment() { state++; } }
Observe como, neste caso, estendemos _$Counter
em vez de Notifier<int>
.
E se executarmos flutter pub run build_runner watch
, o arquivo counter.g.dart
será gerado para nós, com este código dentro dele:
/// See also [Counter]. final counterProvider = AutoDisposeNotifierProvider<Counter, int>( Counter.new, name: r'counterProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : $CounterHash, ); typedef CounterRef = AutoDisposeNotifierProviderRef<int>; abstract class _$Counter extends AutoDisposeNotifier<int> { @override int build(); }
As duas principais coisas a serem observadas são:
- um
counterProvider
foi criado para nós _$Counter
estendeAutoDisposeNotifier<int>
AutoDisposeNotifier
é definido assim dentro do pacote Riverpod:
/// {@template riverpod.notifier} abstract class AutoDisposeNotifier<State> extends BuildlessAutoDisposeNotifier<State> { /// {@macro riverpod.asyncnotifier.build} @visibleForOverriding State build(); }
Como podemos ver, o método build
retorna um tipo State
genérico.
Mas como isso está relacionado à nossa classe Counter
e como o Riverpod Generator sabe qual tipo usar?
A resposta é que _$Counter
estende AutoDisposeNotifier<int>
, e a propriedade state também é um int
porque definimos o método build
para retornar um int
.
Depois de decidirmos qual tipo usar, precisamos usá-lo de forma consistente se quisermos evitar erros em tempo de compilação.
E dentro da nossa classe de widget, todo o código continuará funcionando enquanto importarmos o arquivo counter.g.dart
gerado:
import 'counter.g.dart'; class CounterWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // 1. observe o provedor e reconstrua quando o valor mudar final counter = ref.watch(counterProvider); return ElevatedButton( // 2. use o valor child: Text('Value: $counter'), // 3. alterar o estado dentro de um retorno de chamada de botão onPressed: () => ref.read(counterProvider.notifier).state++, ); } }
StateProvider ou Notificador?
Já cobrimos alguns conceitos importantes, então vamos fazer um breve resumo antes de prosseguir.
StateProvider
ainda é a maneira mais fácil de armazenar estados simples:
final counterProvider = StateProvider<int>((ref) { return 0; });
Mas também podemos fazer o mesmo com uma subclasse Notifier
e um NotifierProvider
:
// counter.dart import 'package:flutter_riverpod/flutter_riverpod.dart'; class Counter extends Notifier<int> { @override int build() { return 0; } void increment() { state++; } } final counterProvider = NotifierProvider<Counter, int>(Counter.new);
Isso é mais detalhado, mas também mais flexível, pois podemos adicionar métodos com lógica complexa às nossas subclasses Notifier
(muito parecido com o que fazemos com StateNotifier
).
E se quisermos, podemos usar a nova sintaxe @riverpod
e gerar automaticamente o counterProvider
:
import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'counter.g.dart'; @riverpod class Counter extends _$Counter { @override int build() { return 0; } void increment() { state++; } } // counterProvider will be generated by build_runner
A classe Counter
que criamos é um exemplo simples de como armazenar algum estado síncrono.
E como veremos, podemos criar classes de estado assíncronas usando AsyncNotifier
e substituir totalmente StateNotifier
e StateNotifierProvider
. 👇
Um exemplo mais complexo com StateNotifier
Se você já usa o Riverpod há algum tempo, estará acostumado a escrever subclasses StateNotifier
para armazenar algum estado imutável que seus widgets possam ouvir.
Por exemplo, podemos querer fazer login do usuário usando uma classe de botão personalizada:
E para implementar a lógica de login, poderíamos criar a seguinte subclasse StateNotifier
:
class AuthController extends StateNotifier<AsyncValue<void>> { AuthController(this.ref) // definir o estado inicial (de forma síncrona) : super(const AsyncData(null)); final Ref ref; Future<void> signInAnonymously() async { // leia o repositório usando ref final authRepository = ref.read(authRepositoryProvider); // definir o estado de carregamento state = const AsyncLoading(); // faça login e atualize o estado (dados ou erro) state = await AsyncValue.guard(authRepository.signInAnonymously); } }
Podemos usar isso para fazer login chamando o método signInAnonymously
do AuthRepository
.
Quando fazemos trabalho assíncrono dentro de um notificador, podemos definir o estado mais de uma vez. Dessa forma, o widget pode reconstruir e mostrar a UI correta para cada estado possível (dados, carregamento e erro).
Também precisamos criar o StateNotifierProvider
correspondente, para que possamos chamar watch, read ou listen dentro do nosso widget:
final authControllerProvider = StateNotifierProvider< AccountScreenController, AsyncValue<void>>((ref) { return AuthController(ref); });
Este provedor pode ser usado para obter o controlador e chamar signInAnonymously()
dentro de um botão callback:
onPressed: () => ref.read(authControllerProvider.notifier).signInAnonymously(),
Embora não haja nada de errado com essa abordagem, StateNotifier
não pode ser inicializado de forma assíncrona.
E a sintaxe para declarar um StateNotifierProvider
é um pouco desajeitada, pois precisa de duas anotações de tipo.
Então, podemos usá-lo para fazer algo assim?
@riverpod class AuthController extends StateNotifier<AsyncValue<void>> { ... }
Se tentarmos isso e executarmos flutter pub run build_runner watch
, obteremos este erro:
Provider classes must contain a method named `build`.
Acontece que não podemos usar a sintaxe @riverpod
com StateNotifier
.
E deveríamos usar a nova classe AsyncNotifier
. 👇
Como funciona o AsyncNotifier?
Os documentos do Riverpod definem AsyncNotifier
como uma implementação do Notifier que é inicializada de forma assíncrona.
E aqui está como podemos usá-lo para converter nossa classe AuthController
:
// 1. adicione as importações necessárias import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; // 2. estender [AsyncNotifier] class AuthController extends AsyncNotifier<void> { // 3. substitua o método [build] para retornar um [FutureOr] @override FutureOr<void> build() { // 4. retornar um valor (ou não fazer nada se o tipo de retorno for nulo) } Future<void> signInAnonymously() async { // 5. leia o repositório usando ref final authRepository = ref.read(authRepositoryProvider); // 6. definir o estado de carregamento state = const AsyncLoading(); // 7. faça login e atualize o estado (dados ou erro) state = await AsyncValue.guard(authRepository.signInAnonymously); } }
Algumas observações:
- A classe base é
AsyncNotifier<void>
em vez deStateNotifier<AsyncValue<void>>
. - Precisamos substituir o método
build
e retornar o valor inicial (ou nada se o tipo de retorno forvoid
). - No método
signInAnonymously
, lemos outro provedor com o objetoref
, mesmo que não tenhamos declarado explicitamenteref
como uma propriedade (mais sobre isso abaixo).
Observe também o uso de FutureOr
: um tipo que representa valores que são Future<T>
ou T
. Isso é útil em nosso exemplo porque o tipo subjacente é void
e não temos nada para retornar.
Uma vantagem do
AsyncNotifier
sobre oStateNotifier
é que ele nos permite inicializar o estado de forma assíncrona
Declarando o AsyncNotifierProvider
Antes de podermos usar o AuthController
atualizado, precisamos declarar o AsyncNotifierProvider
correspondente:
final authControllerProvider = AsyncNotifierProvider<AuthController, void>(() { return AuthController(); });
Ou, usando um separador de construtor:
final authControllerProvider = AsyncNotifierProvider<AuthController, void>(AuthController.new);
Observe como a função que cria o provedor não possui um argumento ref
.
No entanto, ref
está sempre acessível como uma propriedade dentro das subclasses Notifier
ou AsyncNotifier
, facilitando a leitura de outros provedores.
Isso é diferente do StateNotifier
, onde precisamos passar ref
explicitamente como um argumento construtor se quisermos usá-lo.
Nota sobre autoDispose
Observe que se você declarar um AsyncNotifier
e o AsyncNotifierProvider
correspondente usando autoDispose
assim:
class AuthController extends AsyncNotifier<void> { ... } // nota: isso produzirá um erro de execução final authControllerProvider = AsyncNotifierProvider.autoDispose<AuthController, void>(AuthController.new);
Então você receberá um erro de tempo de execução:
Error: Type argument 'AuthController' doesn't conform to the bound 'AutoDisposeAsyncNotifier<T>' of the type variable 'NotifierT' on 'AutoDisposeAsyncNotifierProviderBuilder.call'.
A maneira correta de usar AsyncNotifier
com autoDispose
é estender a classe AutoDisposeAsyncNotifier
:
// usando AutoDispose AsyncNotifier class Counter extends AutoDisposeAsyncNotifier<int> { ... } // usando AsyncNotifierProvider.autoDispose final authControllerProvider = AsyncNotifierProvider.autoDispose<AuthController, void>(AuthController.new);
O modificador .autoDispose
pode ser usado para redefinir o estado do provedor quando todos os ouvintes são removidos.
A boa notícia é que não precisamos nos preocupar com a sintaxe correta se usarmos o Riverpod Generator. 👇
AsyncNotifier com gerador Riverpod
Assim como usamos a nova sintaxe @riverpod
com Notifier
, podemos fazer o mesmo com AsyncNotifier
.
Veja como podemos converter o AuthController
para usá-lo:
// 1. importe isto import 'package:riverpod_annotation/riverpod_annotation.dart'; // 2. declarar um arquivo de peça part 'auth_controller.g.dart'; // 3. anotar @riverpod // 4. estender assim class AuthController extends _$AuthController { // 5. substitua o método [build] para retornar um [FutureOr] @override FutureOr<void> build() { // 6. retornar um valor (ou não fazer nada se o tipo de retorno for nulo) } Future<void> signInAnonymously() async { // 7. leia o repositório usando ref final authRepository = ref.read(authRepositoryProvider); // 8. definir o estado de carregamento state = const AsyncLoading(); // 9. faça login e atualize o estado (dados ou erro) state = await AsyncValue.guard(authRepository.signInAnonymously); } }
Como resultado, a classe base é _$AuthController
e é gerada automaticamente.
E se olharmos o código gerado, encontramos o seguinte:
/// Veja também [AuthController]. final authControllerProvider = AutoDisposeAsyncNotifierProvider<AuthController, void>( AuthController.new, name: r'authControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : $AuthControllerHash, ); typedef AuthControllerRef = AutoDisposeAsyncNotifierProviderRef<void>; abstract class _$AuthController extends AutoDisposeAsyncNotifier<void> { @override FutureOr<void> build(); }
As duas principais coisas a serem observadas são:
- um
authControllerProvider
foi criado para nós _$AuthController
estendeAutoDisposeAsyncNotifier<void>
.
Por sua vez, esta classe é definida assim dentro do pacote Riverpod:
/// {@macro riverpod.asyncnotifier} abstract class AutoDisposeAsyncNotifier<State> extends BuildlessAutoDisposeAsyncNotifier<State> { /// {@macro riverpod.asyncnotifier.build} @visibleForOverriding FutureOr<State> build(); }
Desta vez, o método build retorna um FutureOr<State>
.
Aqui está nossa classe AuthController
mais uma vez:
Como podemos ver no diagrama acima, estamos lidando com void
, FutureOr<void>
e AsyncValue<void>
.
Mas como esses tipos estão relacionados?
Bem, o tipo da propriedade state é AsyncValue<void>
porque o tipo de retorno do método de construção é FutureOr<void>
.
E isso significa que podemos definir o estado como AsyncData
, AsyncLoading
ou AsyncError
no método signInAnonymously
.
StateNotifier ou AsyncNotifier?
Para efeito de comparação, aqui está a implementação anterior baseada em StateNotifier
:
class AuthController extends StateNotifier<AsyncValue<void>> { AuthController(this.ref) : super(const AsyncData(null)); final Ref ref; Future<void> signInAnonymously() async { final authRepository = ref.read(authRepositoryProvider); state = const AsyncLoading(); state = await AsyncValue.guard(authRepository.signInAnonymously); } } final authControllerProvider = StateNotifierProvider<AuthController, AsyncValue<void>>((ref) { return AuthController(ref); });
E aqui está o novo:
@riverpod class AuthController extends _$AuthController { @override FutureOr<void> build() { // retornar um valor (ou não fazer nada se o tipo de retorno for nulo) } Future<void> signInAnonymously() async { final authRepository = ref.read(authRepositoryProvider); state = const AsyncLoading(); state = await AsyncValue.guard(authRepository.signInAnonymously); } }
Com a sintaxe @riverpod
, há menos código para escrever, pois não precisamos mais declarar o provedor manualmente.
E como ref
está disponível como propriedade para todas as subclasses do Notifier
, não precisamos distribuí-lo.
E o código em nossos widgets permanece o mesmo, pois podemos assistir, ler ou ouvir o authControllerProvider
como fizemos antes.
Exemplo com inicialização assíncrona
Como AsyncNotifier
suporta inicialização assíncrona, podemos escrever código como este:
@riverpod class SomeOtherController extends _$SomeOtherController { @override // observe o tipo de retorno [Future] e a palavra-chave async Future<String> build() async { final someString = await someFutureThatReturnsAString(); return anotherFutureThatReturnsAString(someString); } // outros métodos aqui }
Nesse caso, o método build
é verdadeiramente assíncrono e só retornará quando o futuro for concluído.
Mas o método build
de qualquer widget de ouvinte precisa retornar de forma síncrona e não pode esperar a conclusão do futuro:
class SomeWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { // retorna AsyncLoading no primeiro carregamento, // reconstrói com o novo valor quando a inicialização for concluída final valueAsync = ref.watch(someOtherControllerProvider); return valueAsync.when(...); } }
Para lidar com isso, o controlador emitirá dois estados e o widget será reconstruído duas vezes:
- uma vez com um valor
AsyncLoading
temporário no primeiro carregamento - novamente com o novo valor
AsyncData
(ou umAsyncError
) quando a inicialização for concluída
Por outro lado, se usarmos um Notifier
síncrono ou um AsyncNotifier
com um método build
que retorna FutureOr
e não está marcado como async
, o estado inicial estará imediatamente disponível e o widget só será compilado uma vez quando carregado pela primeira vez.
Exemplo: passando argumentos para um AsyncNotifier
Às vezes, pode ser necessário passar argumentos adicionais para um AsyncNotifier.
Isso é feito declarando-os como parâmetros nomeados ou posicionais no método build:
@riverpod class SomeOtherController extends _$SomeOtherController { @override // você pode adicionar parâmetros nomeados ou posicionais ao método build Future<String> build(int someValue) async { final someString = await someFutureThatReturnsAString(someValue); return anotherFutureThatReturnsAString(someString); } // outros métodos aqui }
Em seguida, você pode simplesmente passá-los como argumentos ao assistir, ler ou ouvir o provedor:
// este provedor recebe um argumento posicional do tipo int final state = ref.watch(someOtherControllerProvider(42));
A sintaxe para declarar e passar argumentos para um AsyncNotifier
ou qualquer outro provedor é a mesma. Afinal, eles são apenas argumentos de funções regulares, e o Riverpod Generator cuida de tudo para nós. Para obter mais detalhes, leia: Criando e lendo um FutureProvider anotado do meu artigo anterior.
Novidade no Riverpod 2.x: StreamNotifier
Com o lançamento do Riverpod Generator, agora é possível gerar um provedor que retorne um Stream
:
@riverpod Stream<int> values(ValuesRef ref) { return Stream.fromIterable([1, 2, 3]); }
E se usarmos o pacote Riverpod Lint, podemos converter o provedor acima em uma variante “stateful”:
Este é o resultado:
@riverpod class Values extends _$Values { @override Stream<int> build() { return Stream.fromIterable([1, 2, 3]); } }
E nos bastidores, build_runner
irá gerar um StreamNotifier
e o AutoDisposeStreamNotifierProvider
correspondente.
AsyncNotifier
eStreamNotifier
são variantes de classe dos bons e velhosFutureProvider
eStreamProvider
. Se você precisa assistir a umFuture
ouStream
, mas também adicionar métodos para realizar algumas mutações de dados, uma variante de classe é a melhor opção.
Notifier e AsyncNotifier: valem a pena?
Por muito tempo, StateNotifier
nos serviu bem, dando-nos um local para armazenar estados complexos e a lógica para modificá-los fora da árvore de widgets.
Notifier
e AsyncNotifier
pretendem substituir StateNotifier
e trazer alguns novos benefícios:
- mais fácil de executar inicialização complexa e assíncrona
- API mais ergonômica: não é mais necessário passar
ref
- não é mais necessário declarar os provedores manualmente (se usarmos o Riverpod Generator)
Para novos projetos, esses benefícios valem a pena, pois as novas classes ajudam você a realizar mais com menos código.
Mas se você tiver muito código pré-existente usando StateNotifier
, cabe a você decidir se (ou quando) migrará para a nova sintaxe.
De qualquer forma, o StateNotifier
ainda existirá por um tempo e você poderá migrar seus provedores um de cada vez, se desejar.
Source do projeto git : (Link)
Como testar subclasses AsyncNotifier?
Uma coisa que não abordamos aqui é escrever testes unitários para as subclasses Notifier
e AsyncNotifier
.
Será demonstrado no próximo artigo.
Conclusão
Desde que foi introduzido, o Riverpod evoluiu de uma solução simples de gerenciamento de estado para um cache reativo e estrutura de vinculação de dados.
Riverpod facilita o trabalho com dados assíncronos, graças a classes como FutureProvider
, StreamProvider
e AsyncValue
.
Da mesma forma, as novas classes Notifier
, AsyncNotifier
e StreamNotifier
facilitam a criação de classes de estado personalizadas usando uma API ergonômica.
Descobrir a sintaxe correta para todas as combinações de provedores e modificadores (autoDispose
e family
) foi um grande problema com o Riverpod
.
Mas com o novo pacote riverpod_generator
, todos esses problemas desaparecem, pois você pode aproveitar o build_runner
e gerar todos os provedores dinamicamente.
E com o novo pacote riverpod_lint
, obtemos regras de lint específicas do Riverpod e assistências de código que nos ajudam a acertar a sintaxe.