Gerenciamento de estado em apps Flutter com Riverpod
1. Introdução ao Riverpod e seus diferenciais
O gerenciamento de estado é um dos desafios centrais no desenvolvimento de aplicações Flutter. Entre as diversas soluções disponíveis — Provider, BLoC, GetX, MobX — o Riverpod se destaca por oferecer uma abordagem moderna, segura e altamente testável.
Riverpod foi criado por Rémi Rousselet, o mesmo autor do Provider, mas resolve várias limitações do seu antecessor. Diferente do Provider, que depende da árvore de widgets para funcionar (context-dependent), o Riverpod é completamente independente do contexto do Flutter. Isso significa que os providers podem ser acessados de qualquer lugar, inclusive em serviços, repositórios e testes unitários.
Os conceitos fundamentais do Riverpod incluem:
- Providers: unidades centrais que expõem e gerenciam estado. Cada provider é um objeto global que pode ser lido e escutado.
- Ref: objeto que permite a um provider acessar outros providers, criando uma rede de dependências.
- Escopo global: ao contrário do Provider, não é necessário empilhar providers na árvore de widgets.
Comparado ao BLoC, o Riverpod oferece menos boilerplate. Comparado ao GetX, oferece maior segurança de tipos e previsibilidade. A imutabilidade é incentivada naturalmente, e a testabilidade é um dos seus maiores trunfos.
2. Instalação e configuração inicial
Para começar, adicione a dependência principal ao arquivo pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^2.5.1
riverpod_annotation: ^2.3.5
dev_dependencies:
flutter_test:
sdk: flutter
riverpod_generator: ^2.4.0
build_runner: ^2.4.9
O ponto de entrada da aplicação deve envolver toda a árvore de widgets com ProviderScope:
void main() {
runApp(const ProviderScope(child: MeuApp()));
}
A estrutura de pastas recomendada para projetos com Riverpod segue uma organização por funcionalidades ou camadas:
lib/
├── main.dart
├── app.dart
├── core/
│ └── providers/
├── features/
│ ├── auth/
│ │ ├── providers/
│ │ ├── models/
│ │ └── screens/
│ └── tasks/
│ ├── providers/
│ ├── models/
│ └── screens/
└── shared/
└── widgets/
3. Tipos de Providers essenciais
O Riverpod oferece diversos tipos de providers para diferentes necessidades.
Provider e StateProvider são os mais simples. Provider expõe um valor imutável (como uma instância de serviço), enquanto StateProvider permite atualizar o estado diretamente:
final contadorProvider = StateProvider<int>((ref) => 0);
// Uso no widget
final contador = ref.watch(contadorProvider);
ref.read(contadorProvider.notifier).state++;
FutureProvider é ideal para dados assíncronos, como requisições HTTP:
final usuarioProvider = FutureProvider<Usuario>((ref) async {
final repository = ref.watch(usuarioRepositoryProvider);
return repository.buscarUsuarioLogado();
});
StreamProvider lida com streams contínuas, como conexões em tempo real:
final mensagensProvider = StreamProvider<List<Mensagem>>((ref) {
final chatService = ref.watch(chatServiceProvider);
return chatService.ouvirMensagens();
});
StateNotifierProvider e NotifierProvider são usados para lógica de estado complexa, com métodos que modificam o estado de forma controlada:
class ListaTarefasNotifier extends StateNotifier<List<Tarefa>> {
ListaTarefasNotifier() : super([]);
void adicionar(Tarefa tarefa) {
state = [...state, tarefa];
}
void remover(String id) {
state = state.where((t) => t.id != id).toList();
}
}
final listaTarefasProvider = StateNotifierProvider<ListaTarefasNotifier, List<Tarefa>>((ref) {
return ListaTarefasNotifier();
});
4. Trabalhando com dependências e escopos
O modificador family permite criar providers parametrizados. Por exemplo, para buscar detalhes de um item específico:
final detalhesItemProvider = FutureProvider.family<Item, String>((ref, id) async {
final repository = ref.watch(itemRepositoryProvider);
return repository.buscarPorId(id);
});
// Consumo
final item = ref.watch(detalhesItemProvider('123'));
O modificador autoDispose libera recursos automaticamente quando nenhum widget está escutando o provider:
final temporizadorProvider = StreamProvider.autoDispose<int>((ref) {
return Stream.periodic(const Duration(seconds: 1), (x) => x);
});
Providers podem depender de outros providers, formando uma cadeia de dependências:
final configProvider = Provider<Config>((ref) => Config());
final saudacaoProvider = Provider<String>((ref) {
final config = ref.watch(configProvider);
return config.idioma == 'pt' ? 'Olá' : 'Hello';
});
5. Consumo de estado na interface do usuário
Para escutar mudanças de estado na UI, use ConsumerWidget ou o widget Consumer:
class TelaContador extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final contador = ref.watch(contadorProvider);
return Text('Valor: $contador');
}
}
As diferenças entre os métodos de acesso:
ref.watch: escuta mudanças e reconstrói o widget quando o estado muda.ref.read: lê o valor uma única vez, sem escutar. Use apenas em callbacks.ref.listen: executa uma ação quando o estado muda, sem reconstruir o widget.
Para otimizar rebuilds, use o seletor select:
final nome = ref.watch(usuarioProvider.select((u) => u.nome));
Isso faz com que o widget reconstrua apenas quando nome mudar, ignorando outras propriedades.
6. Gerenciamento de estado assíncrono e erros
Quando usamos FutureProvider, o valor exposto é um AsyncValue, que pode estar em três estados: loading, error ou data.
class TelaUsuario extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final asyncUsuario = ref.watch(usuarioProvider);
return asyncUsuario.when(
loading: () => const CircularProgressIndicator(),
error: (erro, stack) => Text('Erro: $erro'),
data: (usuario) => Text('Nome: ${usuario.nome}'),
);
}
}
Para combinar múltiplos FutureProviders, use ref.watch em um provider que depende de vários:
final perfilCompletoProvider = FutureProvider<PerfilCompleto>((ref) async {
final usuario = await ref.watch(usuarioProvider.future);
final preferencias = await ref.watch(preferenciasProvider.future);
return PerfilCompleto(usuario: usuario, preferencias: preferencias);
});
7. Testes e boas práticas com Riverpod
A testabilidade é um dos maiores diferenciais do Riverpod. Para testar providers isoladamente, use ProviderContainer:
void main() {
test('contadorProvider deve iniciar com 0', () {
final container = ProviderContainer();
final contador = container.read(contadorProvider);
expect(contador, 0);
});
test('deve incrementar corretamente', () {
final container = ProviderContainer();
container.read(contadorProvider.notifier).state++;
final contador = container.read(contadorProvider);
expect(contador, 1);
});
}
Para substituir dependências em testes, use ProviderScope.overrides:
final mockRepositoryProvider = Provider<UsuarioRepository>((ref) => MockRepository());
test('deve retornar usuário mockado', () {
final container = ProviderContainer(
overrides: [
usuarioRepositoryProvider.overrideWithProvider(mockRepositoryProvider),
],
);
// ...
});
Boas práticas para escalabilidade:
- Modularização: separe providers por domínio ou funcionalidade.
- Injeção de dependências: use providers para expor serviços e repositórios.
- Evite lógica complexa nos widgets: mantenha a lógica de negócio nos Notifiers.
- Prefira
NotifierProvidersobreStateNotifierProvider(mais moderno e flexível). - Use
autoDisposecom moderação: apenas quando houver recursos que precisam ser liberados.
Riverpod não é apenas mais uma ferramenta de gerenciamento de estado — é uma plataforma completa que promove código limpo, testável e escalável. Sua adoção em projetos Flutter tem crescido rapidamente, e com razão: ele resolve problemas reais de arquitetura sem introduzir complexidade desnecessária.
Referências
- Documentação oficial do Riverpod — Guia completo, referência de API e tutoriais introdutórios sobre todos os tipos de providers.
- Riverpod no pub.dev — Página oficial do pacote com detalhes de instalação, versões e changelog.
- Tutorial: Gerenciamento de estado com Riverpod (Flutter) — Tutorial oficial da equipe Flutter sobre integração com Riverpod.
- Riverpod: The Ultimate Guide (ResoCoder) — Guia prático e aprofundado com exemplos reais de arquitetura de apps.
- Riverpod vs Provider vs BLoC (FlutterBeans) — Comparativo técnico entre as principais soluções de gerenciamento de estado.