Módulo 07 — Gerenciamento de Estado com Provider

Você chegou ao módulo que separa aplicações Flutter básicas de aplicações Flutter profissionais. Nos módulos anteriores, você aprendeu a construir interfaces sofisticadas, a navegar entre telas com o go_router e a coletar dados com formulários validados. Em todas essas situações, o estado — o conjunto de dados que mudam ao longo do tempo e que a interface precisa refletir — foi gerenciado dentro de widgets individuais com setState. Essa abordagem funciona bem para estado local e isolado. Mas agora, à medida que o Projeto Integrador cresce, você vai perceber que informações como o carrinho de compras, o usuário autenticado e a lista de pedidos precisam ser acessadas e modificadas por múltiplas telas simultaneamente. É exatamente para isso que existe o Provider. Estude este material com atenção antes da aula presencial, execute todos os exemplos no seu ambiente, e chegue preparado para conectar a camada de apresentação do Projeto Integrador com sua camada de lógica de negócios de forma limpa e escalável.


O que é Estado e por que ele é o Coração de Qualquer Aplicação

Antes de discutir qualquer biblioteca ou padrão de gerenciamento de estado, é preciso ter uma definição precisa do que estado significa no contexto de aplicações Flutter. Sem essa clareza, as decisões de arquitetura que você tomará ao longo deste módulo carecerão de fundamento sólido.

Estado, no contexto de desenvolvimento de aplicações, é qualquer dado cujo valor pode mudar ao longo do tempo e cuja mudança precisa ser refletida na interface do usuário. O adjetivo “qualquer” é importante: estado não é apenas o dado em si, mas a combinação de um valor que varia e da expectativa de que a interface se atualize quando esse valor muda. Um número armazenado em uma variável que nunca muda não é estado — é uma constante. Um número que muda mas cuja mudança não precisa ser exibida na tela também não é, no sentido que interessa a este módulo, estado de interface.

Pense no Projeto Integrador: um aplicativo de pedidos de comida. Há vários exemplos de estado que você já manipulou de formas diferentes sem necessariamente nomear como tal. A lista de produtos carregada do servidor é estado — ela começa vazia, é populada após a resposta da API e pode ser filtrada conforme o usuário digita no campo de busca. O carrinho de compras é estado — itens são adicionados e removidos, e o total calculado precisa ser atualizado automaticamente em cada mudança. O usuário autenticado é estado — ele começa como null, passa a existir após o login bem-sucedido e volta a null após o logout. O status de um pedido — “em preparo”, “em entrega”, “entregue” — é estado que pode mudar a qualquer momento em resposta a uma notificação do servidor.

O ponto que une todos esses exemplos é a necessidade de que múltiplas partes da interface reajam às mudanças. Quando o usuário adiciona um produto ao carrinho, o ícone de carrinho na AppBar deve atualizar o badge com o número de itens, a tela de detalhes do produto deve refletir que ele já está no carrinho, e a tela de confirmação de pedido deve listar o novo item. Todos esses widgets estão em partes diferentes da árvore de widgets e não têm nenhuma relação direta de pai para filho entre si. Como fazer todos eles reagirem à mesma mudança de forma coordenada?


Estado Efêmero e Estado de Aplicação

A primeira distinção que o Flutter estabelece ao abordar gerenciamento de estado é entre estado efêmero e estado de aplicação. Compreender essa distinção é o primeiro passo para tomar decisões de arquitetura corretas.

Estado efêmero, também chamado de estado local ou estado de UI, é aquele que pertence a um único widget e não precisa ser compartilhado com nenhum outro widget da aplicação. O Flutter recomenda explicitamente usar setState para este tipo de estado — não é necessário nenhuma biblioteca adicional. Exemplos práticos incluem: se um campo de senha está com o texto visível ou oculto, qual item de um TabBar está selecionado, se um acordeão está expandido ou recolhido, ou se um botão está no estado de carregamento durante uma operação assíncrona. Esses dados são transitórios, específicos daquele widget, e desaparecem quando o widget é removido da árvore.

Estado de aplicação, por outro lado, é o estado que precisa ser compartilhado entre múltiplos widgets ou que precisa sobreviver à navegação entre telas. O carrinho de compras é o exemplo mais claro: ele precisa ser acessível tanto na tela de listagem de produtos quanto na tela de confirmação de pedido. O usuário autenticado precisa ser conhecido pela AppBar que exibe o nome do usuário, pela tela de perfil que exibe os dados completos e pela camada de serviços que precisa incluir o token de autenticação em cada requisição. Esse tipo de estado não pode ficar preso dentro de um único widget — ele precisa ser “elevado” para um ponto da árvore de widgets que seja ancestral de todos os widgets que precisam acessá-lo.

A pergunta prática que você deve se fazer ao encontrar um novo pedaço de estado é: “algum outro widget, em outra parte da tela ou em outra tela, precisa acessar ou reagir a este dado?” Se a resposta for não, use setState. Se a resposta for sim, você precisa de uma solução de estado de aplicação. O Provider é a solução que adotamos nesta disciplina.

Classificando o estado do Projeto Integrador

graph TD
    subgraph Local["Estado Efêmero (setState)"]
        A["Visibilidade da senha no login"]
        B["Aba selecionada na tela de pedidos"]
        C["Estado de carregamento do botão"]
        D["Campo de busca aberto/fechado"]
    end
    subgraph App["Estado de Aplicação (Provider)"]
        E["Carrinho de compras (itens + total)"]
        F["Usuário autenticado (nome, token)"]
        G["Lista de produtos do cardápio"]
        H["Pedidos do histórico do usuário"]
    end


As Limitações do setState em Aplicações de Maior Escala

Para compreender por que o Provider existe, é preciso compreender primeiro o problema que ele resolve. As limitações do setState em aplicações de maior porte não são hipotéticas — você as encontrará de forma concreta à medida que o Projeto Integrador crescer.

O setState, como você aprendeu no Módulo 03, informa ao Flutter que o estado do widget mudou e que o método build precisa ser chamado novamente. Dentro de um único StatefulWidget, isso funciona perfeitamente. O problema começa quando você precisa compartilhar estado entre widgets que não têm uma relação direta de pai para filho.

A solução mais primitiva para compartilhar estado entre widgets é chamada de “elevação de estado” ou “state lifting up”: você move o estado para o ancestral comum mais próximo dos widgets que precisam acessá-lo e passa os dados e os callbacks de modificação como parâmetros. Esse mecanismo funciona, mas tem um custo que cresce com a profundidade da árvore. Imagine que o estado do carrinho de compras precisa ser acessado tanto por um widget de listagem de produtos quanto por um widget de badge no ícone da AppBar. O ancestral comum pode ser o MaterialApp ou o widget raiz da aplicação. Para que o widget de listagem — que está vários níveis abaixo do raiz — acesse o carrinho, você precisaria passar os dados e os callbacks como parâmetros por cada nível intermediário da árvore, mesmo que esses níveis não usem os dados de forma alguma. Esse problema tem um nome na comunidade de desenvolvimento: “prop drilling” ou “parameter drilling”.

O prop drilling tem três consequências negativas. A primeira é a verbosidade: cada widget intermediário precisa aceitar os parâmetros que não usa mas precisa repassar para seus filhos, tornando os construtores mais longos e difíceis de ler. A segunda é o acoplamento: widgets que deveriam ser independentes e reutilizáveis passam a conhecer detalhes de estado que não são de sua responsabilidade, criando dependências frágeis entre partes da aplicação. A terceira é a ineficiência de rebuild: quando o setState é chamado em um ancestral alto na árvore, toda a subárvore abaixo dele é reconstruída, incluindo widgets que não dependem do estado que mudou, o que pode causar degradação de performance em interfaces complexas.

O Provider resolve os três problemas: ele permite que um widget em qualquer ponto da árvore acesse o estado sem que os widgets intermediários precisem ser modificados, mantém os widgets desacoplados do estado que não lhes pertence, e permite delimitar com precisão quais partes da interface serão reconstruídas quando o estado mudar.


O Padrão Observer e o InheritedWidget

O Provider é construído sobre dois pilares: o padrão de projeto Observer e o mecanismo nativo do Flutter chamado InheritedWidget. Entender esses dois fundamentos vai fazer com que o comportamento do Provider seja intuitivo em vez de mágico.

O padrão Observer é um dos padrões de projeto comportamentais mais utilizados em desenvolvimento de software. Ele define dois papéis: o observável — também chamado de subject ou publisher — e os observadores — também chamados de listeners ou subscribers. O observável mantém uma lista de observadores registrados e, sempre que seu estado muda, notifica todos eles. Cada observador, ao ser notificado, pode reagir à mudança da forma que for mais adequada. O observável não precisa saber quem são seus observadores nem o que eles farão com a notificação — ele apenas anuncia que algo mudou. Os observadores, por sua vez, não precisam verificar periodicamente se o estado mudou — eles simplesmente aguardam a notificação.

sequenceDiagram
    participant C as CarrinhoNotifier (Observável)
    participant W1 as BadgeAppBar (Observador)
    participant W2 as TelaCarrinho (Observador)
    participant W3 as BotaoAdicionarItem (Observador)

    C->>C: adicionarItem(produto)
    C->>C: notifyListeners()
    C-->>W1: notificação de mudança
    C-->>W2: notificação de mudança
    C-->>W3: notificação de mudança
    W1->>W1: rebuild (atualiza badge)
    W2->>W2: rebuild (atualiza lista)
    W3->>W3: rebuild (atualiza texto do botão)

No Flutter, o InheritedWidget é o mecanismo nativo que permite a um widget ancestral disponibilizar dados para qualquer descendente na árvore, sem precisar passar esses dados explicitamente por cada nível intermediário. Quando um widget acessa dados de um InheritedWidget, ele se registra como dependente daquele dado. Quando o InheritedWidget é atualizado, o Flutter reconstrói automaticamente apenas os widgets dependentes — não toda a subárvore. O Provider utiliza o InheritedWidget internamente para realizar exatamente esse mecanismo: ele envolve o ChangeNotifier em um InheritedWidget e garante que apenas os widgets que se declararam interessados naquele estado sejam reconstruídos quando ele mudar.

A consequência prática é que você não precisa manipular InheritedWidgets diretamente — o Provider faz isso por você. Mas saber que ele existe embaixo explica por que um widget filho de um ChangeNotifierProvider pode acessar o estado sem precisar recebê-lo como parâmetro, e por que os rebuilds são eficientes: apenas os widgets que chamaram context.watch ou estão dentro de um Consumer são reconstruídos quando o estado muda.


ChangeNotifier: A Classe que Modela o Estado

O ChangeNotifier é a classe base fornecida pelo Flutter que implementa o papel do observável no padrão Observer. Criar uma classe que estende ChangeNotifier é o primeiro passo para construir qualquer peça de estado com o Provider.

O ChangeNotifier fornece um único método essencial: notifyListeners(). Quando você chama esse método dentro da sua classe, o Flutter notifica todos os widgets que estão ouvindo aquele objeto para que se reconstruam. A sua responsabilidade como desenvolvedor é chamar notifyListeners() sempre que o estado da classe mudar de uma forma que a interface precisa refletir. É uma responsabilidade explícita — você controla exatamente quando os widgets são notificados.

A estrutura de uma classe ChangeNotifier para o Projeto Integrador segue um padrão consistente. Os dados que representam o estado são armazenados como campos privados. Os métodos públicos modificam esses campos e chamam notifyListeners() ao final. As propriedades públicas expõem os dados de forma read-only para os widgets que consomem o estado. Esse padrão encapsula a lógica de modificação de estado dentro da própria classe, tornando o comportamento do sistema previsível e fácil de rastrear.

Exemplo — CarrinhoNotifier para o aplicativo de pedidos

import 'package:flutter/foundation.dart';

// Representa um item no carrinho: um produto com sua quantidade.
class ItemCarrinho {
  final String idProduto;
  final String nomeProduto;
  final double precoUnitario;
  final int quantidade;

  const ItemCarrinho({
    required this.idProduto,
    required this.nomeProduto,
    required this.precoUnitario,
    required this.quantidade,
  });

  // Cria uma cópia deste item com uma quantidade diferente.
  // Imutabilidade: em vez de modificar o objeto, criamos um novo.
  ItemCarrinho comQuantidade(int novaQuantidade) {
    return ItemCarrinho(
      idProduto: idProduto,
      nomeProduto: nomeProduto,
      precoUnitario: precoUnitario,
      quantidade: novaQuantidade,
    );
  }

  double get subtotal => precoUnitario * quantidade;
}

// CarrinhoNotifier: gerencia o estado do carrinho de compras.
// Estende ChangeNotifier para que os widgets possam ser notificados
// quando o estado do carrinho muda.
class CarrinhoNotifier extends ChangeNotifier {
  // _itens é privado: os widgets não podem modificá-lo diretamente.
  // Eles só podem fazer isso por meio dos métodos públicos desta classe.
  final List<ItemCarrinho> _itens = [];

  // Expõe uma cópia não modificável da lista de itens.
  // Usar List.unmodifiable impede que o código externo altere
  // a lista sem passar pelos métodos desta classe.
  List<ItemCarrinho> get itens => List.unmodifiable(_itens);

  // Número total de itens no carrinho (soma das quantidades).
  int get totalItens => _itens.fold(0, (soma, item) => soma + item.quantidade);

  // Valor total do carrinho (soma dos subtotais de cada item).
  double get valorTotal => _itens.fold(0.0, (soma, item) => soma + item.subtotal);

  // Retorna true se o produto com o id dado está no carrinho.
  bool contemProduto(String idProduto) {
    return _itens.any((item) => item.idProduto == idProduto);
  }

  // Adiciona um produto ao carrinho.
  // Se o produto já está no carrinho, incrementa a quantidade.
  // Se não está, adiciona um novo ItemCarrinho com quantidade 1.
  void adicionarProduto({
    required String idProduto,
    required String nomeProduto,
    required double preco,
  }) {
    final indiceExistente = _itens.indexWhere(
      (item) => item.idProduto == idProduto,
    );

    if (indiceExistente >= 0) {
      // Produto já existe: incrementa a quantidade.
      final itemExistente = _itens[indiceExistente];
      _itens[indiceExistente] = itemExistente.comQuantidade(
        itemExistente.quantidade + 1,
      );
    } else {
      // Produto novo: adiciona com quantidade 1.
      _itens.add(ItemCarrinho(
        idProduto: idProduto,
        nomeProduto: nomeProduto,
        precoUnitario: preco,
        quantidade: 1,
      ));
    }

    // Notifica todos os widgets ouvintes que o estado mudou.
    // Sem esta chamada, a interface nunca seria atualizada.
    notifyListeners();
  }

  // Remove um produto do carrinho completamente.
  void removerProduto(String idProduto) {
    _itens.removeWhere((item) => item.idProduto == idProduto);
    notifyListeners();
  }

  // Limpa o carrinho inteiro (após confirmação de pedido, por exemplo).
  void limpar() {
    _itens.clear();
    notifyListeners();
  }
}
import 'package:flutter/foundation.dart';

class ItemCarrinho {
  final String idProduto;
  final String nomeProduto;
  final double precoUnitario;
  final int quantidade;

  const ItemCarrinho({
    required this.idProduto,
    required this.nomeProduto,
    required this.precoUnitario,
    required this.quantidade,
  });

  ItemCarrinho comQuantidade(int q) => ItemCarrinho(
        idProduto: idProduto,
        nomeProduto: nomeProduto,
        precoUnitario: precoUnitario,
        quantidade: q,
      );

  double get subtotal => precoUnitario * quantidade;
}

class CarrinhoNotifier extends ChangeNotifier {
  final List<ItemCarrinho> _itens = [];

  List<ItemCarrinho> get itens => List.unmodifiable(_itens);
  int get totalItens => _itens.fold(0, (s, i) => s + i.quantidade);
  double get valorTotal => _itens.fold(0.0, (s, i) => s + i.subtotal);
  bool contemProduto(String id) => _itens.any((i) => i.idProduto == id);

  void adicionarProduto({
    required String idProduto,
    required String nomeProduto,
    required double preco,
  }) {
    final idx = _itens.indexWhere((i) => i.idProduto == idProduto);
    if (idx >= 0) {
      final existente = _itens[idx];
      _itens[idx] = existente.comQuantidade(existente.quantidade + 1);
    } else {
      _itens.add(ItemCarrinho(
        idProduto: idProduto,
        nomeProduto: nomeProduto,
        precoUnitario: preco,
        quantidade: 1,
      ));
    }
    notifyListeners();
  }

  void removerProduto(String idProduto) {
    _itens.removeWhere((i) => i.idProduto == idProduto);
    notifyListeners();
  }

  void limpar() {
    _itens.clear();
    notifyListeners();
  }
}

Observe três aspectos do CarrinhoNotifier acima. Primeiro, a lista _itens é privada — nenhum widget pode escrever diretamente nela. Toda modificação passa pelos métodos adicionarProduto, removerProduto e limpar, que são as únicas “portas de entrada” para o estado. Isso torna o comportamento do sistema previsível: você sempre sabe quais caminhos de código podem mudar o carrinho. Segundo, o método itens retorna uma cópia não modificável via List.unmodifiable — se um widget tentasse chamar carrinho.itens.add(...), receberia um erro em tempo de execução, garantindo que a imutabilidade da interface pública seja preservada. Terceiro, notifyListeners() é chamado ao final de cada método que modifica o estado — nunca no meio de uma operação, sempre quando a mudança já está completa e o estado está em um ponto consistente.

É também importante notar que o ChangeNotifier pertence ao pacote flutter/foundation, não ao Provider. Isso significa que você pode usar ChangeNotifier mesmo sem o Provider — e que classes ChangeNotifier pertencem à camada de domínio ou de lógica da aplicação, sem dependência nenhuma da biblioteca provider. O Provider é apenas o mecanismo de distribuição dessas classes pela árvore de widgets.


ChangeNotifierProvider: Tornando o Estado Disponível na Árvore

Criar um ChangeNotifier resolve o problema de modelar e modificar o estado. O próximo passo é torná-lo acessível aos widgets que precisam dele. É aqui que o ChangeNotifierProvider entra.

O ChangeNotifierProvider é um widget do pacote provider que posiciona um ChangeNotifier na árvore de widgets, tornando-o acessível a qualquer widget descendente. Ele encapsula o ChangeNotifier em um InheritedWidget e gerencia o ciclo de vida do objeto: ele cria o ChangeNotifier quando é inserido na árvore e chama dispose() nele quando é removido. Isso significa que você não precisa se preocupar com o descarte manual do ChangeNotifier — o ChangeNotifierProvider faz isso por você.

A posição do ChangeNotifierProvider na árvore define quais widgets podem acessar o estado que ele fornece: apenas os descendentes têm acesso. Por isso, providers que precisam ser acessados por toda a aplicação são colocados próximos à raiz da árvore — geralmente envolvendo o MaterialApp ou o widget raiz da aplicação. Providers que são relevantes apenas para uma subárvore específica — por exemplo, o estado de um formulário de vários passos — podem ser posicionados mais abaixo na árvore, próximos aos widgets que os utilizam.

Exemplo — Registrando o CarrinhoNotifier na raiz da aplicação

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(const MeuApp());
}

class MeuApp extends StatelessWidget {
  const MeuApp({super.key});

  @override
  Widget build(BuildContext context) {
    // ChangeNotifierProvider envolve o MaterialApp para que o
    // CarrinhoNotifier esteja disponível em TODAS as telas do app.
    // O parâmetro 'create' recebe uma função que cria o ChangeNotifier.
    // O Provider chama essa função uma única vez e gerencia o ciclo de vida.
    return ChangeNotifierProvider<CarrinhoNotifier>(
      create: (context) => CarrinhoNotifier(),
      child: MaterialApp(
        title: 'Pedidos App',
        home: const TelaPrincipal(),
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CarrinhoNotifier(),
      child: const MaterialApp(
        title: 'Pedidos App',
        home: TelaPrincipal(),
      ),
    ),
  );
}

O parâmetro create recebe uma função que recebe um BuildContext e retorna o ChangeNotifier. O BuildContext passado para essa função é o contexto do próprio ChangeNotifierProvider — você pode usá-lo para obter outros providers já registrados acima na árvore, se necessário. A função é chamada de forma “lazy”, ou seja, apenas quando o primeiro descendente tenta acessar o provider pela primeira vez. Se quiser que o objeto seja criado imediatamente ao ser inserido na árvore, você pode usar o parâmetro lazy: false.


Consumer: O Ouvinte Declarativo

Com o ChangeNotifierProvider registrando o estado na árvore, você precisa de uma forma de acessar esse estado dentro dos widgets descendentes e de garantir que esses widgets sejam reconstruídos quando o estado mudar. O Consumer é o widget que faz isso de forma declarativa e com controle preciso sobre qual parte da interface é reconstruída.

O Consumer<T> recebe um parâmetro builder que é uma função com três argumentos: o BuildContext, a instância do ChangeNotifier do tipo T e um widget filho opcional. O widget filho é passado para o builder mas não é reconstruído quando o estado muda — ele é útil para otimização quando parte do conteúdo do Consumer é estática e não precisa ser reconstruída.

O Consumer garante que, quando notifyListeners() é chamado no ChangeNotifier, apenas o widget retornado pelo builder do Consumer é reconstruído — não os widgets ao redor. Isso é especialmente importante quando o Consumer está dentro de uma árvore de widgets maior: apenas a subárvore dentro do Consumer é reconstruída, e não a árvore inteira.

Exemplo — Badge no ícone do carrinho usando Consumer

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// AppBar personalizada que exibe o número de itens no carrinho.
// O Consumer garante que apenas o ícone do carrinho seja reconstruído
// quando o número de itens muda — não a AppBar inteira.
class MinhaAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String titulo;

  const MinhaAppBar({super.key, required this.titulo});

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text(titulo),
      actions: [
        // Consumer ouve o CarrinhoNotifier.
        // Quando o carrinho muda, apenas este IconButton é reconstruído.
        Consumer<CarrinhoNotifier>(
          builder: (context, carrinho, child) {
            return Stack(
              alignment: Alignment.center,
              children: [
                IconButton(
                  icon: const Icon(Icons.shopping_cart_outlined),
                  onPressed: () {
                    // Navega para a tela do carrinho.
                    Navigator.of(context).pushNamed('/carrinho');
                  },
                ),
                // Exibe o badge apenas se há itens no carrinho.
                if (carrinho.totalItens > 0)
                  Positioned(
                    top: 8,
                    right: 8,
                    child: Container(
                      padding: const EdgeInsets.all(2),
                      decoration: const BoxDecoration(
                        color: Colors.red,
                        shape: BoxShape.circle,
                      ),
                      constraints: const BoxConstraints(
                        minWidth: 16,
                        minHeight: 16,
                      ),
                      child: Text(
                        '${carrinho.totalItens}',
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 10,
                        ),
                        textAlign: TextAlign.center,
                      ),
                    ),
                  ),
              ],
            );
          },
        ),
      ],
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class MinhaAppBar extends StatelessWidget implements PreferredSizeWidget {
  final String titulo;

  const MinhaAppBar({super.key, required this.titulo});

  @override
  Size get preferredSize => const Size.fromHeight(kToolbarHeight);

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text(titulo),
      actions: [
        Consumer<CarrinhoNotifier>(
          builder: (_, carrinho, __) => Stack(
            alignment: Alignment.center,
            children: [
              IconButton(
                icon: const Icon(Icons.shopping_cart_outlined),
                onPressed: () => Navigator.of(context).pushNamed('/carrinho'),
              ),
              if (carrinho.totalItens > 0)
                Positioned(
                  top: 8,
                  right: 8,
                  child: Container(
                    padding: const EdgeInsets.all(2),
                    decoration: const BoxDecoration(
                      color: Colors.red,
                      shape: BoxShape.circle,
                    ),
                    constraints:
                        const BoxConstraints(minWidth: 16, minHeight: 16),
                    child: Text(
                      '${carrinho.totalItens}',
                      style: const TextStyle(color: Colors.white, fontSize: 10),
                      textAlign: TextAlign.center,
                    ),
                  ),
                ),
            ],
          ),
        ),
      ],
    );
  }
}

context.watch e context.read: Os Dois Modos de Acesso

O Consumer é a abordagem mais explícita e visual para consumir um provider. Mas o Flutter Provider oferece também duas extensões do BuildContextcontext.watch<T>() e context.read<T>() — que permitem acessar o estado diretamente dentro do método build, com uma escrita mais concisa.

O método context.watch<T>() acessa o ChangeNotifier do tipo T e registra o widget atual como ouvinte. Quando o ChangeNotifier chama notifyListeners(), o widget que chamou context.watch é reconstruído. Esta é a forma mais simples de acessar e exibir dados reativos diretamente no corpo do build. A diferença em relação ao Consumer é de ergonomia: em vez de encapsular parte da árvore em um widget Consumer, você acessa o provider diretamente e usa seus dados no build. A consequência é que o widget inteiro é reconstruído quando o estado muda, não apenas uma subárvore. Para widgets simples, isso é aceitável. Para widgets com muita lógica de construção, prefira o Consumer.

O método context.read<T>() acessa o ChangeNotifier do tipo T sem registrar o widget como ouvinte. Isso significa que o widget não será reconstruído quando o estado mudar. O context.read é usado exclusivamente para chamar métodos que modificam o estado — em callbacks de botões, em onPressed, em onTap, em métodos chamados fora do build. A regra de ouro é: use context.watch para ler dados que precisam ser exibidos e que devem causar rebuild; use context.read para chamar métodos que modificam o estado dentro de handlers de eventos.

Nunca chame context.watch dentro de métodos como initState, dispose ou qualquer callback que não seja o método build. O context.watch registra o widget como ouvinte por meio do BuildContext, e isso só é válido durante a fase de construção do widget. Chamá-lo fora do build pode causar erros difíceis de depurar. Se você precisar acessar o estado fora do build, use context.read.

Exemplo — Botão “Adicionar ao Carrinho” em um card de produto

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// CardProduto: exibe os dados de um produto e permite adicioná-lo ao carrinho.
class CardProduto extends StatelessWidget {
  final String idProduto;
  final String nomeProduto;
  final double preco;
  final String descricao;

  const CardProduto({
    super.key,
    required this.idProduto,
    required this.nomeProduto,
    required this.preco,
    required this.descricao,
  });

  @override
  Widget build(BuildContext context) {
    // context.watch<CarrinhoNotifier>() acessa o carrinho E registra
    // este widget como ouvinte. Toda vez que o carrinho mudar,
    // este método build será chamado novamente.
    final carrinho = context.watch<CarrinhoNotifier>();

    // Verifica se este produto já está no carrinho para exibir
    // o botão correto (adicionar ou remover).
    final estaNoCarrinho = carrinho.contemProduto(idProduto);

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              nomeProduto,
              style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            Text(descricao),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'R\$ ${preco.toStringAsFixed(2)}',
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                    color: Colors.green,
                  ),
                ),
                // ElevatedButton que usa context.read para chamar
                // o método que modifica o estado.
                // context.read NÃO causa rebuild — apenas acessa o método.
                ElevatedButton.icon(
                  icon: Icon(
                    estaNoCarrinho ? Icons.check : Icons.add_shopping_cart,
                  ),
                  label: Text(estaNoCarrinho ? 'No carrinho' : 'Adicionar'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor:
                        estaNoCarrinho ? Colors.grey : Colors.green,
                  ),
                  onPressed: () {
                    if (estaNoCarrinho) {
                      // context.read: apenas lê sem registrar como ouvinte.
                      // Correto para uso dentro de callbacks.
                      context.read<CarrinhoNotifier>().removerProduto(
                        idProduto,
                      );
                    } else {
                      context.read<CarrinhoNotifier>().adicionarProduto(
                        idProduto: idProduto,
                        nomeProduto: nomeProduto,
                        preco: preco,
                      );
                    }
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CardProduto extends StatelessWidget {
  final String idProduto;
  final String nomeProduto;
  final double preco;
  final String descricao;

  const CardProduto({
    super.key,
    required this.idProduto,
    required this.nomeProduto,
    required this.preco,
    required this.descricao,
  });

  @override
  Widget build(BuildContext context) {
    final carrinho = context.watch<CarrinhoNotifier>();
    final estaNoCarrinho = carrinho.contemProduto(idProduto);

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(12),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(nomeProduto,
                style:
                    const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            Text(descricao),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  'R\$ ${preco.toStringAsFixed(2)}',
                  style: const TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                      color: Colors.green),
                ),
                ElevatedButton.icon(
                  icon: Icon(
                      estaNoCarrinho ? Icons.check : Icons.add_shopping_cart),
                  label:
                      Text(estaNoCarrinho ? 'No carrinho' : 'Adicionar'),
                  style: ElevatedButton.styleFrom(
                      backgroundColor:
                          estaNoCarrinho ? Colors.grey : Colors.green),
                  onPressed: () {
                    final c = context.read<CarrinhoNotifier>();
                    estaNoCarrinho
                        ? c.removerProduto(idProduto)
                        : c.adicionarProduto(
                            idProduto: idProduto,
                            nomeProduto: nomeProduto,
                            preco: preco,
                          );
                  },
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Selector: Rebuilds com Precisão Cirúrgica

O Consumer reconstrói o widget sempre que notifyListeners() é chamado, independentemente do que mudou no ChangeNotifier. Em muitos casos, isso é mais do que o necessário: você quer reconstruir um widget somente quando um valor específico do estado muda, não quando qualquer valor muda. O Selector resolve exatamente esse problema.

O Selector<T, S> recebe dois tipos: T é o tipo do ChangeNotifier e S é o tipo do valor selecionado. O parâmetro selector é uma função que recebe o ChangeNotifier e retorna o valor que interessa — por exemplo, apenas o totalItens do carrinho, ou apenas o valorTotal. O Selector compara o valor retornado antes e depois de cada notifyListeners. Se o valor não mudou, o widget não é reconstruído. Se mudou, o widget é reconstruído.

O Selector é a escolha correta quando você tem um ChangeNotifier com muitos campos e um widget que depende apenas de um ou dois deles. Imagine um widget que exibe apenas o preço total do pedido. Sem o Selector, ele seria reconstruído sempre que qualquer coisa no carrinho mudasse — inclusive quando o nome de um produto fosse atualizado, o que não afeta o preço total. Com o Selector, ele só é reconstruído quando o valorTotal efetivamente muda.

Exemplo — Exibindo o total do carrinho com Selector

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Widget que exibe apenas o valor total do carrinho.
// Usa Selector para ser reconstruído somente quando valorTotal muda,
// não quando qualquer outra propriedade do carrinho muda.
class TotalCarrinho extends StatelessWidget {
  const TotalCarrinho({super.key});

  @override
  Widget build(BuildContext context) {
    return Selector<CarrinhoNotifier, double>(
      // 'selector' extrai o valor que interessa do ChangeNotifier.
      // O Selector compara o valor anterior com o novo.
      // Se são iguais (==), o builder NÃO é chamado.
      // Se são diferentes, o builder É chamado e o widget é reconstruído.
      selector: (context, carrinho) => carrinho.valorTotal,
      builder: (context, total, child) {
        return Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              const Text(
                'Total do pedido:',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
              ),
              Text(
                'R\$ ${total.toStringAsFixed(2)}',
                style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                  color: Colors.green,
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class TotalCarrinho extends StatelessWidget {
  const TotalCarrinho({super.key});

  @override
  Widget build(BuildContext context) {
    return Selector<CarrinhoNotifier, double>(
      selector: (_, c) => c.valorTotal,
      builder: (_, total, __) => Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            const Text('Total do pedido:',
                style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            Text(
              'R\$ ${total.toStringAsFixed(2)}',
              style: const TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                  color: Colors.green),
            ),
          ],
        ),
      ),
    );
  }
}

O Selector usa o operador == por padrão para comparar os valores. Para tipos primitivos como int, double, String e bool, isso funciona perfeitamente. Para tipos compostos como listas ou classes customizadas, você precisará implementar o operador == e hashCode corretamente, ou usar o parâmetro shouldRebuild do Selector para fornecer sua própria lógica de comparação.


MultiProvider: Registrando Múltiplos Estados

Em uma aplicação real, você raramente terá apenas um único ChangeNotifier. O Projeto Integrador já tem, pelo menos, três: o CarrinhoNotifier, um UsuarioNotifier para o usuário autenticado, e um PedidosNotifier para o histórico de pedidos. O MultiProvider é o widget que permite registrar todos eles de forma organizada.

O MultiProvider aceita uma lista de providers e os aninha automaticamente, da mesma forma que você poderia aninhá-los manualmente. A diferença é puramente cosmética: em vez de um aninhamento profundo de providers que dificulta a leitura, você tem uma lista plana que é muito mais fácil de entender e manter.

Exemplo — MultiProvider na raiz da aplicação de pedidos

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// UsuarioNotifier: gerencia o usuário autenticado.
class UsuarioNotifier extends ChangeNotifier {
  String? _nomeUsuario;
  String? _email;
  bool _autenticado = false;

  String? get nomeUsuario => _nomeUsuario;
  String? get email => _email;
  bool get autenticado => _autenticado;

  void fazerLogin(String nome, String email) {
    _nomeUsuario = nome;
    _email = email;
    _autenticado = true;
    notifyListeners();
  }

  void fazerLogout() {
    _nomeUsuario = null;
    _email = null;
    _autenticado = false;
    notifyListeners();
  }
}

// PedidosNotifier: gerencia o histórico de pedidos do usuário.
class PedidosNotifier extends ChangeNotifier {
  final List<String> _idsPedidos = [];

  List<String> get idsPedidos => List.unmodifiable(_idsPedidos);

  void adicionarPedido(String idPedido) {
    _idsPedidos.add(idPedido);
    notifyListeners();
  }
}

// main.dart — ponto de entrada da aplicação.
void main() {
  runApp(const MeuApp());
}

class MeuApp extends StatelessWidget {
  const MeuApp({super.key});

  @override
  Widget build(BuildContext context) {
    // MultiProvider organiza múltiplos providers em uma lista plana.
    // Internamente, o Flutter os aninha: o primeiro da lista fica mais
    // acima na árvore, o último fica mais abaixo.
    return MultiProvider(
      providers: [
        // CarrinhoNotifier: disponível em toda a aplicação.
        ChangeNotifierProvider<CarrinhoNotifier>(
          create: (context) => CarrinhoNotifier(),
        ),
        // UsuarioNotifier: disponível em toda a aplicação.
        ChangeNotifierProvider<UsuarioNotifier>(
          create: (context) => UsuarioNotifier(),
        ),
        // PedidosNotifier: disponível em toda a aplicação.
        ChangeNotifierProvider<PedidosNotifier>(
          create: (context) => PedidosNotifier(),
        ),
      ],
      child: MaterialApp(
        title: 'Pedidos App',
        home: const TelaPrincipal(),
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => CarrinhoNotifier()),
        ChangeNotifierProvider(create: (_) => UsuarioNotifier()),
        ChangeNotifierProvider(create: (_) => PedidosNotifier()),
      ],
      child: const MaterialApp(title: 'Pedidos App', home: TelaPrincipal()),
    ),
  );
}

ProxyProvider: Dependências entre Providers

Às vezes, um ChangeNotifier precisa acessar outro provider para funcionar corretamente. Por exemplo, o PedidosNotifier pode precisar do token de autenticação fornecido pelo UsuarioNotifier para fazer requisições autenticadas. O ProxyProvider é o widget que permite criar esse tipo de dependência de forma limpa.

O ProxyProvider<T, R> cria um objeto do tipo R que depende de um provider do tipo T. Sempre que o provider T muda, o ProxyProvider é notificado e recria — ou atualiza — o objeto R. No contexto do Projeto Integrador, isso é útil quando um serviço ou repositório precisa de informações que estão em outro provider para funcionar.

Uma distinção importante: enquanto o ChangeNotifierProvider cria um ChangeNotifier (que notifica widgets), o ProxyProvider cria um objeto genérico qualquer. Se você precisa criar um ChangeNotifier que depende de outro provider, existe o ChangeNotifierProxyProvider.

Exemplo — PedidosNotifier dependendo do UsuarioNotifier

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// PedidosNotifier atualizado: agora recebe o token do UsuarioNotifier
// para poder fazer requisições autenticadas.
class PedidosNotifier extends ChangeNotifier {
  // O token é recebido externamente e atualizado quando o usuário muda.
  String? _tokenAutenticacao;
  final List<String> _idsPedidos = [];

  // Método chamado pelo ChangeNotifierProxyProvider quando o
  // UsuarioNotifier muda.
  void atualizarToken(String? token) {
    if (_tokenAutenticacao != token) {
      _tokenAutenticacao = token;
      // Quando o usuário faz logout (token fica null), limpa os pedidos.
      if (token == null) {
        _idsPedidos.clear();
        notifyListeners();
      }
    }
  }

  List<String> get idsPedidos => List.unmodifiable(_idsPedidos);

  Future<void> carregarPedidos() async {
    if (_tokenAutenticacao == null) return;
    // Aqui usaria o token para fazer a requisição à API.
    // Exemplo simplificado para demonstração.
    await Future.delayed(const Duration(seconds: 1));
    _idsPedidos.add('PED-001');
    _idsPedidos.add('PED-002');
    notifyListeners();
  }
}

// main.dart com ChangeNotifierProxyProvider.
class MeuApp extends StatelessWidget {
  const MeuApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider<UsuarioNotifier>(
          create: (_) => UsuarioNotifier(),
        ),
        // ChangeNotifierProxyProvider: cria o PedidosNotifier e o
        // atualiza sempre que o UsuarioNotifier muda.
        ChangeNotifierProxyProvider<UsuarioNotifier, PedidosNotifier>(
          // 'create': chamado uma única vez para criar a instância inicial.
          create: (_) => PedidosNotifier(),
          // 'update': chamado sempre que o UsuarioNotifier notifica mudanças.
          // 'usuario' é a instância atual do UsuarioNotifier.
          // 'pedidosAnterior' é a instância existente do PedidosNotifier.
          update: (context, usuario, pedidosAnterior) {
            // Atualiza o token no notifier existente (não cria um novo).
            pedidosAnterior!.atualizarToken(
              usuario.autenticado ? 'token-simulado' : null,
            );
            return pedidosAnterior;
          },
        ),
        ChangeNotifierProvider<CarrinhoNotifier>(
          create: (_) => CarrinhoNotifier(),
        ),
      ],
      child: MaterialApp(
        title: 'Pedidos App',
        home: const TelaPrincipal(),
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class MeuApp extends StatelessWidget {
  const MeuApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UsuarioNotifier()),
        ChangeNotifierProxyProvider<UsuarioNotifier, PedidosNotifier>(
          create: (_) => PedidosNotifier(),
          update: (_, usuario, pedidos) {
            pedidos!.atualizarToken(
              usuario.autenticado ? 'token-simulado' : null,
            );
            return pedidos;
          },
        ),
        ChangeNotifierProvider(create: (_) => CarrinhoNotifier()),
      ],
      child: const MaterialApp(title: 'Pedidos App', home: TelaPrincipal()),
    );
  }
}

FutureProvider e StreamProvider: Estado Assíncrono

Os providers que você aprendeu até agora partem do princípio de que os dados já estão disponíveis quando o widget é construído. Mas em muitos casos, os dados precisam ser carregados de forma assíncrona — seja de uma API, de um banco de dados local ou de um stream de dados em tempo real. O FutureProvider e o StreamProvider foram criados para esses cenários.

O FutureProvider<T> recebe um Future<T> e disponibiliza o resultado na árvore de widgets. Enquanto o futuro não está completo, o provider fornece um valor inicial que você especifica. Quando o futuro completa, ele fornece o valor resultante e reconstrói os widgets ouvintes. Se o futuro lançar um exceção, o provider fornece um valor de erro que você pode tratar.

O StreamProvider<T> funciona de forma análoga ao FutureProvider, mas com streams: em vez de um único valor futuro, ele escuta um stream e fornece o valor mais recente emitido. Sempre que o stream emite um novo valor, os widgets ouvintes são reconstruídos. Isso é ideal para dados que mudam continuamente ao longo do tempo, como o status de um pedido que é atualizado em tempo real por um websocket ou um stream do Firestore.

Exemplo — FutureProvider para carregar o cardápio

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Modelo simples de Produto para o cardápio.
class Produto {
  final String id;
  final String nome;
  final double preco;
  final String categoria;

  const Produto({
    required this.id,
    required this.nome,
    required this.preco,
    required this.categoria,
  });
}

// Função que simula a carga do cardápio de uma API.
Future<List<Produto>> carregarCardapio() async {
  await Future.delayed(const Duration(seconds: 2));
  return const [
    Produto(id: '1', nome: 'X-Burguer Artesanal', preco: 28.90, categoria: 'Lanches'),
    Produto(id: '2', nome: 'Batata Rústica', preco: 14.50, categoria: 'Acompanhamentos'),
    Produto(id: '3', nome: 'Refrigerante 350ml', preco: 6.00, categoria: 'Bebidas'),
  ];
}

// Widget que usa FutureProvider para carregar os produtos.
class TelaCardapio extends StatelessWidget {
  const TelaCardapio({super.key});

  @override
  Widget build(BuildContext context) {
    return FutureProvider<List<Produto>?>(
      // 'initialData': valor exibido enquanto o Future não completa.
      // null indica que ainda está carregando.
      initialData: null,
      create: (_) => carregarCardapio(),
      child: const ListaCardapio(),
    );
  }
}

// ListaCardapio consome o FutureProvider via context.watch.
// O tipo é List<Produto>? (nullable) porque o initialData é null.
class ListaCardapio extends StatelessWidget {
  const ListaCardapio({super.key});

  @override
  Widget build(BuildContext context) {
    // context.watch retorna null enquanto o Future não completou.
    final produtos = context.watch<List<Produto>?>();

    if (produtos == null) {
      // Estado de carregamento: mostra indicador de progresso.
      return const Center(child: CircularProgressIndicator());
    }

    if (produtos.isEmpty) {
      return const Center(child: Text('Nenhum produto disponível.'));
    }

    return ListView.builder(
      itemCount: produtos.length,
      itemBuilder: (context, index) {
        final produto = produtos[index];
        return ListTile(
          title: Text(produto.nome),
          subtitle: Text(produto.categoria),
          trailing: Text('R\$ ${produto.preco.toStringAsFixed(2)}'),
        );
      },
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Produto {
  final String id;
  final String nome;
  final double preco;
  final String categoria;
  const Produto({
    required this.id,
    required this.nome,
    required this.preco,
    required this.categoria,
  });
}

Future<List<Produto>> carregarCardapio() async {
  await Future.delayed(const Duration(seconds: 2));
  return const [
    Produto(id: '1', nome: 'X-Burguer Artesanal', preco: 28.90, categoria: 'Lanches'),
    Produto(id: '2', nome: 'Batata Rústica', preco: 14.50, categoria: 'Acompanhamentos'),
    Produto(id: '3', nome: 'Refrigerante 350ml', preco: 6.00, categoria: 'Bebidas'),
  ];
}

class TelaCardapio extends StatelessWidget {
  const TelaCardapio({super.key});

  @override
  Widget build(BuildContext context) {
    return FutureProvider<List<Produto>?>(
      initialData: null,
      create: (_) => carregarCardapio(),
      child: const ListaCardapio(),
    );
  }
}

class ListaCardapio extends StatelessWidget {
  const ListaCardapio({super.key});

  @override
  Widget build(BuildContext context) {
    final produtos = context.watch<List<Produto>?>();
    if (produtos == null) return const Center(child: CircularProgressIndicator());
    if (produtos.isEmpty) return const Center(child: Text('Nenhum produto.'));
    return ListView.builder(
      itemCount: produtos.length,
      itemBuilder: (_, i) => ListTile(
        title: Text(produtos[i].nome),
        subtitle: Text(produtos[i].categoria),
        trailing: Text('R\$ ${produtos[i].preco.toStringAsFixed(2)}'),
      ),
    );
  }
}

Uma limitação importante do FutureProvider é que ele não recria o Future quando o widget é reconstruído. Se você precisar de um mecanismo de “pull to refresh” ou de recarregar os dados em resposta a uma ação do usuário, o FutureProvider isolado não é suficiente. Nesse caso, a abordagem correta é usar um ChangeNotifier que gerencia o estado de carregamento — com campos como bool carregando, List<Produto> produtos e String? erro — e expõe um método carregarProdutos() que pode ser chamado tanto no initState quanto em resposta a ações do usuário.


GetIt: O Service Locator

O Provider é excelente para gerenciar estado reativo na interface — dados que precisam causar rebuilds quando mudam. Mas nem todo objeto que precisa ser compartilhado na aplicação é estado reativo. Repositórios, serviços de API, clientes HTTP, serviços de autenticação, serviços de notificação — todos esses objetos precisam ser acessados em múltiplos lugares, mas não necessariamente causam rebuilds de widgets quando são acessados. Para esses objetos, o GetIt é a ferramenta adequada.

O GetIt é um service locator: um registro central onde você registra instâncias de objetos com tipos específicos e as recupera em qualquer lugar do código, sem precisar passá-las por construtores. Ele funciona de forma semelhante a um dicionário global onde as chaves são os tipos e os valores são as instâncias correspondentes.

A diferença conceitual entre Provider e GetIt é fundamental. O Provider é orientado à interface: ele distribui estado pela árvore de widgets e causa rebuilds quando esse estado muda. O GetIt é orientado a serviços: ele disponibiliza objetos que fornecem funcionalidades — como buscar dados de uma API ou salvar informações no banco de dados — sem nenhuma relação com rebuilds de widgets. Nos projetos desta disciplina, usamos os dois em conjunto: o GetIt registra repositórios e serviços, enquanto o Provider gerencia o estado de UI. Os ChangeNotifiers, que são objetos de estado de UI, obtêm as dependências de repositório e serviço por meio do GetIt, em vez de recebê-las por construtores ou via árvore de providers.

A configuração do GetIt é feita em um arquivo centralizado de injeção de dependências, geralmente core/di/injecao.dart ou similar, e executada antes de runApp. O objeto GetIt possui uma instância global acessível como GetIt.instance ou, mais comumente, por meio de um alias como sl (de service locator) ou simplesmente getIt.

Exemplo — Configuração do GetIt para o Projeto Integrador

import 'package:get_it/get_it.dart';

// Instância global do GetIt, acessível em qualquer arquivo que
// importar este módulo. O alias 'sl' é comum na comunidade Flutter
// (de 'service locator').
final sl = GetIt.instance;

// Interface do repositório de produtos (camada de domínio).
// O GetIt resolve para a implementação concreta, mas o código
// que usa o repositório conhece apenas a interface.
abstract class IProdutoRepositorio {
  Future<List<Produto>> buscarTodos();
  Future<Produto?> buscarPorId(String id);
}

// Implementação concreta do repositório (camada de infraestrutura).
// Em um projeto real, esta classe faria chamadas HTTP ao backend.
class ProdutoRepositorioHttp implements IProdutoRepositorio {
  final String _baseUrl;

  ProdutoRepositorioHttp({required String baseUrl}) : _baseUrl = baseUrl;

  @override
  Future<List<Produto>> buscarTodos() async {
    // Simulação: em produção, faria uma requisição HTTP.
    await Future.delayed(const Duration(seconds: 1));
    return const [
      Produto(id: '1', nome: 'X-Burguer', preco: 28.90, categoria: 'Lanches'),
    ];
  }

  @override
  Future<Produto?> buscarPorId(String id) async {
    await Future.delayed(const Duration(milliseconds: 500));
    return Produto(id: id, nome: 'X-Burguer', preco: 28.90, categoria: 'Lanches');
  }
}

// Função de configuração: chama-se antes do runApp.
Future<void> configurarDependencias() async {
  // registerLazySingleton: cria a instância apenas na primeira chamada sl<T>().
  // A mesma instância é reutilizada em todas as chamadas subsequentes.
  sl.registerLazySingleton<IProdutoRepositorio>(
    () => ProdutoRepositorioHttp(
      baseUrl: 'https://api.pedidosapp.com.br',
    ),
  );

  // registerFactory: cria uma NOVA instância a cada chamada sl<T>().
  // Use para objetos que não devem ser compartilhados entre chamadores.
  // (Neste caso, seria mais natural usar registerLazySingleton.)
  // sl.registerFactory<IProdutoRepositorio>(() => ProdutoRepositorioHttp(...));
}

// main.dart
// import 'injecao.dart';
//
// void main() async {
//   WidgetsFlutterBinding.ensureInitialized();
//   await configurarDependencias();
//   runApp(const MeuApp());
// }
import 'package:get_it/get_it.dart';

final sl = GetIt.instance;

abstract class IProdutoRepositorio {
  Future<List<Produto>> buscarTodos();
  Future<Produto?> buscarPorId(String id);
}

class ProdutoRepositorioHttp implements IProdutoRepositorio {
  final String _baseUrl;
  ProdutoRepositorioHttp(this._baseUrl);

  @override
  Future<List<Produto>> buscarTodos() async {
    await Future.delayed(const Duration(seconds: 1));
    return const [
      Produto(id: '1', nome: 'X-Burguer', preco: 28.90, categoria: 'Lanches'),
    ];
  }

  @override
  Future<Produto?> buscarPorId(String id) async {
    await Future.delayed(const Duration(milliseconds: 500));
    return Produto(id: id, nome: 'X-Burguer', preco: 28.90, categoria: 'Lanches');
  }
}

Future<void> configurarDependencias() async {
  sl.registerLazySingleton<IProdutoRepositorio>(
    () => ProdutoRepositorioHttp('https://api.pedidosapp.com.br'),
  );
}

O GetIt oferece três modos de registro. O registerSingleton cria a instância imediatamente e a reutiliza em todas as chamadas. O registerLazySingleton cria a instância apenas na primeira chamada e a reutiliza depois — é o modo mais comum para repositórios e serviços. O registerFactory cria uma nova instância a cada chamada — útil para objetos que precisam de estado fresco a cada uso.

Para obter um objeto registrado no GetIt, você usa a notação sl<IProdutoRepositorio>() ou sl.get<IProdutoRepositorio>(). O tipo genérico especificado deve corresponder ao tipo com que o objeto foi registrado — se registrou com a interface IProdutoRepositorio, deve obter com sl<IProdutoRepositorio>(), não com sl<ProdutoRepositorioHttp>(). Isso garante que o código consumidor depende da abstração, não da implementação concreta.


A Arquitetura MVVM com Provider

Com ChangeNotifier, Provider e GetIt, você tem todas as peças para construir uma arquitetura limpa e escalável. O padrão que emerge naturalmente da combinação dessas ferramentas é uma variação do MVVM — Model, View, ViewModel — adaptada ao contexto Flutter.

No MVVM clássico, o Model representa os dados e a lógica de negócios do domínio. A View é a interface do usuário — no Flutter, são os widgets. O ViewModel é o intermediário: ele transforma os dados do modelo em uma forma adequada para a View e processa as ações do usuário, traduzindo-as em operações sobre o modelo. No contexto Flutter com Provider, o ChangeNotifier desempenha exatamente o papel do ViewModel.

graph LR
    subgraph Domínio["Camada de Domínio"]
        M1["Produto"]
        M2["Pedido"]
        M3["Usuario"]
        R1["IProdutoRepositorio (interface)"]
    end
    subgraph Infraestrutura["Camada de Infraestrutura"]
        R2["ProdutoRepositorioHttp"]
        R3["GetIt (sl)"]
    end
    subgraph Apresentação["Camada de Apresentação"]
        VM1["CarrinhoNotifier (ViewModel)"]
        VM2["UsuarioNotifier (ViewModel)"]
        V1["TelaCardapio (View)"]
        V2["TelaCarrinho (View)"]
        V3["MinhaAppBar (View)"]
    end

    R1 --> R2
    R2 --> R3
    R3 --> VM1
    R3 --> VM2
    M1 --> VM1
    M2 --> VM2
    VM1 --> V1
    VM1 --> V2
    VM1 --> V3
    VM2 --> V3

A separação entre camadas tem consequências práticas concretas. Os widgets — as Views — ficam extremamente simples: eles apenas renderizam o estado atual do ChangeNotifier e despacham eventos para ele. Eles não contêm lógica de negócios, não fazem chamadas de API, não calculam totais. Toda essa lógica fica no ChangeNotifier. Isso torna os widgets altamente reutilizáveis e fáceis de testar visualmente.

Os ChangeNotifiers, por sua vez, não contêm lógica de acesso a dados. Eles delegam essa responsabilidade aos repositórios obtidos via GetIt. Quando um ChangeNotifier precisa buscar produtos, ele chama sl<IProdutoRepositorio>().buscarTodos() e usa o resultado para atualizar seu estado interno. Essa separação entre estado de UI e acesso a dados permite testar cada parte de forma isolada.

Exemplo — CardapioNotifier integrando Provider e GetIt

import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';

final sl = GetIt.instance;

// CardapioNotifier: ViewModel que gerencia a lista de produtos do cardápio.
// Combina Provider (para notificar a UI) com GetIt (para acessar o repositório).
class CardapioNotifier extends ChangeNotifier {
  // Estado interno: dados, carregamento e erro.
  List<Produto> _produtos = [];
  bool _carregando = false;
  String? _erro;
  String _categoriaFiltro = '';

  // Getters: expõem o estado de forma read-only para a View.
  List<Produto> get produtos {
    if (_categoriaFiltro.isEmpty) return List.unmodifiable(_produtos);
    return _produtos
        .where((p) => p.categoria == _categoriaFiltro)
        .toList();
  }

  bool get carregando => _carregando;
  String? get erro => _erro;
  String get categoriaFiltro => _categoriaFiltro;

  // Lista de categorias únicas disponíveis.
  List<String> get categorias {
    return _produtos.map((p) => p.categoria).toSet().toList()..sort();
  }

  // carregarProdutos: busca os produtos via repositório (obtido do GetIt).
  Future<void> carregarProdutos() async {
    // Ativa o estado de carregamento e limpa qualquer erro anterior.
    _carregando = true;
    _erro = null;
    notifyListeners();

    try {
      // Obtém o repositório do GetIt. O ChangeNotifier não sabe
      // qual implementação concreta está sendo usada — apenas conhece
      // a interface IProdutoRepositorio.
      final repositorio = sl<IProdutoRepositorio>();
      _produtos = await repositorio.buscarTodos();
    } catch (e) {
      // Armazena o erro para que a View possa exibi-lo.
      _erro = 'Erro ao carregar o cardápio: $e';
    } finally {
      // Sempre desativa o carregamento, com sucesso ou erro.
      _carregando = false;
      notifyListeners();
    }
  }

  // filtrarPorCategoria: atualiza o filtro e notifica os ouvintes.
  void filtrarPorCategoria(String categoria) {
    _categoriaFiltro = categoria;
    notifyListeners();
  }

  // limparFiltro: remove o filtro de categoria.
  void limparFiltro() {
    _categoriaFiltro = '';
    notifyListeners();
  }
}
import 'package:flutter/foundation.dart';
import 'package:get_it/get_it.dart';

final sl = GetIt.instance;

class CardapioNotifier extends ChangeNotifier {
  List<Produto> _produtos = [];
  bool _carregando = false;
  String? _erro;
  String _filtro = '';

  List<Produto> get produtos => _filtro.isEmpty
      ? List.unmodifiable(_produtos)
      : _produtos.where((p) => p.categoria == _filtro).toList();

  bool get carregando => _carregando;
  String? get erro => _erro;
  String get filtro => _filtro;

  List<String> get categorias =>
      (_produtos.map((p) => p.categoria).toSet().toList()..sort());

  Future<void> carregarProdutos() async {
    _carregando = true;
    _erro = null;
    notifyListeners();
    try {
      _produtos = await sl<IProdutoRepositorio>().buscarTodos();
    } catch (e) {
      _erro = 'Erro ao carregar o cardápio: $e';
    } finally {
      _carregando = false;
      notifyListeners();
    }
  }

  void filtrarPorCategoria(String categoria) {
    _filtro = categoria;
    notifyListeners();
  }

  void limparFiltro() {
    _filtro = '';
    notifyListeners();
  }
}

Consumindo o CardapioNotifier na View

Com o CardapioNotifier modelando o estado e o GetIt fornecendo o repositório, agora você pode construir a View que consome esse estado. O widget de tela fica responsável apenas por exibir o estado atual e despachar ações para o ChangeNotifier.

Um padrão muito comum em Views que dependem de dados carregados de forma assíncrona é o de três estados: carregando, carregado com dados e carregado com erro. O CardapioNotifier já expõe os campos necessários para implementar esse padrão: carregando, produtos e erro. A View simplesmente verifica esses campos em ordem e renderiza o widget adequado.

Exemplo — TelaCardapio consumindo o CardapioNotifier

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class TelaCardapio extends StatefulWidget {
  const TelaCardapio({super.key});

  @override
  State<TelaCardapio> createState() => _TelaCardapioState();
}

class _TelaCardapioState extends State<TelaCardapio> {
  @override
  void initState() {
    super.initState();
    // Usa context.read porque initState não é uma fase de build.
    // A chamada acontece após o primeiro frame ser inserido na tela.
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) {
        context.read<CardapioNotifier>().carregarProdutos();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    // context.watch registra este widget como ouvinte.
    // Quando o CardapioNotifier chamar notifyListeners(), este build
    // será chamado novamente.
    final cardapio = context.watch<CardapioNotifier>();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Cardápio'),
        actions: [
          // Botão de recarregar.
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () => context.read<CardapioNotifier>().carregarProdutos(),
          ),
        ],
      ),
      body: _construirCorpo(cardapio),
    );
  }

  // Método auxiliar que retorna o widget correto baseado no estado.
  Widget _construirCorpo(CardapioNotifier cardapio) {
    if (cardapio.carregando) {
      return const Center(child: CircularProgressIndicator());
    }

    if (cardapio.erro != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.error_outline, size: 48, color: Colors.red),
            const SizedBox(height: 8),
            Text(cardapio.erro!),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () =>
                  context.read<CardapioNotifier>().carregarProdutos(),
              child: const Text('Tentar novamente'),
            ),
          ],
        ),
      );
    }

    if (cardapio.produtos.isEmpty) {
      return const Center(child: Text('Nenhum produto disponível.'));
    }

    return Column(
      children: [
        // Filtros de categoria.
        _FiltrosCategorias(
          categorias: cardapio.categorias,
          categoriaSelecionada: cardapio.filtro,
        ),
        // Lista de produtos.
        Expanded(
          child: ListView.builder(
            itemCount: cardapio.produtos.length,
            itemBuilder: (context, index) {
              final produto = cardapio.produtos[index];
              return CardProduto(
                idProduto: produto.id,
                nomeProduto: produto.nome,
                preco: produto.preco,
                descricao: produto.categoria,
              );
            },
          ),
        ),
      ],
    );
  }
}

// Widget de filtros de categoria.
class _FiltrosCategorias extends StatelessWidget {
  final List<String> categorias;
  final String categoriaSelecionada;

  const _FiltrosCategorias({
    required this.categorias,
    required this.categoriaSelecionada,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 48,
      child: ListView(
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.symmetric(horizontal: 8),
        children: [
          // Chip para limpar o filtro.
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 4),
            child: FilterChip(
              label: const Text('Todos'),
              selected: categoriaSelecionada.isEmpty,
              onSelected: (_) =>
                  context.read<CardapioNotifier>().limparFiltro(),
            ),
          ),
          // Um chip para cada categoria disponível.
          for (final categoria in categorias)
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 4),
              child: FilterChip(
                label: Text(categoria),
                selected: categoriaSelecionada == categoria,
                onSelected: (_) =>
                    context.read<CardapioNotifier>().filtrarPorCategoria(categoria),
              ),
            ),
        ],
      ),
    );
  }
}
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class TelaCardapio extends StatefulWidget {
  const TelaCardapio({super.key});

  @override
  State<TelaCardapio> createState() => _TelaCardapioState();
}

class _TelaCardapioState extends State<TelaCardapio> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) context.read<CardapioNotifier>().carregarProdutos();
    });
  }

  @override
  Widget build(BuildContext context) {
    final cardapio = context.watch<CardapioNotifier>();
    return Scaffold(
      appBar: AppBar(
        title: const Text('Cardápio'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () =>
                context.read<CardapioNotifier>().carregarProdutos(),
          ),
        ],
      ),
      body: switch ((cardapio.carregando, cardapio.erro, cardapio.produtos)) {
        (true, _, _) => const Center(child: CircularProgressIndicator()),
        (false, final e?, _) => Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Icon(Icons.error_outline, size: 48, color: Colors.red),
                const SizedBox(height: 8),
                Text(e),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () =>
                      context.read<CardapioNotifier>().carregarProdutos(),
                  child: const Text('Tentar novamente'),
                ),
              ],
            ),
          ),
        (false, null, final p) when p.isEmpty =>
          const Center(child: Text('Nenhum produto.')),
        _ => Column(
            children: [
              _FiltrosCategorias(
                categorias: cardapio.categorias,
                categoriaSelecionada: cardapio.filtro,
              ),
              Expanded(
                child: ListView.builder(
                  itemCount: cardapio.produtos.length,
                  itemBuilder: (_, i) {
                    final p = cardapio.produtos[i];
                    return CardProduto(
                      idProduto: p.id,
                      nomeProduto: p.nome,
                      preco: p.preco,
                      descricao: p.categoria,
                    );
                  },
                ),
              ),
            ],
          ),
      },
    );
  }
}

class _FiltrosCategorias extends StatelessWidget {
  final List<String> categorias;
  final String categoriaSelecionada;
  const _FiltrosCategorias({
    required this.categorias,
    required this.categoriaSelecionada,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 48,
      child: ListView(
        scrollDirection: Axis.horizontal,
        padding: const EdgeInsets.symmetric(horizontal: 8),
        children: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 4),
            child: FilterChip(
              label: const Text('Todos'),
              selected: categoriaSelecionada.isEmpty,
              onSelected: (_) =>
                  context.read<CardapioNotifier>().limparFiltro(),
            ),
          ),
          for (final c in categorias)
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 4),
              child: FilterChip(
                label: Text(c),
                selected: categoriaSelecionada == c,
                onSelected: (_) =>
                    context.read<CardapioNotifier>().filtrarPorCategoria(c),
              ),
            ),
        ],
      ),
    );
  }
}

Observe um detalhe importante no initState do exemplo: o carregarProdutos() é chamado por meio de WidgetsBinding.instance.addPostFrameCallback. Isso garante que a chamada aconteça após o primeiro frame ser inserido na tela — ou seja, após o build inicial ter sido executado e o BuildContext estar completamente configurado. Chamar context.read diretamente dentro de initState, antes do primeiro frame, pode causar erros porque o contexto ainda não foi inserido na árvore de InheritedWidgets.


Erros Comuns e Como Evitá-los

O gerenciamento de estado com Provider é conceptualmente simples, mas tem armadilhas que você vai encontrar em algum momento. Conhecê-las antes de encontrá-las economiza horas de depuração.

O primeiro erro frequente é chamar context.watch fora do método build. Isso inclui chamadas dentro de initState, didChangeDependencies, callbacks de botões e outros lugares. O context.watch só é válido durante a fase de build. Se você precisa acessar o provider fora do build, use context.read.

O segundo erro é esquecer de chamar notifyListeners() após modificar o estado. Quando isso acontece, o estado é modificado mas a interface não é atualizada. O sintoma é frustrante: você pode ver no depurador que o valor do campo mudou, mas a tela permanece inalterada. A solução é garantir que todo método do ChangeNotifier que modifica o estado chame notifyListeners() ao final — ou use padrões que tornem essa chamada explícita e difícil de esquecer.

O terceiro erro é registrar o ChangeNotifierProvider abaixo do widget que precisa do estado. Se você colocar o ChangeNotifierProvider<CarrinhoNotifier> dentro de um widget filho, ele não estará disponível para os widgets que ficam acima ou ao lado dele na árvore. A regra é: o provider deve ser colocado acima do widget mais ancestral que precisa acessá-lo.

O quarto erro, mais sutil, é modificar o estado durante o método build. Chamar um método do ChangeNotifier que chama notifyListeners() dentro de build causa um loop infinito: build é chamado → método chama notifyListeners()notifyListeners agenda um rebuild → build é chamado novamente. O Flutter detecta loops desse tipo e lança uma exceção. A modificação do estado deve sempre acontecer em resposta a eventos do usuário ou a resultados de operações assíncronas — nunca diretamente dentro do build.

O quinto erro é usar context.read no lugar de context.watch ao exibir dados que precisam ser reativos. Se você usa context.read para obter a lista de produtos e exibi-la, a lista será exibida uma vez e nunca será atualizada quando o ChangeNotifier mudar. O context.read é para ações — context.watch e Consumer são para dados exibidos.


Visão Completa: Fluxo de Dados no Projeto Integrador

Para consolidar todos os conceitos deste módulo, é útil ver o fluxo completo de dados em uma operação típica do Projeto Integrador: o usuário adiciona um produto ao carrinho e o total atualizado aparece imediatamente em todas as telas onde ele é exibido.

sequenceDiagram
    actor U as Usuário
    participant B as BotaoAdicionarItem (View)
    participant CN as CarrinhoNotifier (ViewModel)
    participant AB as MinhaAppBar (Consumer)
    participant TC as TelaCarrinho (Consumer)

    U->>B: toca em "Adicionar"
    B->>CN: context.read<CarrinhoNotifier>().adicionarProduto(...)
    CN->>CN: _itens.add(novoItem)
    CN->>CN: notifyListeners()
    CN-->>AB: rebuild (badge atualizado)
    CN-->>TC: rebuild (lista atualizada)
    AB->>U: exibe "3 itens no carrinho"
    TC->>U: exibe item recém-adicionado

O fluxo é unidirecional: o usuário interage com a View, a View chama um método do ViewModel via context.read, o ViewModel modifica o estado e chama notifyListeners(), e o Provider reconstrói apenas os widgets registrados como ouvintes. Não há comunicação direta entre Views — toda coordenação passa pelo ViewModel compartilhado. Esse fluxo unidirecional é o que torna a aplicação previsível e fácil de depurar.


O Provider no Projeto Integrador

Neste módulo, você adiciona ao Projeto Integrador a camada de gerenciamento de estado que conectará todas as telas construídas nos módulos anteriores. Esse é o marco em que o aplicativo começa a se comportar como um produto coeso: as telas deixam de ser independentes e passam a compartilhar dados de forma reativa.

A estrutura de providers e serviços que você deve implementar no Projeto Integrador neste módulo segue a arquitetura discutida ao longo de todo o material. O arquivo de configuração do GetIt — geralmente lib/core/di/injecao.dart — deve registrar os repositórios e serviços que serão utilizados pelos ChangeNotifiers. O MultiProvider na raiz da aplicação deve expor os ChangeNotifiers que precisam ser acessados globalmente: o CarrinhoNotifier, o UsuarioNotifier, e o CardapioNotifier.

Cada tela do aplicativo deve ser refatorada para consumir o estado dos ChangeNotifiers em vez de gerenciá-lo internamente com setState. A tela de listagem de produtos passa a observar o CardapioNotifier para exibir os produtos e o CarrinhoNotifier para exibir o estado do botão “Adicionar” de cada produto. A AppBar passa a observar o CarrinhoNotifier para atualizar o badge do ícone do carrinho. A tela de confirmação de pedido lê o estado do CarrinhoNotifier para montar o resumo do pedido e chama carrinho.limpar() após a confirmação.

A separação que você estabelece neste módulo — entre dados (repositórios no GetIt), lógica de estado (ChangeNotifiers no Provider) e apresentação (widgets da View) — é a fundação sobre a qual todos os módulos seguintes serão construídos. Nos módulos de persistência local, de consumo de APIs REST e de integração com AWS, você adicionará implementações concretas dos repositórios que o GetIt fornecerá aos ChangeNotifiers. A arquitetura permanece a mesma; apenas as implementações mudam.

A combinação de Provider e GetIt que você aprendeu neste módulo não é apenas uma escolha técnica — é uma decisão arquitetural que tem consequências em testabilidade, escalabilidade e manutenibilidade. Nos módulos futuros, quando você precisar testar o CarrinhoNotifier de forma isolada, bastará registrar no GetIt um repositório falso — um “mock” — que simula o comportamento da API sem fazer chamadas de rede reais. A abstração da interface IProdutoRepositorio é o que torna isso possível. Guarde essa ideia: ela é o cerne da inversão de dependência, e ela ficará cada vez mais clara conforme o Projeto Integrador evoluir.