Capítulo 10: Gerenciamento de Estado com Riverpod (Providers, AsyncValue)
Capítulo 10: Gerenciamento de Estado com Riverpod (Providers, AsyncValue)
Especialização Multiplataforma com Flutter • Flutter 3.x & Dart 3
🗺️ Mapa Conceitual da Arquitetura Reativa do Riverpod 2.x
flowchart TD
APP["ProviderScope (Contêiner Global Fora da Árvore de Widgets)"] --> PROVIDERS["Providers Declarativos (Top-Level)"]
subgraph PROVIDER_TYPES["Tipos Canônicos de Providers"]
P_SYNC["Provider<T>: Computação pura derivada"]
P_NOTIFIER["NotifierProvider<Notifier, State>: Mutação de estado síncrona"]
P_ASYNC["FutureProvider<T> / AsyncNotifier: Chamadas de rede assíncronas"]
end
PROVIDERS --> P_SYNC
PROVIDERS --> P_NOTIFIER
PROVIDERS --> P_ASYNC
subgraph CONSUMPTION["Consumo Reativo na Camada de UI (ConsumerWidget)"]
WATCH["ref.watch(provider) -> Assina e reconstrói o nó cirurgicamente"]
READ["ref.read(provider.notifier) -> Invoca métodos em botões sem rebuild"]
LISTEN["ref.listen(provider, (prev, next) -> ...) -> Dispara SnackBars e Diálogos"]
end
P_ASYNC -->|"Retorna AsyncValue<T>"| PATTERN_WHEN["asyncValue.when(data: ..., loading: ..., error: ...)"]
PATTERN_WHEN --> WATCH
style APP fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style PROVIDERS fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
style P_ASYNC fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style PATTERN_WHEN fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style WATCH fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
🏛️ 1. O Paradigma do Riverpod 2.x e o Fim das Amarras do BuildContext
O gerenciamento de estado no ecossistema Flutter foi liderado durante anos pelo pacote provider. No entanto, o provider possuía restrições fundamentais: dependia inteiramente da árvore de widgets (BuildContext), disparava exceções de runtime (ProviderNotFoundException) caso um widget tentasse ler um estado fora do escopo e impedia a coexistência de dois providers do mesmo tipo:
- Compilação Segura sem BuildContext: O Riverpod (um anagrama de Provider) redesenhou o conceito de estado. Os providers são declarados como constantes globais e acessados através do objeto
WidgetRef. Se você declarar uma referência incorreta, o código nem sequer compila, eliminando qualquer risco de falha por “provedor não encontrado” em tempo de execução. AsyncValuee o Fim das Telas em Branco: Tratar requisições assíncronas comFutureBuilderlegado gerava código verboso com dezenas de verificações manuais desnapshot.connectionStateesnapshot.hasError. O tipo seladoAsyncValue<T>do Riverpod transforma o estado assíncrono em um fluxo funcional que obriga a implementação dos três ramos essenciais via método.when():data,loadingeerror.- Reconstrução Cirúrgica com
ref.watch: O Riverpod garante que apenas o menor widget que consome a propriedade alterada seja reconstruído. O restante do Scaffold e das árvores adjacentes permanece inalterado, preservando a taxa de quadros e a autonomia da bateria.
📊 2. Comparativo: Provider Tradicional vs Riverpod 2.x
| Aspecto de Engenharia | Provider Legado | Riverpod 2.x (Padrão Ouro) |
|---|---|---|
Dependência do BuildContext |
Obrigatória (acoplado à árvore de widgets) | Nenhuma (acessível em controladores e serviços via Ref) |
| Detecção de Erros de Escopo | Apenas em tempo de execução (crashes de tela) | 100% em tempo de compilação estática |
| Múltiplos Provedores do Mesmo Tipo | Impossível sem criar classes wrapper artificiais | Ilimitado (identificados pelo nome da variável) |
| Tratamento Assíncrono | Manual via FutureProvider e ConnectionState |
Nativo via AsyncValue com suporte a .when() |
| Testabilidade Unitária | Exige subir a árvore do Flutter nos testes | Testes puramente na VM Dart com ProviderContainer |
💻 3. Laboratório Prático: NotifierProvider e AsyncValue em Produção
O código abaixo apresenta a modelagem de um catálogo corporativo com carregamento assíncrono e gerenciamento de estado imutável com Riverpod:
// Arquivo: lib/estado_riverpod_producao.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Modelo de Domínio Imutável
@immutable
class Colaborador {
final int id;
final String nome;
final String departamento;
const Colaborador({required this.id, required this.nome, required this.departamento});
}
// 2. Provider Assíncrono de API (FutureProvider)
final colaboradoresApiProvider = FutureProvider<List<Colaborador>>((ref) async {
// Simula consulta HTTP remota
await Future.delayed(const Duration(milliseconds: 1000));
return const [
Colaborador(id: 1, nome: 'Eduardo Santana', departamento: 'Engenharia de Software'),
Colaborador(id: 2, nome: 'Mariana Duarte', departamento: 'Segurança da Informação'),
Colaborador(id: 3, nome: 'Lucas Alencar', departamento: 'DevOps & Cloud'),
];
});
// 3. Notifier Síncrono para Filtro de Pesquisa (NotifierProvider)
class FiltroDepartamentoNotifier extends Notifier<String?> {
@override
String? build() => null; // Sem filtro inicial
void definirFiltro(String? departamento) {
state = departamento;
}
}
final filtroDepartamentoProvider = NotifierProvider<FiltroDepartamentoNotifier, String?>(
FiltroDepartamentoNotifier.new,
);
// 4. Provider Derivado / Computado (Computed Provider)
final colaboradoresFiltradosProvider = Provider<AsyncValue<List<Colaborador>>>((ref) {
final filtro = ref.watch(filtroDepartamentoProvider);
final listaAsync = ref.watch(colaboradoresApiProvider);
return listaAsync.whenData((lista) {
if (filtro == null) return lista;
return lista.where((c) => c.departamento == filtro).toList();
});
});
// 5. Widget de Apresentação (ConsumerWidget)
class TelaGestaoColaboradores extends ConsumerWidget {
const TelaGestaoColaboradores({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// ref.watch garante que este widget seja reconstruído quando os dados chegarem ou o filtro mudar
final dadosAsync = ref.watch(colaboradoresFiltradosProvider);
final filtroAtivo = ref.watch(filtroDepartamentoProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Colaboradores (Riverpod 2.x)'),
actions: [
IconButton(
icon: Icon(filtroAtivo == null ? Icons.filter_alt_outlined : Icons.filter_alt),
onPressed: () {
// Alterna filtro via ref.read sem reconstruir desnecessariamente
final novoFiltro = filtroAtivo == null ? 'Engenharia de Software' : null;
ref.read(filtroDepartamentoProvider.notifier).definirFiltro(novoFiltro);
},
),
],
),
body: dadosAsync.when(
data: (colaboradores) {
if (colaboradores.isEmpty) {
return const Center(child: Text('Nenhum colaborador encontrado com este filtro.'));
}
return ListView.separated(
padding: const EdgeInsets.all(12),
itemCount: colaboradores.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) {
final item = colaboradores[index];
return ListTile(
leading: CircleAvatar(child: Text('${item.id}')),
title: Text(item.nome, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(item.departamento),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator.adaptive()),
error: (erro, stack) => Center(child: Text('Erro no carregamento: $erro')),
),
);
}
}
⚠️ 4. Armadilhas Comuns no Uso do Riverpod
- Esquecer o
ProviderScopena Raiz: Esquecer de encapsular a aplicação inteira emrunApp(const ProviderScope(child: MeuApp()))fará a aplicação falhar imediatamente no primeiro widget que tentar acessar qualquer provider. - Usar
ref.watchDentro de Callbacks de Botão: O métodoref.watch()só deve ser utilizado no corpo debuild()para registrar assinaturas reativas. Em listeners de clique (onPressed), utilize estritamenteref.read(provider.notifier).metodo(). - Mutação Direta de Objetos no Estado: Modificar internamente campos de uma classe sem gerar uma nova instância imutável impede que o Riverpod detecte a alteração através do operador
!=de igualdade estrutural, impedindo a reconstrução da interface.