Módulo 07 — Exercícios: Gerenciamento de Estado com Provider
Estes exercícios foram elaborados para que você coloque em prática os conceitos de gerenciamento de estado com Provider estudados no material do Módulo 07. Gerenciar estado é, antes de mais nada, uma habilidade de raciocínio: é preciso saber identificar o que é estado, onde ele deve residir e quem tem o direito de modificá-lo. Tente resolver cada exercício sem consultar o material antes de chegar ao final do enunciado. O esforço de trabalhar com o que você reteve é o que consolida o aprendizado — especialmente neste tema, onde a compreensão dos mecanismos subjacentes é mais importante do que a memorização de APIs.
Exercício 1 — Nível Básico
Acompanhamento de Status de Pedido com ChangeNotifier e Consumer
O primeiro estado compartilhado: um notifier simples que conecta botões e textos sem setState
Todo estado compartilhado em uma aplicação Flutter com Provider começa com o mesmo passo: uma classe que estende ChangeNotifier. Antes de lidar com múltiplos providers, dados assíncronos ou injeção de dependências, é preciso dominar o ciclo fundamental: criar o notifier, registrá-lo na árvore com ChangeNotifierProvider, acessar os dados com Consumer ou context.watch, e modificar o estado com context.read. Este exercício constrói exatamente esse ciclo, do zero, em um cenário concreto do Projeto Integrador.
Você vai implementar a tela de acompanhamento de um pedido em andamento. O arquivo deve ser executável com flutter run e conter em um único arquivo todas as classes e o main necessários. O pubspec.yaml deve incluir a dependência provider: ^6.1.5+1.
O primeiro componente é o StatusPedidoNotifier, uma classe que estende ChangeNotifier. Ela deve representar o ciclo de vida de um pedido após a confirmação, com um campo privado _etapaAtual do tipo int inicializado com 0. Os valores possíveis de _etapaAtual são 0 (Pedido recebido), 1 (Em preparo), 2 (Saiu para entrega) e 3 (Entregue). O notifier deve ter um getter público etapaAtual que expõe o valor de _etapaAtual, um getter descricaoEtapa que retorna o texto correspondente à etapa atual — use um switch ou if-else para mapear cada valor inteiro ao texto correto —, e um getter etapaFinal do tipo bool que retorna true quando _etapaAtual == 3. Implemente dois métodos públicos: avancarEtapa(), que incrementa _etapaAtual em 1 apenas se _etapaAtual < 3 e depois chama notifyListeners(), e reiniciarPedido(), que redefine _etapaAtual para 0 e chama notifyListeners().
O widget raiz do aplicativo deve ser um StatelessWidget chamado MeuApp que retorna um ChangeNotifierProvider<StatusPedidoNotifier> com create: (_) => StatusPedidoNotifier(), cujo child é um MaterialApp com título 'Acompanhamento de Pedido' e home apontando para a tela principal.
O widget TelaAcompanhamento é um StatelessWidget com AppBar de título 'Meu Pedido' e corpo composto por uma Column centralizada. O primeiro elemento da coluna é um Consumer<StatusPedidoNotifier> cujo builder deve construir os seguintes elementos visuais: um ícone que muda conforme a etapa — Icons.receipt_long para a etapa 0, Icons.restaurant para a etapa 1, Icons.delivery_dining para a etapa 2 e Icons.check_circle para a etapa 3 —, com tamanho 80 e cor primária do tema; abaixo, um Text com o texto de descricaoEtapa no estilo headlineSmall; abaixo, um indicador de progresso visual implementado como uma Row com quatro _PontoProgresso separados por Expanded com Divider, onde cada _PontoProgresso é um CircleAvatar de raio 16 com a letra 'P' substituída pelo número da etapa somado a 1, preenchido com a cor primária do tema se a etapa correspondente já foi alcançada (ou seja, se o índice da etapa for menor ou igual a etapaAtual), e cinza caso contrário. O _PontoProgresso pode ser implementado como um widget auxiliar privado dentro do arquivo.
O segundo elemento da coluna é um Padding com SizedBox(height: 40) de espaçamento e, em seguida, um ElevatedButton com texto 'Avançar etapa' que usa context.read<StatusPedidoNotifier>().avancarEtapa() no onPressed, e cujo onPressed é null quando etapaFinal for verdadeiro — use context.watch<StatusPedidoNotifier>().etapaFinal para isso, mas atenção: você deve separar corretamente as chamadas a context.watch (dentro do build) e context.read (dentro do callback). Abaixo do botão de avançar, um TextButton com texto 'Reiniciar simulação' que chama reiniciarPedido() via context.read.
Antes de começar a codificar, responda mentalmente a estas perguntas. Por que o ChangeNotifierProvider deve envolver o MaterialApp em vez de envolver apenas a TelaAcompanhamento? Se você colocasse o provider abaixo do MaterialApp, quais widgets ainda conseguiriam acessar o StatusPedidoNotifier? E por que o ElevatedButton usa context.watch para verificar etapaFinal mas usa context.read para chamar avancarEtapa()? O que aconteceria se você usasse context.watch dentro do onPressed?
O indicador de progresso com Row, Expanded e Divider entre os pontos é a parte mais trabalhosa de construir visualmente. Uma forma de simplificar é usar um for com Iterable.generate(4, (i) => i) para criar os quatro _PontoProgresso e intercalar os Expanded(child: Divider()) entre eles manualmente. Preste atenção no número de divisores: entre quatro pontos, há três divisores, não quatro.
O que deve ser entregue: um arquivo chamado g_ex1.dart, onde g é o nome do seu grupo.
Exercício 2 — Nível Intermediário
Cardápio com Estado de Carregamento e Carrinho Reativo usando MultiProvider
Dois notifiers, um mesmo aplicativo: carregamento assíncrono, filtragem e carrinho compartilhado
Um aplicativo de pedidos de comida tem, no mínimo, dois pedaços de estado que precisam ser compartilhados entre múltiplas telas: o cardápio de produtos (que é carregado de forma assíncrona e pode ser filtrado) e o carrinho de compras (que é modificado de forma síncrona e cujos totais precisam aparecer em múltiplos lugares). Este exercício integra os dois em uma única tela funcional usando MultiProvider, com foco no gerenciamento correto dos estados de carregamento, dado e erro do cardápio, e na reatividade imediata do carrinho.
Você vai implementar a tela principal do aplicativo de pedidos com cardápio e carrinho. O arquivo deve ser executável com flutter run, incluir provider: ^6.1.5+1 no pubspec.yaml e implementar todas as classes necessárias em um único arquivo.
Comece definindo a classe de modelo ItemCardapio com campos finais id do tipo String, nome do tipo String, preco do tipo double e categoria do tipo String. Ela deve ter um construtor com parâmetros nomeados e obrigatórios.
O primeiro ChangeNotifier é o CardapioNotifier. Ele deve gerenciar os campos privados _itens do tipo List<ItemCardapio> inicializado vazio, _carregando do tipo bool inicializado como false, _erro do tipo String? inicializado como null, e _filtroCategoria do tipo String inicializado como ''. Os getters públicos são: itens, que retorna uma List<ItemCardapio> filtrada — se _filtroCategoria estiver vazio, retorna todos; caso contrário, retorna apenas os itens cuja categoria é igual ao filtro; carregando, que retorna _carregando; erro, que retorna _erro; filtroCategoria, que retorna _filtroCategoria; e categorias, que retorna uma List<String> com as categorias únicas presentes em _itens, ordenadas alfabeticamente. O método carregarCardapio() deve ser assíncrono: define _carregando = true, _erro = null e chama notifyListeners(); aguarda Future.delayed(const Duration(seconds: 2)); popula _itens com ao menos quatro instâncias de ItemCardapio com categorias variadas — por exemplo, dois lanches e duas bebidas; e no finally define _carregando = false e chama notifyListeners(). O método definirFiltro(String categoria) deve definir _filtroCategoria = categoria e chamar notifyListeners(). O método limparFiltro() deve definir _filtroCategoria = '' e chamar notifyListeners().
O segundo ChangeNotifier é o CarrinhoNotifier. Ele deve gerenciar um Map<String, int> _quantidades que mapeia o id de cada item à sua quantidade no carrinho. Os getters públicos são: totalItens, que retorna a soma de todos os valores do mapa; e valorTotal, que — para este exercício — pode ser calculado percorrendo os itens do cardápio. No entanto, como o CarrinhoNotifier não deve depender diretamente do CardapioNotifier, implemente valorTotal como um método que recebe uma List<ItemCardapio> como parâmetro e retorna o double calculado. Os métodos públicos são: quantidadeItem(String id), que retorna a quantidade atual do item no carrinho (ou 0 se não estiver); adicionarItem(String id), que incrementa a quantidade do item e chama notifyListeners(); e removerItem(String id), que decrementa a quantidade do item — se chegar a 0, remove o id do mapa — e chama notifyListeners().
No main, registre os dois notifiers com MultiProvider envolvendo o MaterialApp.
O widget TelaCardapio é um StatefulWidget. No initState, chame carregarCardapio() usando WidgetsBinding.instance.addPostFrameCallback para garantir que o context esteja disponível. O método build deve usar context.watch<CardapioNotifier>() para obter o estado do cardápio. A lógica de exibição deve seguir três casos: se carregando for verdadeiro, exibe CircularProgressIndicator centralizado; se erro não for nulo, exibe mensagem de erro com botão “Tentar novamente” que chama carregarCardapio(); caso contrário, exibe o conteúdo normal. O conteúdo normal é uma Column com: uma linha de filtros de categoria implementada como um SizedBox de altura 48 com ListView horizontal contendo um FilterChip para “Todos” e um FilterChip para cada categoria, usando context.read<CardapioNotifier>().definirFiltro(...) e context.read<CardapioNotifier>().limparFiltro() nos callbacks; e uma ListView.builder com os itens filtrados. Cada item da lista deve ser exibido como um Card com uma ListTile mostrando nome como title, 'R\$ ${preco.toStringAsFixed(2)}' como subtitle, e como trailing um widget que exibe a quantidade atual no carrinho e dois IconButtons — um com Icons.remove e outro com Icons.add — que chamam removerItem e adicionarItem via context.read<CarrinhoNotifier>(). Ao exibir a quantidade atual no carrinho dentro do trailing, você deve ler a quantidade usando context.watch<CarrinhoNotifier>().quantidadeItem(item.id) para garantir que o widget seja atualizado quando o carrinho mudar.
A AppBar da tela deve exibir o título 'Cardápio' e, nas actions, um Selector<CarrinhoNotifier, int> que seleciona apenas totalItens. O selector deve retornar carrinho.totalItens, e o builder deve exibir um Stack com um IconButton de ícone Icons.shopping_cart_outlined e, se o total for maior que zero, um Positioned com um Container circular vermelho exibindo o número total de itens.
O Selector<CarrinhoNotifier, int> na AppBar é o mecanismo que garante que o badge do carrinho seja atualizado sem reconstruir a tela inteira. Se você usasse context.watch<CarrinhoNotifier>() diretamente na AppBar, toda a AppBar seria reconstruída a cada mudança no carrinho — o que inclui o título, as ações e qualquer outro elemento. Com o Selector, apenas o widget dentro do builder é reconstruído, e somente quando o valor de totalItens efetivamente muda. Pense em como o Selector se compara ao Consumer neste aspecto: qual a diferença entre usar Selector que seleciona totalItens e usar Consumer que acessa o CarrinhoNotifier inteiro?
A parte mais sutil deste exercício é o controle de quais widgets são reconstruídos quando o carrinho muda. Se você envolver todo o ListView.builder com um único Consumer<CarrinhoNotifier>, toda a lista será reconstruída cada vez que qualquer item for adicionado ou removido. A abordagem correta é usar context.watch<CarrinhoNotifier>() de dentro do builder do ListView — ou seja, dentro do itemBuilder — para que apenas o card do item modificado seja atualizado. Como o ListView.builder chama o itemBuilder para cada item visível, e o context.watch é chamado dentro do build de cada card, o Flutter consegue rastrear quais cards dependem de quais partes do estado. Reflita sobre onde exatamente você vai posicionar as chamadas a context.watch e context.read para obter esse comportamento.
O que deve ser entregue: um arquivo chamado g_ex2.dart, onde g é o nome do seu grupo.
Exercício 3 — Nível Desafiador
Arquitetura Completa com GetIt, ChangeNotifierProxyProvider e Fluxo de Autenticação
O desafio que integra service locator, dependência entre providers e fluxo reativo completo
Em uma aplicação de produção, os ChangeNotifiers raramente operam sozinhos. Eles dependem de repositórios para buscar e salvar dados, e alguns deles dependem do estado de outros notifiers para funcionar corretamente. Este exercício implementa o padrão completo do Projeto Integrador: o GetIt como service locator para repositórios, o ChangeNotifierProxyProvider para criar dependências entre notifiers, e o fluxo de autenticação como o evento que dispara o carregamento reativo de dados de um segundo notifier. A complexidade aqui está na orquestração — fazer tudo trabalhar junto de forma coesa e previsível.
Você vai implementar o fluxo de autenticação e listagem de pedidos do Projeto Integrador. O arquivo deve ser executável com flutter run, incluir provider: ^6.1.5+1 e get_it: ^9.2.0 no pubspec.yaml e implementar todos os modelos, interfaces, repositórios, notifiers e widgets necessários em um único arquivo.
Os modelos de dados. Defina a classe Pedido com campos finais id do tipo String, descricao do tipo String, total do tipo double e status do tipo String. Ela deve ter um construtor com parâmetros nomeados e obrigatórios.
O service locator. Declare uma variável de nível superior final sl = GetIt.instance. Defina a classe abstrata IPedidoRepositorio com o método assíncrono Future<List<Pedido>> buscarPedidosDoUsuario(String idUsuario). Implemente a classe concreta PedidoRepositorioSimulado que implementa IPedidoRepositorio: o método buscarPedidosDoUsuario deve aguardar Future.delayed(const Duration(seconds: 1)) e retornar uma lista de dois Pedido com dados fictícios. Escreva a função configurarDependencias(), síncrona, que chama sl.registerLazySingleton<IPedidoRepositorio>(() => PedidoRepositorioSimulado()). Chame configurarDependencias() no início da função main, antes de runApp.
O UsuarioNotifier. Esta classe estende ChangeNotifier e gerencia os campos privados _nomeUsuario do tipo String? e _idUsuario do tipo String?, ambos inicializados como null. Os getters públicos são nomeUsuario, idUsuario e autenticado — este último retorna _idUsuario != null. O método fazerLogin(String id, String nome) define os dois campos e chama notifyListeners(). O método fazerLogout() redefine ambos como null e chama notifyListeners().
O PedidosNotifier. Esta classe estende ChangeNotifier e gerencia os campos privados _pedidos do tipo List<Pedido> inicializado vazio, _carregando do tipo bool inicializado como false, _erro do tipo String? inicializado como null e _idUsuarioAtual do tipo String? inicializado como null. Os getters públicos são pedidos, carregando e erro. O método mais importante é sincronizarComUsuario(String? idUsuario): se idUsuario for nulo (usuário deslogou), limpe _pedidos, redefina _idUsuarioAtual para null e chame notifyListeners(); se idUsuario for não nulo e diferente de _idUsuarioAtual (usuário logou ou mudou), armazene o novo idUsuario em _idUsuarioAtual, ative o carregamento, e chame o método privado _carregarPedidos(idUsuario). O método privado _carregarPedidos(String idUsuario) deve usar try/finally: no try, obtenha o repositório com sl<IPedidoRepositorio>(), chame buscarPedidosDoUsuario(idUsuario) e armazene o resultado em _pedidos; no catch, armazene a mensagem de erro em _erro; no finally, defina _carregando = false e chame notifyListeners().
O MultiProvider e o ChangeNotifierProxyProvider. No main, use MultiProvider com dois providers na seguinte ordem. O primeiro é ChangeNotifierProvider<UsuarioNotifier>(create: (_) => UsuarioNotifier()). O segundo é ChangeNotifierProxyProvider<UsuarioNotifier, PedidosNotifier>: o parâmetro create deve retornar PedidosNotifier(), e o parâmetro update deve receber (context, usuario, pedidosAnterior) e chamar pedidosAnterior!.sincronizarComUsuario(usuario.idUsuario) antes de retornar pedidosAnterior. Esta é a chave do exercício: sempre que o UsuarioNotifier chamar notifyListeners(), o update será chamado automaticamente, propagando o novo idUsuario para o PedidosNotifier.
As telas. Implemente dois widgets de tela. O TelaLogin é um StatelessWidget com AppBar de título 'Entrar' e corpo centralizado com dois ElevatedButtons: um com texto 'Entrar como João' que chama context.read<UsuarioNotifier>().fazerLogin('user-01', 'João Silva'), e outro com texto 'Entrar como Maria' que chama fazerLogin('user-02', 'Maria Souza'). O TelaPedidos é um StatelessWidget com AppBar de título 'Meus Pedidos' e uma action de IconButton com Icons.logout que chama context.read<UsuarioNotifier>().fazerLogout(). O corpo da TelaPedidos deve usar context.watch<PedidosNotifier>() e exibir três estados: se carregando, exibe CircularProgressIndicator; se erro != null, exibe mensagem de erro; caso contrário, exibe a lista de pedidos com ListView.builder, onde cada item é um ListTile com id como title, descricao como subtitle e o total formatado como trailing.
A navegação entre telas. O widget raiz deve ser TelaRaiz, um StatelessWidget que usa context.watch<UsuarioNotifier>().autenticado para decidir qual tela exibir: se autenticado, exibe TelaPedidos; caso contrário, exibe TelaLogin. Use um AnimatedSwitcher para transição suave entre as duas telas.
O aspecto mais delicado deste exercício é o método sincronizarComUsuario. Observe que ele deve comparar o idUsuario recebido com o _idUsuarioAtual armazenado antes de decidir se carrega os pedidos novamente. Sem essa comparação, cada notifyListeners() do UsuarioNotifier — inclusive os que não mudam o idUsuario — causaria uma nova carga de pedidos. Pense no que acontece quando o usuário toca em um botão que chama fazerLogout() e depois imediatamente toca em fazerLogin(): quantas vezes update do ChangeNotifierProxyProvider é chamado? Quantas vezes _carregarPedidos deve ser chamado nesse fluxo?
O AnimatedSwitcher exige que os widgets filhos tenham chaves diferentes para detectar a troca. Se você usar TelaLogin() e TelaPedidos() sem chaves explícitas, o AnimatedSwitcher pode não animar a transição corretamente porque o Flutter pode não reconhecer que o widget filho mudou. Use ValueKey('login') e ValueKey('pedidos') nos respectivos widgets para garantir a detecção correta da mudança.
O que deve ser entregue: um arquivo chamado g_ex3.dart, onde g é o nome do seu grupo.
Se você concluiu os três exercícios com sucesso, considere este desafio adicional para o exercício 3: como você testaria o PedidosNotifier de forma isolada, sem depender da implementação real do PedidoRepositorioSimulado? Reflita sobre como trocar a implementação registrada no GetIt por uma implementação falsa que retorna dados fixos instantaneamente — sem nenhuma espera assíncrona. Essa é a inversão de dependência em ação: a camada de domínio define o contrato (IPedidoRepositorio), e a infraestrutura se adapta a ele. No Módulo 15, quando você estudar testes automatizados, esse padrão será a base de todos os testes de unidade dos ChangeNotifiers.