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:

  1. 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.
  2. AsyncValue e o Fim das Telas em Branco: Tratar requisições assíncronas com FutureBuilder legado gerava código verboso com dezenas de verificações manuais de snapshot.connectionState e snapshot.hasError. O tipo selado AsyncValue<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, loading e error.
  3. 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

  1. Esquecer o ProviderScope na Raiz: Esquecer de encapsular a aplicação inteira em runApp(const ProviderScope(child: MeuApp())) fará a aplicação falhar imediatamente no primeiro widget que tentar acessar qualquer provider.
  2. Usar ref.watch Dentro de Callbacks de Botão: O método ref.watch() só deve ser utilizado no corpo de build() para registrar assinaturas reativas. Em listeners de clique (onPressed), utilize estritamente ref.read(provider.notifier).metodo().
  3. 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.