Como usar Notifier e AsyncNotifier com o novo Flutter Riverpod Generator

Tempo de leitura: 12 minutes

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

 

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 um int (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 estende AutoDisposeNotifier<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 de StateNotifier<AsyncValue<void>>.
  • Precisamos substituir o método build e retornar o valor inicial (ou nada se o tipo de retorno for void).
  • No método signInAnonymously, lemos outro provedor com o objeto ref, mesmo que não tenhamos declarado explicitamente ref 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 o StateNotifier é 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 estende AutoDisposeAsyncNotifier<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 um AsyncError) 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 e StreamNotifier são variantes de classe dos bons e velhos FutureProvider e StreamProvider. Se você precisa assistir a um Future ou Stream, 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.