Principais alterações e utilização prática do Riverpod 3.0

P. Por que deveríamos revisitar o Riverpod 3.0 agora mesmo?

O Riverpod 3.0 não é apenas um salto de versão — ele repensa como lidamos com estado e fluxos assíncronos. Quando o anúncio saiu, eu estava mantendo uma base de código em Riverpod 2.x e vivia me perguntando: “Parece familiar, mas o que realmente mudou?”. Essa pergunta me impulsionou a construir um repositório de exemplo reproduzível no Flutter 3.41.x + FVM e percorrer as mudanças que eu gostaria de validar antes de levar a atualização para produção.

P. Do que eu preciso para começar a experimentar?

Pontos de foco

  • Versões fixadas com FVM: Cada exemplo fixa as versões com FVM, para que você possa reproduzir o mesmo ambiente a qualquer momento.

  • Layout multi-pacote: A estrutura permite explorar aplicativos, repositórios compartilhados e códigos de migração lado a lado.

  • Suítes de testes inclusas: Estão inclusos conjuntos de testes para que você possa verificar o comportamento na prática, em vez de apenas confiar nas descrições.

P. Quais foram as mudanças mais notáveis no Riverpod 3.0?

1. O FutureProvider agora faz tentativas automáticas (Retries) com backoff exponencial

O FutureProvider pode tentar novamente chamadas que falharam de forma automática, em vez de forçar você a escrever blocos try-catch personalizados. Basta apenas ativar a política de tentativa (retry policy).

O que se destacou na prática:

  • Usuários em redes instáveis veem a interface “piscar e se recuperar” em vez de enfrentar falhas críticas.

  • Você pode desativar as tentativas por provider para cenários que precisam falhar rapidamente (fail fast).

  • Rastrear as tentativas de repetição tornou-se trivial, o que tornou o QA (controle de qualidade) e a depuração mais fluidos.

Experimente: apps/network_retry_demo

  • Alterne entre tentativas globais ou exceções específicas por provider diretamente na interface.
  • O RetryLogger expõe a contagem de tentativas como uma stream que tanto a interface quanto os testes consomem.
// apps/network_retry_demo/lib/main.dart
final userFutureProvider = FutureProvider.autoDispose<String>((ref) async {
  final enableRetry = ref.watch(retryToggleProvider);
  final disableForThis = ref.watch(disableForUserProvider);
  final logger = ref.watch(retryLoggerProvider);
  final service = _FlakyUserService(logger: logger);
  
  if (!enableRetry || disableForThis) {
    return service.fetchUser();
  }
  const maxAttempts = 3;
  for (var attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      logger.recordAttempt();
      return await service.fetchUser();
    } catch (err) {
      if (attempt == maxAttempts - 1) rethrow;
      await Future.delayed(Duration(milliseconds: 200 * pow(2, attempt).toInt()));
    }
  }
  throw StateError('Unreachable');
});

Teste unitário: apps/network_retry_demo/test/retry_test.dart

test('retry succeeds after transient failure', () async {
  final logger = RetryLogger();
  final container = ProviderContainer(overrides: [
    retryLoggerProvider.overrideWithValue(logger),
  ]);
  addTearDown(() {
    logger.dispose();
    container.dispose();
  });
  
  final result = await container.read(userFutureProvider.future);
  expect(result, equals('Riverpod Fan'));
});

2. A regeneração do Notifier significa: “extraia seus recursos”

A partir da versão 3.0, o Notifier e o AsyncNotifier criam instâncias novas a cada reconstrução (rebuild). O comportamento de “pseudo-singleton” da versão 2.x não se aplica mais; portanto, manter cronômetros (timers) ou controladores (controllers) dentro do notifier causa vazamento de recursos (memory leaks). A solução é separar esses recursos em providers e vincular o ciclo de vida deles com o ref.onDispose.

Repositório prático: apps/counter_app

// apps/counter_app/lib/counter.dart
final stopwatchRepoProvider = Provider<StopwatchRepo>((ref) {
  final repo = StopwatchRepo();
  ref.onDispose(repo.dispose);
  return repo;
});

class CounterNotifier extends Notifier<int> {
  late final StopwatchRepo _repo = ref.read(stopwatchRepoProvider);
  @override
  int build() {
    _repo.start();
    ref.onDispose(_repo.stop);
    return 0;
  }
  void increment() => state++;
  Future<void> persist() async {
    final repo = ref.read(persistenceRepoProvider);
    await repo.write('counter', state);
    if (!ref.mounted) return;
  }
}

Você pode comparar o código ‘antes e depois’ em migration/v2_style e migration/v3_final. A diferença entre ‘recursos dentro do notifier’ e ‘recursos providos + descartados externamente’ torna-se óbvia.

3. ref.mounted como uma rede de segurança para fluxos assíncronos

Tarefas assíncronas longas acabarão sendo concluídas após o descarte (disposal); o ref.mounted oferece uma proteção de apenas uma linha.

Future<void> persist() async {
  final repo = ref.read(persistenceRepoProvider);
  await repo write('counter', state);
  if (!ref.mounted) return; // prevent updates after dispose
}

Agora trato esse padrão como obrigatório e o valido em testes invalidando os providers e verificando se o ref.mounted muda para falso.

4. O StreamProvider pausa quando ninguém está ouvindo

O StreamProvider finalmente pausa as inscrições (subscriptions) quando não há ouvintes, evitando vazamentos de recursos silenciosos.

Demonstração: apps/stream_pause_demo Alterne o interruptor e o fluxo do cronômetro (ticker stream) pausa imediatamente, enquanto a interface exibe ‘Stream pausado’. O TickerRepo é injetado como um provider para que você possa reutilizá-lo em qualquer outro lugar.

// apps/stream_pause_demo/lib/main.dart
final tickerRepoProvider = Provider<TickerRepo>((ref) => TickerRepo());
final listenToggleProvider = NotifierProvider<ListenToggleNotifier, bool>(ListenToggleNotifier.new);
final tickStreamProvider = StreamProvider<int>((ref) {
  final ticker = ref.watch(tickerRepoProvider);
  return ticker.tick();
});

P. Como eu percorro os exemplos de migração? O diretório migration/ contém dois pacotes independentes para que você possa explorar tanto o estilo legado quanto o atualizado sem precisar ficar alternando entre branches.

  • migration/v2_style: Registra a abordagem da versão 2.x centrada no StateNotifierProvider. Verifique lib/counter.dart para relembrar como os recursos ficavam “vivos” dentro do notifier.

  • migration/v3_final: Aplica a refatoração da 3.0: recursos extraídos para providers, ciclo de vida gerenciado com ref.onDispose e a mudança para o NotifierProvider.

Cada pasta possui um guia (v2_style/README.md, v3_final/README.md) descrevendo as mudanças e como executar o código. Após usar fvm use 3.35.3, execute fvm flutter test para confirmar que ambos os pacotes ainda passam nos testes.

Diff representativo:

// v2_style/lib/counter.dart (StateNotifier baseline)
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
}
// v3_final/lib/counter.dart (Notifier + external resource)
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
  late final StopwatchRepo _repo;
  @override
  int build() {
    _repo = ref.read(stopwatchRepoProvider);
    _repo.start();
    ref.onDispose(_repo.stop);
    return 0;
  }
  void increment() => state++;
}

Seguir esse fluxo esclarece como se proteger contra a regeneração do notifier, transferindo os recursos para providers dedicados.

P. Como você organizou o ambiente de desenvolvimento? Migrar sem ferramentas adequadas parecia arriscado, então reuni a análise de código (linting), a geração de código e os fluxos de trabalho de múltiplos pacotes (multi-package) no mesmo esforço.

  • melos.yaml + FVM (fvm use 3.35.3): Mantêm os aplicativos e pacotes alinhados.

  • analysis_options.yaml: Ativa o riverpod_lint para capturar o uso incorreto de watch/read em tempo de compilação.

  • apps/counter_app: Já inclui o riverpod_generator e o build_runner, de modo que o comando melos run build regenera o código sob demanda.

P. Como podemos experimentar com Persistência e Mutações? O Riverpod 3.0 traz APIs experimentais de persistência e mutação. Elas ainda não são finais, mas você pode simular a experiência armazenando o estado e transmitindo as atualizações.

// packages/shared_repos/lib/src/persistence_repo.dart
class MockPersistenceRepo {
  final Map<String, Object?> _store = <String, Object?>{};
  final _streamController = StreamController<Map<String, Object?>>.broadcast();
  
   Future<void> write(String key, Object? value) async {
    _store[key] = value;
    _streamController.add(Map<String, Object?>.unmodifiable(_store));
  }
  Stream<Map<String, Object?>> get changes => _streamController.stream;
}

Pressionar ‘Persist snapshot’ na demonstração do contador grava o estado atual e exibe as alterações via StreamBuilder. Substitua pela sua camada de persistência real quando estiver pronto.

P. Qual checklist de migração funcionou melhor?

  • Mova recursos internos (Stopwatch, Timer, controllers, etc.) para providers e descarte-os via ref.onDispose.

  • Defina políticas de tentativa (retry) em níveis global e por provider, validadas por testes.

  • Encerre todo método assíncrono com if (!ref.mounted) return;.

  • Seja intencional com as inscrições do StreamProvider e o comportamento de pausa.

  • Adote ferramentas de linting, geração de código e multi-pacotes para identificar problemas precocemente.

  • Você pode seguir a sequência exata dentro do diretório migration/ — executar o código é mais rápido do que comparar branches.

P. Considerações finais? O Riverpod 3.0 vai muito além de um ajuste de sintaxe; ele remodela a história do estado e do assíncrono. Adote as tentativas automáticas, a regeneração de notifiers, o ref.mounted e a pausa do StreamProvider, e você eliminará uma quantidade surpreendente de riscos legados. Execute as demonstrações, leia os testes e decida quais atualizações se ajustam primeiro à sua base de código.

Fluxo de comandos sugerido: fvm use 3.35.3fvm flutter --versionfvm dart pub global run melos bootstrap → dentro de um diretório de app, execute fvm flutter run ou fvm flutter test.

Se você descobrir táticas de migração mais fluidas ou novas ideias de demonstração, me avise — a experiência compartilhada sempre torna a próxima atualização mais fácil.

Please follow and like us:
error0
fb-share-icon
Tweet 20
fb-share-icon20