Módulo 12 — Exercícios: Notificações Push com Firebase Cloud Messaging

Chegamos ao módulo em que o aplicativo de delivery deixa de ser uma experiência passiva — em que o usuário precisa abrir o aplicativo para descobrir o que aconteceu com o pedido — e passa a ser proativo, alcançando o usuário no exato momento em que algo relevante acontece. O Firebase Cloud Messaging é o motor dessa transformação: ele permite que o backend envie notificações diretamente ao dispositivo do usuário, independentemente de o aplicativo estar aberto, em segundo plano ou completamente encerrado. Os três exercícios a seguir percorrem esse caminho de forma progressiva: o primeiro estabelece a fundação da configuração Firebase e do recebimento de notificações em foreground com notificações locais; o segundo amplia para o tratamento completo do ciclo de vida — foreground, background e terminated — e o roteamento inteligente ao tocar em notificações com go_router; o terceiro fecha o ciclo integrando o backend AWS, com a Lambda Function que envia notificações quando o status do pedido muda e o repositório Flutter que gerencia o token FCM e a inscrição em tópicos. Leia cada enunciado na íntegra antes de escrever qualquer linha de código, pois as decisões de design que você tomar aqui afetam diretamente a experiência do usuário em situações que são difíceis de testar localmente.


Exercício 1

FirebaseNotificacaoServico e notificações em foreground: a fundação do sistema

Antes de notificar, é preciso configurar, inicializar e escutar corretamente

Toda a cadeia de notificações push do aplicativo de delivery começa com um conjunto de decisões técnicas tomadas ainda no processo de inicialização do aplicativo, antes de qualquer interação do usuário. Se o Firebase não for inicializado no momento correto, se o handler de background não for registrado antes do runApp, se o google-services.json estiver na pasta errada — qualquer um desses detalhes fará com que as notificações simplesmente não funcionem, sem nenhuma mensagem de erro clara. Este exercício trata da fundação: você vai implementar a inicialização completa do Firebase no main.dart, criar o FirebaseNotificacaoServico com o handler de foreground e as notificações locais, e solicitar a permissão de notificação ao usuário no momento contextualmente correto.

O contexto importa aqui. Você aprendeu que o FCM entrega mensagens de três formas distintas dependendo do estado do aplicativo. Quando o aplicativo está aberto e visível para o usuário — estado foreground — o FCM entrega a mensagem ao stream FirebaseMessaging.onMessage, mas não exibe automaticamente nenhuma notificação na bandeja do sistema. Essa decisão de design faz sentido: seria perturbador exibir uma notificação de bandeja enquanto o usuário já está interagindo com o aplicativo. A responsabilidade de apresentar a informação ao usuário é, portanto, do código do aplicativo. No contexto do delivery, você vai usar dois mecanismos complementares: um SnackBar para informar brevemente o usuário quando uma notificação chega, e o pacote flutter_local_notifications para exibir uma notificação completa na bandeja — especialmente útil quando o usuário está em uma tela que não é a tela do pedido em questão.

O primeiro componente que você deve implementar é a inicialização do Firebase no main.dart. Antes de chamar runApp, você precisa chamar WidgetsFlutterBinding.ensureInitialized() — esse método é obrigatório sempre que operações assíncronas são realizadas no main antes do runApp, pois os bindings do Flutter precisam estar prontos para operações que envolvem canais de plataforma. Em seguida, inicialize o Firebase com await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform), onde DefaultFirebaseOptions vem do arquivo firebase_options.dart gerado pela ferramenta flutterfire configure. Por fim, registre o handler de background com FirebaseMessaging.onBackgroundMessage(_handlerMensagemBackground). Essa chamada precisa ocorrer antes do runApp, não dentro de um widget, porque o Firebase precisa conhecer o handler antes de o isolate principal do Flutter ser configurado.

O handler de background é uma função de nível superior — não pode ser um método de classe, não pode ser uma closure — e precisa ser anotada com @pragma('vm:entry-point') para sobreviver ao processo de tree-shaking que o compilador Dart aplica em builds de release. O corpo do handler, neste exercício, deve inicializar o Firebase e salvar os dados da notificação recebida em SharedPreferences para que o aplicativo os processe quando o usuário o abrir. Especificamente, se o campo mensagem.data['tipo'] for 'status_pedido', salve o pedidoId e o status nas chaves 'notificacao_pendente_pedido_id' e 'notificacao_pendente_status' da SharedPreferences. Isso é necessário porque, no contexto do handler de background, o Flutter não está renderizando widgets, nenhum BuildContext está disponível e os providers do Provider não estão inicializados — o SharedPreferences é o único mecanismo seguro de persistência disponível nesse contexto.

O segundo componente é o FirebaseNotificacaoServico, que deve residir na pasta features/notificacoes/infraestrutura/. Essa classe recebe uma instância de FirebaseMessaging pelo construtor, com FirebaseMessaging.instance como valor padrão, garantindo que em testes unitários você possa substituir a instância real por um mock sem alterar o código de produção. A classe deve conter um StreamController<RemoteMessage>.broadcast() privado, exposto como um getter público onMensagemRecebida do tipo Stream<RemoteMessage>. O método configurarHandlerForeground configura a escuta do stream FirebaseMessaging.onMessage e publica cada mensagem recebida no StreamController, além de chamar exibirNotificacaoLocal — descrita adiante — para que a notificação apareça na bandeja mesmo com o aplicativo aberto. O método dispose deve fechar o StreamController. Os métodos solicitarPermissao e obterToken devem seguir a implementação descrita no material do módulo: o primeiro chama requestPermission com alert: true, badge: true e sound: true, retornando true para os status authorized e provisional; o segundo retorna a string do token ou null em caso de exceção.

O terceiro componente é a função exibirNotificacaoLocal, que recebe um RemoteMessage e usa o pacote flutter_local_notifications para exibir uma notificação na bandeja do sistema. Para o canal de notificação, use o id 'pedidos_delivery', com nome 'Atualizações de Pedidos' e Importance.high. O FlutterLocalNotificationsPlugin deve ser inicializado no main.dart, após a inicialização do Firebase, usando AndroidInitializationSettings('@mipmap/ic_launcher'). No momento de exibir, o payload da notificação deve ser a serialização JSON do mapa mensagem.data — isso é o que permitirá, em exercícios posteriores, navegar para a tela correta quando o usuário tocar na notificação local.

O quarto componente é uma tela simples de demonstração que escuta o stream onMensagemRecebida do serviço e exibe, em um ListView, as últimas notificações recebidas com título, corpo e os pares chave-valor do campo data. A tela deve solicitar a permissão de notificação ao usuário no momento adequado: não no initState da tela de login, mas sim em resposta a uma ação contextual — neste exercício, exiba um botão “Ativar notificações para acompanhar meus pedidos” que, quando pressionado, exibe primeiro um AlertDialog explicando o valor das notificações e só então chama solicitarPermissao. Se o usuário negar, exiba uma mensagem discreta de que ele pode ativar nas configurações do sistema; nunca bloqueie a navegação ou exiba um erro em resposta à negação.

Todos os componentes devem ser implementados em um único arquivo g_ex1.dart com uma main que demonstra o fluxo completo. Como o exercício envolve Firebase — que requer configuração de projeto Google — na demonstração da main você pode criar mocks das classes Firebase para que o arquivo compile e execute sem credenciais reais, mas os comentários devem deixar claro o que seria diferente em produção.

Antes de implementar, reflita sobre três questões que vão além da sintaxe. Por que o handler de background precisa ser uma função de nível superior anotada com @pragma('vm:entry-point'), e o que aconteceria se você tentar usar um método de classe? O que significa dizer que o FCM não exibe a notificação automaticamente em foreground, e por que essa decisão de design faz sentido para o usuário? Por que salvar os dados da notificação em SharedPreferences no handler de background é necessário — e não, por exemplo, atualizar diretamente um ChangeNotifier?

O canal de notificação no Android precisa ser criado antes de qualquer notificação ser exibida. Se você tentar exibir uma notificação com um channel_id que ainda não existe, o sistema operacional silenciosamente ignora a notificação a partir do Android 8.0 — não há nenhuma exceção, nenhuma mensagem de log no console do Flutter, e a notificação simplesmente não aparece. Por isso, a criação do canal com createNotificationChannel deve acontecer na inicialização do aplicativo, e não no momento em que a primeira notificação é recebida. Além disso, canais criados com uma determinada configuração de importância não podem ter sua importância reduzida posteriormente via código — só o usuário pode mudar a importância de um canal nas configurações do sistema.

O que deve ser entregue: um arquivo chamado g_ex1.dart, onde g é o nome do seu grupo.


Exercício 2

Ciclo de vida completo e roteamento por notificação: foreground, background e terminated

Uma notificação sem roteamento inteligente é uma oportunidade de engajamento perdida

Imagine a experiência do usuário do delivery. O pedido foi feito dez minutos atrás. O telefone está no bolso. Uma notificação chega: “Seu pedido saiu para entrega!” O usuário tira o telefone do bolso e toca na notificação. O que acontece a seguir define a qualidade do aplicativo: se o aplicativo abrir na tela inicial genérica, o usuário vai precisar navegar até encontrar o pedido em questão — uma experiência frustrantemente redundante. Se o aplicativo abrir diretamente na tela de acompanhamento do pedido #42, o usuário encontra exatamente o que procurava sem nenhum esforço. A diferença entre esses dois comportamentos é o que você vai implementar neste exercício: o roteamento inteligente baseado no payload da notificação, cobrindo os três estados em que o aplicativo pode estar quando a notificação é recebida.

O desafio técnico central deste exercício é que cada um dos três estados — foreground, background e terminated — requer uma abordagem diferente para detectar o toque na notificação. Em foreground, você escuta o stream onMensagemRecebida do FirebaseNotificacaoServico que você implementou no Exercício 1, e ao tocar na notificação local (gerenciada pelo flutter_local_notifications), o callback onDidReceiveNotificationResponse fornece os dados. Em background, o Firebase dispara o evento FirebaseMessaging.onMessageOpenedApp quando o usuário toca na notificação da bandeja — o aplicativo está em memória, então o roteador já está inicializado. Em terminated, quando o usuário toca na notificação e o sistema operacional inicializa o aplicativo do zero, nem o onMessageOpenedApp nem o onMensagemRecebida estão disponíveis: você precisa chamar getInitialMessage() durante a inicialização para recuperar a mensagem que causou a abertura.

O primeiro componente deste exercício é o NotificacaoNotifier, que deve estender ChangeNotifier. Ele é responsável por coordenar o estado das notificações na camada de apresentação e serve como intermediário entre os eventos de notificação e o roteador da aplicação. Ele recebe pelo construtor um FirebaseNotificacaoServico e um GoRouter (para navegar programaticamente sem precisar de um BuildContext). O notifier deve expor um campo List<NotificacaoItem> público que acumula as últimas notificações recebidas — limite a lista a, no máximo, 50 itens para evitar consumo excessivo de memória. A classe NotificacaoItem é um modelo simples com campos final: titulo, corpo, tipo, pedidoId e recebidaEm (um DateTime). O notifier deve expor também um int contadorNaoLidas, que é decrementado quando o usuário navega para a tela de uma notificação.

O método inicializar do NotificacaoNotifier é onde toda a configuração acontece e deve ser chamado uma única vez, no initState do widget raiz do aplicativo. Esse método deve executar quatro tarefas. A primeira é configurar o listener de foreground: escute o stream _servico.onMensagemRecebida, adicione cada mensagem como NotificacaoItem à lista, incremente o contador e notifique os listeners. A segunda é configurar o listener de background: escute FirebaseMessaging.onMessageOpenedApp, que será disparado quando o usuário tocar em uma notificação enquanto o aplicativo está em background — ao receber esse evento, extraia o pedidoId e navegue para /pedidos/$pedidoId usando o GoRouter injetado. A terceira é verificar a mensagem inicial: chame FirebaseMessaging.instance.getInitialMessage() e, se o resultado for não-nulo, agende a navegação usando WidgetsBinding.instance.addPostFrameCallback para garantir que o roteador já esteja pronto. A quarta é verificar e processar notificações pendentes: leia as chaves 'notificacao_pendente_pedido_id' e 'notificacao_pendente_status' da SharedPreferences — salvas pelo handler de background no Exercício 1 — e, se existirem, crie um NotificacaoItem, adicione à lista, limpe as chaves da SharedPreferences e notifique os listeners.

O segundo componente é o DeliveryApp atualizado como StatefulWidget. No initState, obtenha o NotificacaoNotifier via context.read e chame inicializar. O widget raiz deve ser construído com MaterialApp.router usando a instância do GoRouter injetada pelo GetIt. A razão para usar StatefulWidget aqui — e não inicializar no main.dart — é que o NotificacaoNotifier precisa de um BuildContext para poder acessar os providers via context.read, e o BuildContext só está disponível dentro de um widget.

O terceiro componente é o listener de toque em notificações locais. No callback onDidReceiveNotificationResponse do flutter_local_notifications, você recebe um NotificationResponse cujo campo payload contém a serialização JSON do mapa data da mensagem FCM. Deserialize esse JSON, extraia o pedidoId e navegue para /pedidos/$pedidoId. O listener deve ser registrado durante a inicialização do flutter_local_notifications no main.dart, e a navegação deve ser feita via a instância do GoRouter do GetIt — nunca tente acessar um BuildContext nesse callback, pois ele pode ser chamado fora da árvore de widgets.

O quarto componente é a TelaNotificacoes, que exibe a lista notifier.notificacoes do NotificacaoNotifier. Cada item deve mostrar o título, o corpo, o tipo e o horário de recebimento formatado. Ao tocar em um item cujo tipo seja 'status_pedido' e cujo pedidoId seja não-nulo, navegue para /pedidos/${item.pedidoId} e decremente o contador de não lidas. A tela deve exibir um badge com o contadorNaoLidas no ícone de notificações da barra de navegação inferior.

O quinto componente é uma tela de detalhe de pedido mínima (TelaPedido) que recebe o pedidoId como parâmetro de rota — use o sistema de parâmetros do go_router — e exibe o texto “Pedido #$pedidoId” centralizado. Em um cenário real, essa tela buscaria os dados do pedido via API, mas para este exercício o objetivo é demonstrar que o roteamento funciona corretamente ao tocar em uma notificação em qualquer um dos três estados.

Implemente todos os componentes no arquivo g_ex2.dart. O GoRouter deve ser configurado com pelo menos as rotas /home, /notificacoes e /pedidos/:pedidoId. Use context.push para navegações que devem empilhar telas na pilha de navegação (como ao tocar em uma notificação quando o aplicativo já está aberto) e GoRouter.go (através da instância do GoRouter, sem BuildContext) quando a navegação é disparada de fora da árvore de widgets.

Reflita sobre por que getInitialMessage() precisa ser chamado apenas uma vez na vida do aplicativo, e o que aconteceria se você o chamasse toda vez que o widget raiz é reconstruído. Por que addPostFrameCallback é necessário antes de navegar com o resultado de getInitialMessage()? O que aconteceria se você tentasse navegar imediatamente dentro do initState, antes do primeiro frame ser renderizado? Por que é importante limpar as chaves de notificação pendente da SharedPreferences imediatamente após processá-las, e não aguardar o próximo ciclo de renderização?

O evento FirebaseMessaging.onMessageOpenedApp só é disparado quando o aplicativo está em background — não quando está em foreground e não quando está encerrado. Confundir esses três casos é uma fonte comum de bugs difíceis de reproduzir: o desenvolvedor testa tocando na notificação enquanto o aplicativo está aberto (foreground) e não vê o handler ser chamado, concluindo erroneamente que há um problema no código. Na verdade, o handler simplesmente não é o mecanismo correto para foreground — nesse estado, é o onDidReceiveNotificationResponse do flutter_local_notifications que trata o toque. Cada estado tem seu próprio mecanismo, e entender essa distinção é o que separa uma implementação robusta de uma que “funciona na maioria das vezes”.

O que deve ser entregue: um arquivo chamado g_ex2.dart, onde g é o nome do seu grupo.


Exercício 3

Integração completa: Lambda AWS, token FCM, tópicos e envio por mudança de status

O ciclo só fecha quando o backend consegue alcançar o dispositivo do usuário

Os dois exercícios anteriores trataram do lado do Flutter: configurar o Firebase, receber mensagens nos três estados e rotear o usuário para a tela correta ao tocar em uma notificação. Esse trabalho é necessário, mas insuficiente: sem um backend que saiba para qual token enviar a notificação e que a dispare no momento certo — quando o status do pedido muda — toda a estrutura de recebimento que você construiu permanece inerte. Este exercício fecha o ciclo: você vai implementar o HttpNotificacaoRepositorio no Flutter para registrar e renovar o token FCM no backend, a Lambda Function Python que envia notificações usando o Firebase Admin SDK quando a fila SQS entrega um evento de mudança de status, e o mecanismo de inscrição em tópicos para notificações coletivas como promoções e avisos de plataforma.

O contexto arquitetural é importante. A Lambda delivery-processar-pedido que você criou no Módulo 10 processa mensagens da fila SQS. Até agora, ela apenas atualizava o status do pedido no banco de dados. Neste exercício, você vai expandi-la para, após atualizar o status, buscar os tokens FCM ativos do usuário na tabela tokens_fcm e enviar a notificação correspondente via Firebase Admin SDK. Isso significa que o caminho completo de uma notificação de status de pedido passa por cinco sistemas distintos: o aplicativo Flutter (que criou o pedido via API Gateway), a Lambda criar-pedido (que enfileirou a mensagem no SQS), a Lambda processar-pedido (que atualiza o banco e envia a notificação), o Firebase FCM (que armazena e entrega a mensagem), e o dispositivo Android do usuário (que a recebe e a exibe).

O primeiro componente do lado Flutter é o HttpNotificacaoRepositorio, que reside na pasta features/notificacoes/infraestrutura/. Essa classe implementa a interface INotificacaoRepositorio — que você pode definir como uma classe abstrata na pasta features/notificacoes/dominio/ com os métodos registrarToken, desativarToken e inscreverEmTopico. O repositório recebe um HttpClienteAutenticado — o cliente HTTP com renovação transparente de tokens que você implementou no Módulo 11 — e a String baseUrl pelo construtor. O método registrarToken faz um POST /notificacoes/token com o corpo JSON {'tokenFcm': token, 'plataforma': 'android'} e trata o código de resposta: 200 indica sucesso, 401 relança SessaoExpiradaException (que o cliente HTTP já deveria ter lançado antes), e qualquer outro código lança uma NotificacaoException com a mensagem de erro do corpo da resposta. O método desativarToken faz um DELETE /notificacoes/token com o mesmo token no corpo. O método inscreverEmTopico não faz uma chamada HTTP — ele usa diretamente o FirebaseMessaging.instance.subscribeToTopic(topico), pois a inscrição em tópicos é gerenciada pelo próprio SDK do Firebase no dispositivo, sem passar pelo backend da aplicação.

O segundo componente Flutter é a expansão do SessaoNotifier do Módulo 11. Após um login bem-sucedido, ele deve: solicitar a permissão de notificação via FirebaseNotificacaoServico.solicitarPermissao(); obter o token com obterToken(); registrar o token no backend via INotificacaoRepositorio.registrarToken(); inscrever o dispositivo nos tópicos 'avisos_plataforma' e, se o usuário aceitou promoções nas configurações, 'promocoes_delivery'; e configurar o listener de renovação de token com ouvirRenovacaoToken, que deve chamar registrarToken com o novo token automaticamente. Toda essa sequência deve ser executada em um bloco try-catch que trata falhas sem propagá-las como erros fatais — falhar ao registrar o token FCM não impede o usuário de usar o aplicativo, apenas o priva de receber notificações naquele dispositivo.

O terceiro componente é o schema SQL e a Lambda Python de registro de token. O schema da tabela tokens_fcm deve ser criado como mostrado a seguir — não precisa de tabset, pois é SQL:

CREATE TABLE IF NOT EXISTS tokens_fcm (
    id            SERIAL PRIMARY KEY,
    usuario_id    UUID NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE,
    token         TEXT NOT NULL UNIQUE,
    plataforma    VARCHAR(10) NOT NULL DEFAULT 'android'
                      CHECK (plataforma IN ('android', 'ios', 'web')),
    ativo         BOOLEAN NOT NULL DEFAULT TRUE,
    criado_em     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    atualizado_em TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_tokens_fcm_usuario_ativo
    ON tokens_fcm(usuario_id)
    WHERE ativo = TRUE;

A Lambda delivery-registrar-token em Python deve extrair o usuario_id do campo requestContext.authorizer.usuarioId — preenchido pelo Lambda Authorizer do Módulo 11 — ler o tokenFcm e a plataforma do corpo da requisição, e executar um UPSERT na tabela tokens_fcm: se o token já existir, atualizar o usuario_id, a plataforma, definir ativo = TRUE e atualizar o atualizado_em; se não existir, inserir um novo registro. O UPSERT com ON CONFLICT (token) DO UPDATE é a forma correta de implementar isso, pois evita race conditions em inserções paralelas e garante que o token sempre esteja associado ao usuário mais recente que o utilizou — o que é importante quando o mesmo dispositivo é compartilhado ou quando o usuário reinstala o aplicativo e faz login com uma conta diferente.

O quarto componente é a expansão da Lambda delivery-processar-pedido. Após o código existente que atualiza o status do pedido no banco de dados, adicione as seguintes etapas: inicializar o Firebase Admin SDK lendo as credenciais da Service Account armazenadas no AWS Secrets Manager; buscar os tokens FCM ativos do usuário com SELECT token FROM tokens_fcm WHERE usuario_id = %s AND ativo = TRUE; se houver tokens, chamar messaging.send_each_for_multicast com um MulticastMessage contendo os campos notification (com título e corpo baseados no novo status), data (com tipo, pedidoId e status como strings) e android (com priority='high', ttl=3600 e channel_id='pedidos_delivery'); após o envio, iterar sobre as respostas e marcar como ativo = FALSE os tokens cujo erro seja 'registration-token-not-registered' ou 'invalid-registration-token', pois esses erros indicam que o aplicativo foi desinstalado ou o token é inválido. As mensagens de status devem ser amigáveis para o usuário: 'confirmado' exibe “Pedido confirmado! Será preparado em breve.”, 'em_preparo' exibe “Seu pedido está sendo preparado!”, 'saiu_para_entrega' exibe “A caminho! Seu pedido saiu para entrega.”, 'entregue' exibe “Pedido entregue! Bom apetite.”, e 'cancelado' exibe “Pedido cancelado. Entre em contato se precisar de ajuda.”

O quinto componente é uma Lambda opcional — mas de grande valor arquitetural — chamada delivery-enviar-promocao, que envia uma notificação para o tópico 'promocoes_delivery' usando messaging.Message(topic='promocoes_delivery', ...). Ao contrário do envio por token, o envio por tópico não requer busca no banco de dados: o FCM gerencia internamente a lista de dispositivos inscritos. Essa Lambda deve receber um payload com titulo, corpo e opcionalmente promocaoId, e enviar a notificação com prioridade 'normal' e ttl=86400 (24 horas) — promoções têm menos urgência que atualizações de status de pedido. Inclua essa implementação no arquivo de entrega, mesmo que seja uma demonstração sem a infraestrutura completa do AWS.

Todos os componentes Flutter devem ser implementados em g_ex3.dart. Os arquivos Python das Lambdas devem ser incluídos como strings de código comentadas no mesmo arquivo Dart, precedidas pelo comentário // --- Lambda Python: nome_da_funcao.py ---, para que o professor possa avaliar ambas as partes em um único arquivo de entrega.

Reflita sobre três aspectos arquiteturais que este exercício levanta. Por que a tabela tokens_fcm usa uma constraint UNIQUE no campo token e não no par (usuario_id, token), e qual a consequência prática dessa escolha para um dispositivo compartilhado entre dois usuários que fazem login alternadamente? Por que marcar tokens inválidos como ativo = FALSE — em vez de deletá-los imediatamente — é a abordagem mais segura para um sistema em produção? Por que a inscrição em tópicos FCM é feita diretamente no dispositivo via SDK, sem passar pelo backend da aplicação, enquanto o registro do token de dispositivo segue o caminho oposto?

O Firebase Admin SDK para Python precisa ser inicializado apenas uma vez por instância da Lambda. O padrão correto é usar uma variável global _firebase_inicializado = False e um bloco if not _firebase_inicializado: que inicializa o SDK e então define a variável como True. Se você chamar firebase_admin.initialize_app() mais de uma vez na mesma instância — o que pode acontecer se a variável de controle não existir — o SDK lança uma exceção ValueError: The default Firebase app already exists. Lembre-se também de que as credenciais da Service Account nunca devem ser incluídas diretamente no código-fonte da Lambda: elas devem ser lidas do AWS Secrets Manager a cada inicialização da instância — não a cada invocação — pois leituras frequentes do Secrets Manager têm custo e latência adicionais.

O que deve ser entregue: um arquivo chamado g_ex3.dart, onde g é o nome do seu grupo. As implementações Python das Lambdas devem estar incluídas no mesmo arquivo como comentários de código, conforme descrito no enunciado.