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?
-
Repositório de demonstração: https://github.com/caneto/RiverPod3
-
Exemplos de apps:
apps/ -
Repositórios compartilhados:
packages/shared_repos/ -
Passo a passo da migração:
migration/
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
RetryLoggerexpõ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 noStateNotifierProvider. Verifiquelib/counter.dartpara 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 comref.onDisposee a mudança para oNotifierProvider.
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 oriverpod_lintpara capturar o uso incorreto dewatch/readem tempo de compilação. -
apps/counter_app: Já inclui oriverpod_generatore obuild_runner, de modo que o comandomelos run buildregenera 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
StreamProvidere 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.3 → fvm flutter --version → fvm 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.