Módulo 10 — Exercícios: Integração com Backend AWS
Chegamos ao módulo que conecta tudo o que você construiu até aqui com o mundo real da computação em nuvem. Nos módulos anteriores você aprendeu a estruturar widgets, gerenciar estado com Provider, persistir dados localmente e consumir APIs HTTP. Agora você vai integrar o aplicativo de delivery com uma infraestrutura AWS composta por API Gateway, Lambda Functions em Python, RDS PostgreSQL e SQS. Esses exercícios não são exercícios de memorização de serviços AWS — eles são exercícios de integração, onde a qualidade da sua solução depende diretamente da consistência entre o backend Python e o frontend Dart. Um único nome de campo diferente entre o JSON que a Lambda retorna e o que o Flutter espera é suficiente para que a integração falhe silenciosamente, sem nenhum erro aparente. Leia cada enunciado com atenção, compreenda o contrato de cada endpoint antes de escrever a primeira linha de código e trate os erros como cidadãos de primeira classe — não como casos excepcionais que você vai tratar depois.
Exercício 1
Lambda + API Gateway: listando e buscando produtos
Construindo o contrato entre backend e frontend: do banco ao widget
Este exercício é o ponto de partida da integração real entre a Lambda Function em Python e o repositório Flutter que você escreverá no lado do cliente. Antes de qualquer implementação, é preciso compreender o que significa um contrato entre backend e frontend: é o conjunto de acordos sobre o formato dos dados trocados, o nome exato dos campos, os códigos de status HTTP para cada situação e o comportamento esperado diante de erros. Quando esses acordos não existem explicitamente, surgem bugs que não geram mensagens de erro — o código simplesmente produz resultados silenciosamente incorretos. Este exercício treina exatamente a disciplina de manter esse contrato consistente de ponta a ponta.
O contexto é o catálogo de produtos do aplicativo de delivery. A tela de cardápio exibe todos os produtos disponíveis e permite filtrar por categoria. Quando o usuário toca em um produto específico, o aplicativo busca os detalhes daquele produto individualmente. Essa arquitetura exige dois endpoints distintos: um para listar e um para buscar por ID. Você implementará os dois, tanto no backend quanto no frontend.
Você deve implementar dois artefatos distintos: um arquivo Python chamado g_ex1.py, contendo as Lambda Functions, e um arquivo Dart chamado g_ex1.dart, contendo o repositório Flutter e os modelos de dados correspondentes.
Parte Python — Lambda Functions:
Comece pela função que trata GET /produtos. O handler Python recebe o evento da requisição HTTP como um dicionário e deve extrair, de forma segura, o parâmetro de consulta opcional categoriaId. A extração segura significa que você deve usar event.get("queryStringParameters") or {} — o operador or {} garante que, mesmo quando o API Gateway entrega queryStringParameters como None (o que ocorre quando não há parâmetros na URL), você trabalhe com um dicionário vazio em vez de tentar chamar .get() em None.
A conexão com o RDS PostgreSQL deve ser tratada com o padrão de reutilização de conexão no nível de módulo. Defina uma variável global _conexao = None fora do handler e implemente uma função obter_conexao() que verifica se _conexao é None ou se _conexao.closed é diferente de zero, e só então cria uma nova conexão usando psycopg2.connect. As credenciais de conexão — host, nome do banco, usuário e senha — devem ser lidas exclusivamente de variáveis de ambiente usando os.environ["DB_HOST"], os.environ["DB_NAME"], os.environ["DB_USER"] e os.environ["DB_PASSWORD"]. Em hipótese alguma escreva credenciais diretamente no código-fonte.
A consulta SQL para listar produtos deve usar JOIN com a tabela categorias para incluir o nome da categoria no resultado. Quando categoriaId estiver presente na requisição, adicione uma cláusula WHERE p.categoria_id = %s e filtre apenas os produtos disponíveis (WHERE p.disponivel = true). Quando categoriaId não estiver presente, liste todos os produtos disponíveis sem filtro de categoria. Use sempre psycopg2.extras.RealDictCursor para que os resultados sejam retornados como dicionários em vez de tuplas — isso facilita a serialização e torna o código mais legível. Para serializar o resultado como JSON, use json.dumps(lista, ensure_ascii=False, default=str), onde ensure_ascii=False preserva caracteres UTF-8 como acentos e cedilhas, e default=str garante que tipos como Decimal (retornado pelo psycopg2 para campos NUMERIC) sejam convertidos para string sem lançar TypeError.
O retorno da Lambda deve seguir o formato exigido pelo API Gateway com integração Lambda Proxy: um dicionário com as chaves statusCode, headers e body. O header Content-Type: application/json é obrigatório. Em caso de psycopg2.OperationalError, redefina a variável global _conexao = None (para forçar uma nova conexão na próxima invocação) e retorne statusCode 503 com body {"erro": "serviço temporariamente indisponível"}. Para qualquer outra exceção não tratada, retorne statusCode 500 com body {"erro": str(e)}.
A segunda função trata GET /produtos/{produtoId}. Extraia o produtoId de event.get("pathParameters") or {}, converta para inteiro e execute um SELECT com JOIN para buscar o produto específico junto com o nome da sua categoria. Quando o cursor retornar um resultado vazio — o que indica que não existe produto com aquele ID —, retorne statusCode 404 com body {"erro": "produto não encontrado"}. Quando o produto for encontrado, retorne statusCode 200 com o produto serializado. Use a mesma função obter_conexao() e o mesmo tratamento de erros das demais funções para manter consistência.
Parte Dart — Repositório Flutter:
O repositório HttpProdutoRepositorio deve receber, no construtor, dois parâmetros: String urlBase e http.Client cliente. Receber o cliente HTTP por injeção de dependência é uma prática que permite substituir o cliente real por um mock nos testes unitários sem nenhuma alteração no repositório. A classe deve implementar uma interface abstrata IProdutoRepositorio que declara os dois métodos, garantindo que a camada de domínio nunca dependa da implementação concreta.
O modelo Produto deve conter os campos id (int), nome (String), descricao (String), categoria (String), preco (double), disponivel (bool) e um construtor factory fromJson(Map<String, dynamic> json). Preste atenção ao campo preco: o JSON pode trazer o valor como uma string (porque o psycopg2 serializa Decimal com default=str) ou como um número, e o fromJson deve tratar ambos os casos usando (json['preco'] as num).toDouble() ou double.parse(json['preco'].toString()). Defina também as exceções ProdutoNaoEncontradoException e ApiException, ambas estendendo Exception.
O método Future<List<Produto>> listarProdutos({String? categoriaId}) deve construir a URL base com o caminho /produtos e, quando categoriaId não for nulo, adicioná-lo como parâmetro de consulta. Use Uri.parse('$urlBase/produtos') e depois uri.replace(queryParameters: {'categoriaId': categoriaId}) para montar a URL sem concatenar strings manualmente. Verifique o statusCode da resposta: 200 indica sucesso e você deve desserializar a lista com (jsonDecode(response.body) as List).map((e) => Produto.fromJson(e)).toList(); qualquer outro código deve resultar em ApiException com o código e a mensagem do erro.
O método Future<Produto> buscarProduto(int id) deve tratar especificamente o código 404 lançando ProdutoNaoEncontradoException em vez de ApiException, pois esses dois erros têm semânticas muito diferentes do ponto de vista da interface do usuário: um produto não encontrado pode exigir uma mensagem de “item não disponível”, enquanto um erro genérico de API exige uma mensagem de “tente novamente mais tarde”.
Para construir a URL do endpoint no Dart, use sempre Uri.parse em vez de concatenação de strings. O parâmetro de consulta categoriaId no Flutter deve ser exatamente categoriaId — a mesma chave que o Python extrai em event.get("queryStringParameters") or {}. Qualquer diferença entre categoriaId e categoria_id, por exemplo, resultará em um filtro que nunca é aplicado no backend, e o frontend receberá todos os produtos sem saber que o filtro foi ignorado.
Para a query SQL, use sempre parâmetros posicionais com %s — nunca use f-strings ou concatenação de strings para inserir valores em consultas SQL, pois isso abre vulnerabilidades de injeção de SQL mesmo em um ambiente interno.
Antes de começar, reflita sobre estas questões: por que usar RealDictCursor em vez do cursor padrão do psycopg2? O que acontece com a conexão global _conexao se o RDS for reiniciado enquanto a Lambda está em warm start? Por que o JSON serializado deve usar default=str e o que acontece sem ele quando o campo preco é do tipo Decimal retornado pelo psycopg2?
O parâmetro de consulta para filtrar por categoria deve usar exatamente a mesma chave tanto no Python quanto no Dart. Se a Lambda espera categoriaId mas o Flutter envia categoria_id, o filtro nunca será aplicado — e esse tipo de inconsistência é um dos bugs mais difíceis de rastrear porque não gera erro nenhum: a resposta simplesmente retorna todos os produtos em vez do subconjunto filtrado, e o comportamento parece “quase correto” em muitos cenários de teste.
O que deve ser entregue: dois arquivos — g_ex1.py, contendo as duas Lambda Functions (handler para GET /produtos e handler para GET /produtos/{produtoId}), incluindo a função obter_conexao() e o tratamento completo de erros; e g_ex1.dart, contendo o modelo Produto com o factory fromJson, as exceções ProdutoNaoEncontradoException e ApiException, a interface IProdutoRepositorio e a classe HttpProdutoRepositorio com os dois métodos implementados. Substitua g pelo nome do seu grupo.
Exercício 2
Lambda transacional + Flutter: criando e atualizando pedidos
Transações, consistência e o estado assíncrono que o usuário precisa ver
A criação de um pedido em um sistema de delivery não é uma operação simples de INSERT. Ela envolve verificar a disponibilidade de cada produto solicitado, calcular o total com base nos preços registrados no banco de dados — nunca nos preços enviados pelo cliente —, inserir o pedido, inserir cada item do pedido de forma atômica e, após a confirmação da transação, enfileirar uma mensagem para que outros sistemas sejam notificados do novo pedido. Se qualquer uma dessas etapas falhar, o estado do banco de dados deve ser revertido ao que era antes. Esse conjunto de garantias é o que define uma transação de banco de dados, e implementá-la corretamente em uma Lambda Function exige atenção a detalhes que vão muito além da sintaxe SQL.
Do lado do Flutter, a criação de um pedido é uma operação assíncrona que passa por estados bem definidos: o usuário inicia, o sistema processa, e o resultado pode ser sucesso ou um erro com uma causa específica. Modelar esses estados com sealed class no Dart 3 é a abordagem correta porque o compilador garante que todos os estados são tratados e que nenhum caso foi esquecido. Este exercício treina exatamente essa dupla disciplina: transações corretas no backend e estados assíncronos bem modelados no frontend.
Você deve implementar dois artefatos: g_ex2.py (Python) e g_ex2.dart (Dart).
Parte Python — Lambda Functions:
A Lambda Function para POST /pedidos começa lendo o body da requisição com json.loads(event.get("body") or "{}"). Esse or "{}" é uma salvaguarda necessária porque o API Gateway pode entregar body como None quando a requisição não possui corpo, e json.loads(None) lança TypeError. Após a leitura, valide que o body contém os campos obrigatórios: usuarioId deve ser uma string não vazia e itens deve ser uma lista com pelo menos um elemento. Se qualquer uma dessas validações falhar, retorne imediatamente statusCode 400 com uma mensagem descritiva — não prossiga para o banco de dados.
Toda a lógica de banco de dados deve ocorrer dentro de uma transação explícita. Configure a conexão com autocommit = False — esse é o padrão do psycopg2, mas torná-lo explícito documenta a intenção. A transação deve seguir esta sequência de operações:
Primeiro, verifique a disponibilidade de todos os produtos em uma única consulta usando SELECT id, preco, disponivel FROM produtos WHERE id = ANY(%s), passando a lista de IDs dos itens solicitados. O operador ANY do PostgreSQL aceita um array e é a forma correta de verificar múltiplos IDs em uma única roundtrip ao banco. Compare o número de produtos retornados com o número de IDs distintos enviados: se forem diferentes, algum produto não existe — faça rollback e retorne 404. Se algum produto retornado tiver disponivel = False, faça rollback e retorne 422 com uma mensagem que identifique quais produtos estão indisponíveis.
Segundo, calcule o total do pedido usando os preços retornados pelo banco de dados, e não os preços que o cliente enviou no body da requisição. Esse é um princípio de segurança básico: o cliente não pode determinar o quanto vai pagar. Para cada item recebido no body, localize o produto correspondente nos resultados do banco e multiplique quantidade por preco (do banco). Some todos os valores para obter o total.
Terceiro, insira o pedido com INSERT INTO pedidos (usuario_id, total) VALUES (%s, %s) RETURNING id e recupere o ID gerado pelo banco. Quarto, para cada item do pedido, insira uma linha em itens_pedido com pedido_id, produto_id, quantidade e preco_unitario (o preço do banco, não do cliente). Quinto, execute conn.commit().
Após o commit, enfileire uma mensagem no SQS FIFO. Use boto3.client('sqs') e o método send_message, passando QueueUrl (lida de os.environ["SQS_PEDIDOS_URL"]), MessageBody como JSON com os campos pedidoId, usuarioId e evento: "PEDIDO_CRIADO", MessageGroupId como "pedidos" e MessageDeduplicationId como str(pedido_id). Retorne statusCode 201 com body {"pedidoId": pedido_id, "total": float(total)} — o float() converte o Decimal do psycopg2 para um tipo serializável pelo JSON padrão. Em qualquer exceção durante a transação, execute conn.rollback() no bloco except antes de retornar 500.
A Lambda Function para PATCH /pedidos/{pedidoId}/status é mais simples mas tem um detalhe que muitos estudantes erram: o código de retorno bem-sucedido é 204, não 200. O código 204 significa “sem conteúdo” e não possui body. Extraia pedidoId de pathParameters, converta para inteiro e leia novoStatus do body. Valide que novoStatus é um dos valores permitidos pelo schema: confirmado, em_preparo, saiu_para_entrega, entregue ou cancelado. Se o valor não for válido, retorne 400. Execute o UPDATE com RETURNING id: se cursor.fetchone() for None, nenhuma linha foi afetada e o pedido não existe — retorne 404. Se a atualização for bem-sucedida, retorne statusCode 204 com body: "".
Parte Dart — PedidoNotifier e tela:
Defina um sealed class PedidoState com quatro variantes: PedidoInicial, PedidoCarregando, PedidoSucesso com campos pedidoId (int) e total (double), e PedidoErro com campo mensagem (String). A palavra-chave sealed garante que o switch sobre esse tipo seja exaustivo em tempo de compilação — se você esquecer de tratar PedidoCarregando, o compilador emite um aviso.
A classe PedidoNotifier extends ChangeNotifier deve ter estado interno PedidoState _estado = PedidoInicial() e um getter público PedidoState get estado. O método Future<void> criarPedido(String usuarioId, List<({int produtoId, int quantidade})> itens) deve: primeiro, atualizar o estado para PedidoCarregando() e notificar os ouvintes; depois, construir o body JSON com jsonEncode({'usuarioId': usuarioId, 'itens': itens.map((i) => {'produtoId': i.produtoId, 'quantidade': i.quantidade}).toList()}) e fazer o POST com o header Content-Type: application/json.
O tratamento de status codes deve ser preciso: 201 indica sucesso e você deve desserializar o body para extrair pedidoId e total, atualizando o estado para PedidoSucesso; 400 deve resultar em PedidoErro("Dados inválidos na requisição"); 404 deve resultar em PedidoErro("Um ou mais produtos não foram encontrados"); 422 deve resultar em PedidoErro("Um ou mais produtos estão indisponíveis"); qualquer outro código deve resultar em PedidoErro("Erro inesperado: ${response.statusCode}"). Capture também TimeoutException resultando em PedidoErro("Tempo de resposta esgotado. Verifique sua conexão.").
O método Future<void> atualizarStatus(int pedidoId, String novoStatus) deve fazer PATCH para {urlBase}/pedidos/{pedidoId}/status com body {"novoStatus": novoStatus}. O statusCode 204 significa sucesso silencioso — não tente ler o body de uma resposta 204. O statusCode 404 deve lançar PedidoNaoEncontradoException. Qualquer outro código deve lançar ApiException.
A tela TelaConfirmacaoPedido deve ser um StatelessWidget que usa Consumer<PedidoNotifier> para reagir ao estado. Use um switch exaustivo sobre estado para decidir o que exibir: PedidoInicial — um botão “Confirmar Pedido” que chama notifier.criarPedido(...); PedidoCarregando — um CircularProgressIndicator centralizado com um texto “Processando seu pedido…”; PedidoSucesso — uma coluna com o número do pedido e o valor total formatado em reais; PedidoErro — a mensagem de erro em vermelho e um botão “Tentar novamente” que reseta o estado para PedidoInicial.
Antes de implementar, reflita sobre estas questões: por que o total do pedido deve ser calculado a partir dos preços do banco de dados e não dos valores enviados pelo cliente no body da requisição? O que aconteceria se, após o commit bem-sucedido no banco de dados, o enfileiramento no SQS falhasse — o pedido seria perdido? Como o MessageDeduplicationId baseado no pedidoId protege contra o processamento duplicado de uma mesma mensagem?
O PATCH bem-sucedido retorna statusCode 204, que por definição HTTP não possui body. Se você tentar chamar jsonDecode(response.body) em uma resposta com statusCode 204, receberá uma FormatException porque o body é uma string vazia. Verifique sempre o statusCode antes de qualquer tentativa de decodificar o body. Esse erro é especialmente insidioso porque aparece apenas no caminho de sucesso, não no caminho de erro, e muitos testes não cobrem explicitamente a atualização de status bem-sucedida.
O que deve ser entregue: dois arquivos — g_ex2.py, contendo a Lambda de criação de pedido com transação completa e enfileiramento SQS, e a Lambda de atualização de status com validação de valores permitidos; e g_ex2.dart, contendo o sealed class PedidoState com as quatro variantes, a classe PedidoNotifier com os dois métodos implementados, a TelaConfirmacaoPedido com switch exaustivo e as exceções necessárias. Substitua g pelo nome do seu grupo.
Exercício 3
Arquitetura completa: Lambda + SQS + CloudWatch + Flutter ponta a ponta
Integração vertical completa com logging estruturado, hierarquia de erros e configuração de ambiente
Este é o exercício mais próximo de um sistema real em produção que você verá nesta disciplina. Ele une todas as peças que você aprendeu neste módulo: a Lambda produtora que cria pedidos e enfileira mensagens, a Lambda consumidora que processa a fila e atualiza o status dos pedidos, o logging estruturado em JSON para que o CloudWatch Logs Insights possa filtrar e agregar os eventos por campo, e a camada Flutter com configuração de ambiente via constantes de compilação, hierarquia tipada de exceções de domínio e gerenciamento de estado baseado em sealed class. Cada uma dessas peças tem subtilezas que só aparecem quando você as combina.
O aspecto mais importante deste exercício não é o volume de código — é a qualidade das decisões arquiteturais. Por que a Lambda produtora não deve reverter a transação do banco de dados quando o SQS falha? Por que a Lambda consumidora deve tratar um pedido inexistente de forma diferente de um erro de banco de dados? Por que String.fromEnvironment é uma constante de compilação e não pode ser definida em runtime? Essas perguntas têm respostas concretas, e você deve ser capaz de justificá-las nos comentários do seu código.
O exercício é substancialmente mais extenso que os anteriores, e isso é intencional. Em um ambiente de desenvolvimento real, você raramente implementa uma Lambda isolada — você implementa um fluxo de ponta a ponta que precisa funcionar como um sistema coerente. Planeje sua implementação antes de escrever código, identifique as interfaces entre os componentes e valide cada contrato explicitamente.
Você deve implementar dois artefatos: g_ex3.py e g_ex3.dart. O arquivo Python deve conter dois blocos claramente delimitados por comentários — a Lambda produtora e a Lambda consumidora. O arquivo Dart deve conter todos os componentes listados a seguir.
Parte Python — Lambda produtora (delivery-criar-pedido):
Esta é uma versão aprimorada da Lambda do Exercício 2, com três adições significativas. A primeira é o logging estruturado em JSON. Importe logging e configure logger = logging.getLogger() seguido de logger.setLevel(logging.INFO) no nível de módulo, fora do handler. Implemente uma função auxiliar log(nivel, mensagem, **contexto) que chama logger.log(getattr(logging, nivel.upper()), json.dumps({"fn": "criar-pedido", "mensagem": mensagem, **contexto}, default=str, ensure_ascii=False)). Essa estrutura garante que cada linha de log seja um objeto JSON independente, o que permite ao CloudWatch Logs Insights filtrar por campo, como filter fn = "criar-pedido" and mensagem = "pedido criado". Registre um log INFO no início do handler (com usuario_id e quantidade de itens), outro após o commit (com pedido_id e total) e outro no bloco except (com o tipo e a mensagem da exceção).
A segunda adição é a validação de quantidade. Antes de acessar o banco de dados, valide que cada item do body tem quantidade > 0. Retorne 400 se qualquer item tiver quantidade inválida — essa validação é mais barata que uma consulta ao banco e filtra dados obviamente incorretos antes de qualquer operação de I/O.
A terceira adição é o tratamento separado da falha no SQS. O enfileiramento deve estar em um bloco try/except independente do bloco da transação. Se o SQS falhar após o commit bem-sucedido no banco de dados, registre o erro como WARNING — não como ERROR — e retorne 201 normalmente. Justifique esse comportamento em um comentário: o pedido já está persistido e correto; reverter o commit prejudicaria o usuário por uma falha em um sistema auxiliar. Em sistemas de produção, a solução ideal seria uma Dead Letter Queue e um processo de reconciliação, mas para o escopo deste exercício o comportamento correto é logar e seguir.
Parte Python — Lambda consumidora (delivery-processar-pedido):
A Lambda consumidora é acionada automaticamente pelo SQS quando há mensagens na fila. O evento recebido contém uma lista de registros em event.get("Records", []). Para cada registro, faça json.loads(registro["body"]) para extrair o dicionário com pedidoId e usuarioId.
Para cada registro, execute UPDATE pedidos SET status = 'confirmado', atualizado_em = NOW() WHERE id = %s e commit. Registre um log INFO com pedido_id e usuario_id indicando que o pedido foi processado. Se cursor.rowcount for zero após o UPDATE, significa que o pedido não existe no banco — talvez tenha sido cancelado ou excluído entre o momento do enfileiramento e o processamento. Nesse caso, registre um log WARNING e continue sem lançar exceção. Isso é intencional: lançar exceção aqui faria o SQS devolver a mensagem à fila, gerando retentativas para um pedido que sabidamente não existe. Para erros de banco de dados genuínos — como falha de conexão ou timeout — relance a exceção. O SQS, ao receber a exceção, devolverá a mensagem à fila e tentará novamente após o período de visibilidade configurado.
Parte Dart — Estrutura completa:
Defina a classe AppConfig com um campo estático env declarado como static const String env = String.fromEnvironment('APP_ENV', defaultValue: 'dev') e um getter estático urlBase que retorna a URL correspondente ao ambiente: 'https://abc123.execute-api.sa-east-1.amazonaws.com/dev' para dev e 'https://abc123.execute-api.sa-east-1.amazonaws.com/prod' para qualquer outro valor. O String.fromEnvironment é uma constante de compilação — o valor é fixado no momento em que você executa flutter run --dart-define=APP_ENV=prod ou flutter build, não em tempo de execução.
Implemente a hierarquia de exceções de domínio. Defina abstract class ErroDelivery implements Exception com um campo mensagem e um construtor const. A partir dela, derive class ErroRede extends ErroDelivery, class ErroAutenticacao extends ErroDelivery, class ErroPermissao extends ErroDelivery, class ErroLimiteTaxa extends ErroDelivery, class ErroServico extends ErroDelivery, class ErroNaoEncontrado extends ErroDelivery, class ErroEntidade extends ErroDelivery (para respostas 400 e 422) e class ErroServidor extends ErroDelivery (para 500). Todos os construtores devem ser const.
A classe HttpPedidoRepositorio deve implementar um método Future<PedidoCriado> criarPedido(String usuarioId, List<Map<String, int>> itens). Use switch expression do Dart 3 sobre response.statusCode para mapear cada código ao comportamento correto: 201 => desserializar, 400 => throw ErroEntidade(...), 401 => throw ErroAutenticacao(...), 403 => throw ErroPermissao(...), 404 => throw ErroNaoEncontrado(...), 422 => throw ErroEntidade(...), 429 => throw ErroLimiteTaxa(...), 503 => throw ErroServico(...), _ => throw ErroServidor(...). O wildcard _ captura todos os demais status codes, incluindo 500. Envolva a chamada HTTP em um bloco try/catch que captura TimeoutException — lançando ErroRede — e SocketException — também lançando ErroRede.
Defina a interface abstrata IProdutoRepositorio com o método Future<List<Produto>> listarProdutos({String? categoriaId}). A classe CardapioNotifier extends ChangeNotifier deve receber IProdutoRepositorio repositorio no construtor — nunca HttpProdutoRepositorio diretamente. Isso garante que a camada de apresentação dependa de uma abstração, não de uma implementação concreta.
Defina sealed class CardapioState com as variantes: CardapioCarregando, CardapioSucesso com campo produtos (List<Produto>), CardapioErro com campo mensagem (String) e CardapioAviso com campos produtos (List<Produto>) e aviso (String) — esse último estado representa o caso em que os dados foram carregados mas há uma informação relevante para exibir, como “alguns produtos estão temporariamente indisponíveis”.
A tela TelaCardapioAws deve ser um StatelessWidget com Consumer<CardapioNotifier> e um switch exaustivo sobre o CardapioState atual: CardapioCarregando — indicador de progresso; CardapioSucesso — grid de produtos; CardapioErro — mensagem de erro e botão “Tentar novamente”; CardapioAviso — grid de produtos com um banner de aviso na parte superior da tela.
Antes de implementar, reflita sobre estas questões: por que a Lambda produtora não deve reverter o commit do banco de dados quando o enfileiramento no SQS falha? O que acontece com a mensagem SQS se a Lambda consumidora lança uma exceção — ela é perdida ou será processada novamente? Como o AppConfig com String.fromEnvironment elimina a necessidade de alterar o código-fonte para trocar entre ambientes de desenvolvimento e produção?
No Dart, String.fromEnvironment é uma constante de compilação — ela não pode receber um valor calculado em tempo de execução. Isso significa que, para trocar o ambiente de dev para prod, você precisa passar --dart-define=APP_ENV=prod no momento da compilação, usando flutter run --dart-define=APP_ENV=prod ou flutter build apk --dart-define=APP_ENV=prod. Qualquer tentativa de definir essa variável dentro de main() ou em qualquer método executado em runtime não terá efeito sobre const String.fromEnvironment(...), porque o valor já foi fixado pelo compilador antes da execução.
O que deve ser entregue: dois arquivos — g_ex3.py, contendo a Lambda produtora com logging estruturado, validação de quantidade e enfileiramento SQS em bloco separado, e a Lambda consumidora com processamento de registros SQS, tratamento diferenciado para pedidos inexistentes versus erros de banco e logging estruturado; e g_ex3.dart, contendo AppConfig, a hierarquia completa de exceções ErroDelivery, HttpPedidoRepositorio com switch expression, IProdutoRepositorio, sealed class CardapioState com as quatro variantes, CardapioNotifier com injeção de interface e TelaCardapioAws com switch exaustivo. Substitua g pelo nome do seu grupo.