graph TD
A["MaterialApp"] --> B["Scaffold"]
B --> C["body: SafeArea"]
C --> D["SingleChildScrollView"]
D --> E["Padding<br/>(h: 24, v: 32)"]
E --> F["Column<br/>(crossAxisAlignment: stretch)"]
F --> G["SizedBox<br/>(height: 64)"]
F --> H["Center"]
H --> H1["Image.asset<br/>logo, height: 80"]
F --> I["SizedBox<br/>(height: 40)"]
F --> J["Text<br/>'Bem-vindo de volta'<br/>(headlineSmall, bold)"]
F --> K["SizedBox<br/>(height: 8)"]
F --> L["Text<br/>'Acesse sua conta para continuar'<br/>(bodyMedium)"]
F --> M["SizedBox<br/>(height: 32)"]
F --> N["TextField<br/>(label: 'E-mail')<br/>(prefixIcon: email_outlined)"]
F --> O["SizedBox<br/>(height: 16)"]
F --> P["TextField<br/>(label: 'Senha')<br/>(obscureText: _senhaVisivel)<br/>(suffixIcon: toggle visibilidade)"]
F --> Q["SizedBox<br/>(height: 8)"]
F --> R["Align<br/>(centerRight)"]
R --> S["TextButton<br/>'Esqueci minha senha'"]
F --> T["SizedBox<br/>(height: 24)"]
F --> U["ElevatedButton<br/>'Entrar'<br/>(onPressed: _handleLogin)"]
F --> V["SizedBox<br/>(height: 16)"]
F --> W["Row<br/>(mainAxisAlignment: center)"]
W --> X["Text<br/>'Nao tem conta? '"]
W --> Y["TextButton<br/>'Cadastre-se'"]
Módulo 03 — Exercícios: Widgets Fundamentais e Árvore de Widgets
Estes exercícios foram elaborados para que você consolide, na prática, o que estudou no material do Módulo 03. Cada um exige que você raciocine sobre decisões de design de interfaces — quando usar StatelessWidget ou StatefulWidget, como gerenciar o ciclo de vida corretamente, como organizar a árvore de widgets de forma que ela seja legível e sustentável. Não se trata de memorizar sintaxe: trata-se de desenvolver o julgamento de um programador Flutter que constrói interfaces com intenção. Leia o enunciado de cada exercício com atenção, pense antes de escrever qualquer linha de código e, quando tiver dúvidas, consulte o material do módulo — mas tente resolver sozinho antes.
Exercício 1
Componentes Visuais com StatelessWidget e StatefulWidget
A diferença entre um widget que exibe e um widget que reage
O primeiro passo para construir qualquer tela Flutter é identificar quais partes dela precisam de estado e quais são puramente descritivas. Esse julgamento é o que separa um código bem estruturado de um código onde tudo é StatefulWidget sem necessidade — ou onde estados importantes ficam escondidos em lugares errados. Este exercício coloca você diante exatamente dessa decisão, em um contexto diretamente ligado ao Projeto Integrador.
Considere que o seu aplicativo precisa exibir, na tela principal, um cartão visual para cada produto do catálogo. Cada cartão deve mostrar o nome do produto, a categoria, o preço formatado em reais e um ícone de coração que o usuário pode tocar para marcar ou desmarcar o produto como favorito. Além disso, a tela deve exibir, no topo, um contador que indica quantos produtos foram favoritados até o momento.
A sua tarefa é implementar dois widgets distintos, com as seguintes características e restrições.
O primeiro widget se chama CartaoProduto. Ele recebe, por parâmetros, o nome do produto (String), a categoria (String), o preço (double), um booleano favoritado que indica o estado atual do favorito, e uma função de callback sem parâmetros chamada aoAlternarFavorito. O widget exibe as informações do produto em um Card com InkWell (para que toda a área seja tocável), o nome em negrito, a categoria em um badge colorido usando Container com BoxDecoration, o preço formatado como R$ X.XX e o ícone de coração — vermelho se favoritado for verdadeiro, cinza caso contrário. Este widget deve ser obrigatoriamente um StatelessWidget, pois ele não gerencia estado: apenas exibe o que recebe e notifica o pai quando o usuário interage.
O segundo widget se chama ContadorFavoritos. Ele recebe uma lista de produtos (use a classe Produto com os campos id, nome, categoria, preco e disponivel) e exibe, no centro da tela, o número de produtos favoritados junto com a lista de cartões. Ele é responsável por manter o conjunto de IDs favoritados em seu estado interno, expondo para cada CartaoProduto o estado correto do favorito e o callback para alternar. Este widget deve ser obrigatoriamente um StatefulWidget.
Implemente também uma função main que execute o aplicativo com um MaterialApp simples e exiba o ContadorFavoritos com pelo menos cinco produtos na lista, incluindo pelo menos um com disponivel igual a false. Produtos indisponíveis devem ser exibidos com opacidade reduzida e sem a possibilidade de serem favoritados — o ícone de coração deve aparecer desabilitado.
Antes de começar, reflita sobre estas questões: por que o CartaoProduto não pode gerenciar o estado do favorito internamente? O que aconteceria se cada card guardasse seu próprio estado sem que o pai soubesse? Como o callback aoAlternarFavorito resolve esse problema sem que o CartaoProduto precise conhecer a lógica de negócio?
Não se esqueça de declarar o construtor do CartaoProduto como const. Para isso, todos os seus parâmetros precisam ser compatíveis com const. Reflita sobre qual tipo usar para o callback e se ele é compatível.
O que deve ser entregue: um arquivo chamado g_ex1.dart, onde g é o nome do seu grupo.
Exercício 2
Ciclo de Vida, Keys, BuildContext e Tema
Gerenciar recursos corretamente é tão importante quanto exibi-los
Um widget Flutter que cria controladores, timers ou assinaturas de stream tem uma responsabilidade que vai além da construção visual: ele precisa liberar esses recursos quando deixa de existir. Falhar nisso é criar vazamentos de memória que se acumulam silenciosamente e degradam a experiência do usuário. Além disso, telas com listas dinâmicas têm um comportamento sutil relacionado às Keys que, quando ignorado, produz bugs visuais difíceis de rastrear. Este exercício treina exatamente esses dois aspectos dentro de um cenário real do seu projeto.
Você vai construir uma tela chamada TelaHistoricoPedidos, que exibe o histórico de pedidos de um usuário. A tela deve atender a um conjunto preciso de requisitos, descrito a seguir.
Em relação ao campo de busca, a tela deve ter um TextField controlado por um TextEditingController que filtra os pedidos exibidos em tempo real conforme o usuário digita. O campo deve exibir um ícone de lupa como prefixo e um botão de limpar (Icons.clear) como sufixo, visível apenas quando houver texto digitado. O controlador deve ser criado em initState e obrigatoriamente liberado em dispose. Se o widget for removido da tela enquanto uma operação estiver em andamento (simule isso com um Future.delayed de três segundos em initState, representando um carregamento inicial fictício), o código deve verificar mounted antes de chamar setState.
Em relação à lista de pedidos, cada pedido deve ser representado pela classe ResumoPedido, com os campos id (String), descricao (String), valorTotal (double) e status (String, com os valores possíveis 'pendente', 'confirmado', 'entregue' e 'cancelado'). A lista deve ser construída com ListView.builder, e cada item deve receber obrigatoriamente uma ValueKey construída a partir do id do pedido. Pedidos com status 'cancelado' devem ser exibidos com um visual diferente dos demais — use TextStyle com decoration: TextDecoration.lineThrough no texto da descrição e uma cor de fundo mais suave no card. Inclua pelo menos oito pedidos de exemplo, com diferentes status e valores, de forma que ao filtrar pelo termo "pizza" retenham-se apenas os pedidos cuja descrição contenha essa palavra.
Em relação ao uso do tema, toda cor, tamanho de texto e estilo de botão deve ser obtido via Theme.of(context). Não use cores ou tamanhos de texto escritos diretamente no código. O cabeçalho da AppBar deve usar theme.textTheme.titleLarge e a cor de fundo deve ser theme.colorScheme.primary.
Em relação à contagem de resultados, logo abaixo do campo de busca, exiba uma linha de texto indicando quantos pedidos foram encontrados. Se a lista estiver vazia após a filtragem, exiba um estado vazio com um ícone centralizado e uma mensagem descritiva — não exiba uma lista vazia.
O ponto mais delicado deste exercício é a simulação do carregamento inicial com Future.delayed. Pense em como iniciar esse Future em initState, o que acontece se o usuário sair da tela antes de os três segundos passarem, e como garantir que o setState posterior ao await só seja executado se o widget ainda estiver montado.
Por que as ValueKeys importam aqui? Se você embaralhar a ordem dos pedidos — por exemplo, ao ordenar por valor — e os itens forem StatefulWidget, o Flutter pode associar o estado antigo ao widget errado sem as keys. Mesmo que neste exercício os itens sejam StatelessWidget, pratique o hábito de sempre usar ValueKey em listas cujos itens podem ser reordenados, adicionados ou removidos.
O que deve ser entregue: um arquivo chamado g_ex2.dart, onde g é o nome do seu grupo.
Exercício 3
InheritedWidget, Decomposição de Widgets e Tema Centralizado
O desafio que une a arquitetura da árvore ao design da interface
Um aplicativo bem projetado nunca passa dados de tela em tela por parâmetro quando esses dados são globais — como as informações da sessão do usuário logado. Ele também nunca repete definições visuais espalhadas pelo código quando pode centralizá-las em um único lugar. E, mais importante, ele nunca constrói métodos build com centenas de linhas quando pode decompor a interface em widgets coesos e reutilizáveis. Este exercício exige que você aplique os três princípios simultaneamente, construindo a fundação de uma tela real do Projeto Integrador do professor.
Você vai implementar um conjunto de componentes interligados que formam a estrutura da tela principal autenticada do aplicativo. O exercício é dividido em três partes que devem funcionar em conjunto.
Parte 1: O InheritedWidget de sessão. Crie uma classe chamada SessaoUsuario que estende InheritedWidget. Ela deve armazenar os dados do usuário logado: nomeCompleto (String), email (String) e nivelAcesso (String, com os valores possíveis 'cliente' e 'admin'). Implemente o método estático of(BuildContext context) para que qualquer descendente possa acessar esses dados com SessaoUsuario.of(context). Implemente updateShouldNotify de forma que os descendentes só sejam reconstruídos quando os dados da sessão realmente mudarem — e não em qualquer reconstrução do pai. Crie também uma classe AppTheme com um getter estático tema que retorna um ThemeData completo usando useMaterial3: true, com uma paleta de cores coerente definida via ColorScheme.fromSeed, estilos personalizados para AppBar, ElevatedButton, Card e TextTheme.
Parte 2: A decomposição da tela. A tela principal autenticada (TelaPrincipalAutenticada) deve ser um StatefulWidget que gerencia o índice da aba ativa em seu estado. Ela deve usar um Scaffold com BottomNavigationBar de três abas: “Início”, “Pedidos” e “Perfil”. Cada aba deve exibir um widget distinto, extraído em sua própria classe separada. O widget da aba “Início” (PainelInicio) deve ser um StatelessWidget que lê a sessão via SessaoUsuario.of(context) e exibe uma saudação personalizada com o nome do usuário, além de uma lista estática de três cards de resumo com ícones e contadores fictícios (por exemplo: “Pedidos ativos: 2”, “Favoritos: 5”, “Notificações: 1”). O widget da aba “Pedidos” (PainelPedidos) deve ser um StatelessWidget que exibe uma mensagem informando que o histórico de pedidos está disponível, junto com um ElevatedButton cujo estilo venha exclusivamente do tema. O widget da aba “Perfil” (PainelPerfil) deve ser um StatelessWidget que lê a sessão via SessaoUsuario.of(context) e exibe o nome completo, o e-mail e o nível de acesso do usuário — este último como um Chip com cor de fundo diferente para 'admin' e para 'cliente'.
Parte 3: A integração e o comportamento condicional. O widget PainelInicio deve verificar, via SessaoUsuario.of(context).nivelAcesso, se o usuário tem nível 'admin'. Se sim, deve exibir um card adicional com o texto “Painel administrativo disponível” e um ícone de destaque. Se não, esse card não deve aparecer. Implemente também o dispose da TelaPrincipalAutenticada: crie um TextEditingController fictício em seu initState — apenas para demonstrar que você sabe onde criá-lo e onde liberá-lo — e libere-o corretamente no dispose. Por fim, a função main deve criar o MaterialApp com o tema centralizado (AppTheme.tema), envolver a TelaPrincipalAutenticada com o SessaoUsuario fornecendo um usuário de nível 'admin', e executar o aplicativo.
O aspecto mais delicado desta implementação é a posição do SessaoUsuario na árvore. Ele deve ser colocado acima da TelaPrincipalAutenticada na árvore de widgets para que todos os painéis possam acessá-lo via of(context). Se você colocá-lo no lugar errado, o of(context) retornará null e o aplicativo lançará uma exceção em tempo de execução. Pense cuidadosamente em qual widget é o responsável por envolver a tela com o InheritedWidget.
A decomposição em widgets separados não é apenas uma questão estética: ela tem implicações diretas sobre o desempenho. Quando o índice da aba ativa muda e a TelaPrincipalAutenticada chama setState, apenas os widgets que dependem de dados modificados precisam reconstruir. Se toda a interface estivesse em um único build, tudo seria reconstruído. Reflita sobre quais widgets serão reconstruídos na sua implementação quando o usuário troca de aba.
O updateShouldNotify do SessaoUsuario merece atenção especial. Implemente-o de forma a comparar cada campo individualmente. Se você retornar true sempre, todos os descendentes que leem a sessão serão reconstruídos mesmo quando os dados não mudaram — o que derrota o propósito do InheritedWidget.
O que deve ser entregue: um arquivo chamado g_ex3.dart, onde g é o nome do seu grupo.
Se você concluiu os três exercícios com sucesso, considere este desafio adicional para o exercício 3: como você tornaria o SessaoUsuario mutável — ou seja, capaz de atualizar os dados do usuário em tempo de execução (por exemplo, após o usuário editar o perfil) e notificar automaticamente os widgets que dependem dessas informações? O InheritedWidget por si só é imutável; que padrão você combinaria a ele para obter esse comportamento? Pesquise sobre InheritedNotifier como ponto de partida.
Os exercícios a seguir têm um propósito diferente dos três anteriores. Enquanto nos primeiros você escreveu código Flutter, nos próximos seis você vai transitar entre dois mundos complementares: o mundo visual das telas — o que o usuário enxerga — e o mundo estrutural das árvores de widgets — o que o Flutter executa internamente. Nos exercícios 4, 5 e 6, você partirá de telas descritas em detalhes e deverá derivar a árvore de widgets correspondente, justificando cada decisão de hierarquia. Nos exercícios 7, 8 e 9, o caminho se inverte: você receberá uma árvore de widgets já definida e deverá implementá-la em Flutter, produzindo uma interface funcional e esteticamente coerente. Dominar essa transição nos dois sentidos é o que separa quem reproduz código de quem projeta interfaces com intenção.
Parte II — Da Tela à Árvore de Widgets
Saber ler uma interface e traduzir o que se vê em uma hierarquia de widgets é uma habilidade que todo desenvolvedor Flutter precisa desenvolver antes de se tornar produtivo de verdade. Quando você abre um aplicativo e contempla uma tela, existe por trás de cada pixel uma árvore cuidadosamente organizada. O exercício de recriar essa árvore — mesmo sem acesso ao código-fonte — é exatamente o que você praticará aqui. Para isso, você precisará pensar como o Flutter pensa: identificar os widgets contêineres, os widgets de layout, os widgets de apresentação e, sobretudo, as relações de pai e filho entre eles. Nas três telas a seguir, todas elas pertencentes ao aplicativo de pedidos do Projeto Integrador do professor, a sua tarefa é construir a árvore de widgets que daria origem a cada interface.
Exercício 4
Tela de Detalhes do Produto: do visual à estrutura
Aprender a enxergar widgets onde outros enxergam pixels
A primeira habilidade de um desenvolvedor Flutter maduro não é digitar código rápido — é olhar para uma tela e imediatamente enxergar a estrutura que a sustenta. Toda tela tem uma hierarquia, e essa hierarquia é a árvore de widgets. Este exercício treina exatamente esse olhar: dado um protótipo de tela bem definido, você deve derivar a estrutura interna que o Flutter usaria para renderizá-la, justificando cada decisão de hierarquia com base no que você estudou no material deste módulo.
A tela que você deve analisar é a de detalhes de um produto do aplicativo de pedidos. Observe a representação visual abaixo com atenção, identificando cada região da tela e os elementos que a compõem.
Clique aqui para abrir a imagem em outra aba.
Observe cada região com cuidado antes de começar a montar a árvore. A tela inteira é contida por um Scaffold. A barra superior é um AppBar com um botão de retorno implícito (que o Flutter adiciona automaticamente quando existe rota anterior) e uma ação à direita representada por um ícone de coração. O corpo da tela precisa ser rolável, pois o conteúdo pode ultrapassar a altura da tela em dispositivos menores — mas o botão “Adicionar ao pedido” deve permanecer sempre visível na parte inferior, independentemente da posição de rolagem. Isso já é uma pista importante sobre a estrutura que você deve adotar.
A imagem ocupa toda a largura disponível sem padding lateral. Abaixo dela, há uma seção com padding horizontal contendo, organizados verticalmente: o nome do produto com tipografia de destaque, uma linha com as categorias representadas por chips, o preço em uma tipografia maior, e a linha de avaliação composta por cinco ícones de estrela seguidos de um texto com a nota e o total de avaliações. Na sequência, dois blocos de texto separados por divisores — a descrição e os ingredientes. O botão fica fixo na parte inferior da tela, fora da área rolável.
A sua tarefa é criar a árvore de widgets correspondente a essa tela, representada como um diagrama Mermaid com graph TD. Para cada nó da árvore, indique o tipo do widget e, quando relevante, os parâmetros mais significativos. Além do diagrama, escreva um parágrafo justificando por que o corpo da tela exige uma estrutura que separa o conteúdo rolável do botão inferior — ou seja, por que ele não pode ser implementado como uma única Column contendo todos os elementos. Justifique também se TelaProdutoDetalhe deve ser StatelessWidget ou StatefulWidget, levando em conta que o ícone de coração no AppBar alterna entre favorito e não-favorito quando tocado.
Pense em onde exatamente termina o conteúdo rolável e onde começa o botão fixo. O Scaffold oferece as propriedades bottomNavigationBar e persistentFooterButtons que mantêm widgets sempre visíveis na parte inferior, fora da rolagem. Mas há também a abordagem de usar uma Column no corpo com um Expanded envolvendo o SingleChildScrollView e o botão fora dele. Ambas funcionam — qual delas você considera mais semântica para este caso?
👉 Dica: use os seguintes widgets: AppBar, BackButton, IconButton, Column, Expanded, SingleChildScrollView, Image.network, Padding, Text, SizedBox, Wrap, Chip, Row, Icon, Divider e ElevatedButton.
A linha de avaliação com estrelas é um Row com widgets de ícone e texto. Mas as estrelas têm estados diferentes — quatro preenchidas e uma vazia. Como você representaria isso na árvore sem recorrer a um único Text com caracteres especiais? Há pelo menos duas abordagens válidas: uma Row com cinco Icon widgets individuais, ou um único widget personalizado AvaliacaoEstrelas que encapsula essa lógica. Pense em qual das duas aparece na árvore que você vai desenhar e como representar a que não aparece.
O que deve ser entregue: um arquivo g_ex4.md com o diagrama Mermaid e o parágrafo de justificativa, onde g é o nome do seu grupo. Nenhum código Dart é necessário neste exercício.
Exercício 5
Tela de Acompanhamento de Pedido: mapeando hierarquias complexas
Quando a tela conta uma história com vários capítulos de layout
Algumas telas são formadas por seções bem definidas, cada uma com sua própria estrutura interna. Mapear a árvore de uma tela assim exige que você identifique os grandes blocos independentes da interface antes de se aprofundar nos detalhes de cada um. Esse processo de análise de cima para baixo é exatamente o que este exercício treina, em uma tela com maior densidade de widgets e maior variedade de composições.
A tela que você deve analisar é a de acompanhamento de pedido, exibida após o usuário confirmar um pedido e querer saber em que estado ele se encontra.
Clique aqui para abrir a imagem em outra aba.
A tela é composta por quatro blocos de card empilhados verticalmente dentro de um conteúdo rolável. O primeiro card exibe o status atual do pedido: um ícone ao lado do nome do status, um indicador de progresso linear logo abaixo, e uma linha de etapas representando as quatro fases possíveis do pedido — “Confirmado”, “Em preparo”, “Saiu para entrega” e “Entregue”. As fases concluídas aparecem com preenchimento sólido e as pendentes com círculo vazio. O segundo card lista os itens do pedido e o resumo financeiro, com divisores separando a lista dos totais. O terceiro exibe o endereço de entrega com um ícone de localização. O quarto exibe as informações do entregador com uma foto circular, o nome, o cargo e um botão para ligar.
A sua tarefa é criar a árvore completa de widgets dessa tela como um diagrama Mermaid, expandindo cada card até o nível dos widgets folha. Para a linha de etapas, você deve pensar em como representar os círculos conectados horizontalmente: uma Row com quatro Columns, onde cada coluna contém um Container circular e um Text abaixo, é o ponto de partida — mas há o problema da linha que conecta os círculos. Pense em como resolvê-lo e represente a solução escolhida na árvore. Além do diagrama, escreva um parágrafo respondendo à seguinte questão: faz sentido extrair cada card em um widget próprio — como CardStatusPedido, CardItensPedido, CardEnderecoEntrega e CardEntregador? Que critérios de coesão e reutilização você aplicaria para tomar essa decisão?
A linha de etapas é o elemento mais desafiador desta tela. Se você usar apenas um Row com quatro Columns, não haverá nenhuma linha horizontal conectando os círculos. Há pelo menos duas abordagens: usar um Stack com um Divider ou Container fino posicionado atrás dos círculos na altura de seus centros, ou alternar na Row entre os círculos e widgets Expanded com linhas horizontais entre eles. Ambas são válidas — o importante é que a solução que você escolher apareça claramente no diagrama.
Esta tela, como está descrita, é puramente de exibição — não há interação do usuário. Isso tornaria todos os widgets candidatos a StatelessWidget. Mas pense no cenário real: o status do pedido muda ao longo do tempo, provavelmente por meio de atualização periódica. Se você precisasse adaptar esta tela para consultar o servidor a cada 30 segundos e atualizar o status automaticamente, qual widget precisaria se tornar StatefulWidget e quais campos de estado ele precisaria manter?
👉 Dica: use os seguintes widgets: AppBar, SingleChildScrollView, Column, Card, Padding, Row, Icon, SizedBox, Text, LinearProgressIndicator, Expanded, ListTile, Divider, CircleAvatar, ElevatedButton.
O que deve ser entregue: um arquivo g_ex5.md com o diagrama Mermaid e o parágrafo de justificativa, onde g é o nome do seu grupo.
Exercício 6
Tela Inicial do Aplicativo: decomposição em widgets próprios
O mapa completo de uma tela que o usuário enxerga como simples
Para o usuário, a tela inicial é apenas “a tela inicial”. Para o desenvolvedor Flutter, ela é uma composição cuidadosa de dezenas de widgets organizados em múltiplos níveis de hierarquia, com listas rolando em direções diferentes e dados provenientes de seções distintas da interface. Mapear essa tela exige não apenas identificar os widgets, mas também tomar decisões arquiteturais sobre quais partes da árvore merecem ser extraídas como widgets independentes e justificar por quê.
A tela que você deve analisar é a tela inicial do aplicativo de pedidos, exibida imediatamente após o usuário autenticar-se.
Clique aqui para abrir a imagem em outra aba.
Esta é a tela mais complexa dos exercícios desta seção. Ela apresenta múltiplas seções com comportamentos de rolagem distintos: a lista de categorias rola horizontalmente de forma independente, o PageView dos banners tem lógica própria de paginação com indicadores de ponto, os cards de produtos populares também rolam horizontalmente, e a lista de estabelecimentos próximos é uma lista vertical não rolável dentro do conteúdo geral. Toda a tela rola verticalmente como conjunto, usando um SingleChildScrollView ou CustomScrollView como raiz do corpo.
A sua tarefa tem três partes que devem ser entregues juntas. Na primeira, crie o diagrama Mermaid da árvore completa. Para os widgets que você decidir extrair como classes independentes — como CartaoCategoria, BannerCarrossel, CartaoProdutoPopular ou CardEstabelecimento —, represente-os como nós folha no diagrama, sem expandir sua estrutura interna. Na segunda parte, construa uma tabela Markdown com três colunas — nome do widget extraído, tipo (StatelessWidget ou StatefulWidget) e justificativa — para cada widget que você decidiu fatorar. Na terceira, escreva um parágrafo explicando como o BannerCarrossel precisaria gerenciar seu estado interno para manter o indicador de pontos atualizado enquanto o usuário desliza os banners.
A decisão mais importante neste exercício não é qual widget usar em cada lugar — é onde traçar os limites entre widgets extraídos e widgets inline. Um widget merece ser extraído quando aparece mais de uma vez na tela, quando tem responsabilidade única e bem delimitada, ou quando sua estrutura interna é suficientemente complexa para tornar o método build do pai ilegível. Aplique esses critérios com rigor e seja capaz de justificar cada extração que você fizer.
O FloatingActionButton exibe um badge com a contagem de itens no carrinho. Esse número é um dado global — qualquer tela do aplicativo precisa ter acesso a ele, não apenas a tela inicial. Isso significa que ele não deve ser estado local desta tela. Indique no diagrama, com um comentário no nó correspondente, de onde esse dado viria em uma arquitetura real — sem implementar a solução agora, apenas sinalizando a origem correta.
👉 Dica: use os seguintes widgets: AppBar, Badge, FloatingActionButton, SingleChildScrollView, Column, Text, SizedBox, ListView, PageView, ClipRRect e, Row.
O que deve ser entregue: um arquivo g_ex6.md com o diagrama Mermaid, a tabela de widgets extraídos e o parágrafo sobre o BannerCarrossel, onde g é o nome do seu grupo.
Parte III — Da Árvore de Widgets ao Código Flutter
Nos exercícios anteriores, você praticou o caminho da tela para a árvore. Agora você percorrerá o caminho contrário: receberá uma árvore de widgets já definida e deverá implementá-la em código Flutter. Esse é o processo que ocorre no dia a dia de desenvolvimento: o designer entrega um protótipo ou especificação estrutural, e o desenvolvedor lê a hierarquia e a traduz para código. A árvore que você receberá em cada exercício já tomou as decisões de layout e hierarquia — o seu trabalho é transformá-la em um arquivo Dart funcional, respeitando exatamente a estrutura proposta e preenchendo os dados de exemplo com conteúdo coerente com o Projeto Integrador.
Exercício 7
Implementando a tela de autenticação a partir da árvore
Ler uma árvore e escrever o código que a realiza
A tradução de uma árvore de widgets para código Flutter é uma habilidade direta, mas que exige disciplina: cada nó da árvore vira uma chamada de construtor ou um widget filho, na ordem exata descrita. Este exercício parte de uma tela simples para que você possa focar na precisão da tradução, sem se preocupar com complexidade excessiva — mas atenção: simples não significa trivial.
A árvore a seguir descreve a tela de autenticação (login) do aplicativo de pedidos.
A partir desta árvore, implemente o arquivo Dart completo da tela de login. A tela deve se chamar TelaLogin e ser um StatefulWidget, pois ela precisa gerenciar dois estados internos: a visibilidade da senha — controlada pelo botão de alternância no sufixo do campo — e o conteúdo dos dois campos de texto, gerenciados por dois TextEditingControllers. Crie ambos os controladores em initState e libere-os obrigatoriamente em dispose. A imagem do logo não precisa existir como arquivo real — use um Container com um ícone centralizado como substituto, mas mantenha a hierarquia da árvore exatamente como especificada: o Center envolvendo o Image.asset (ou seu substituto) deve continuar presente. O botão “Entrar” deve imprimir no console o e-mail e a senha digitados quando pressionado. O botão “Cadastre-se” e o link “Esqueci minha senha” podem ter ações vazias por enquanto.
O campo de senha tem um comportamento interativo: o ícone à direita alterna entre o olho aberto e o olho fechado, e ao ser tocado muda o obscureText do campo. Isso significa que o estado _senhaVisivel precisa ser lido tanto na construção do TextField — para definir obscureText — quanto na construção do suffixIcon — para escolher qual ícone exibir. Certifique-se de que ambos os usos referenciem a mesma variável de estado e que setState seja chamado quando o usuário toca no ícone.
O crossAxisAlignment: stretch na Column é o que faz o ElevatedButton ocupar toda a largura disponível sem envolvê-lo em um SizedBox com width: double.infinity. Entenda por que isso funciona: a Column estica seus filhos na direção cruzada (horizontal), e o ElevatedButton, por padrão, respeita as restrições de largura que recebe do pai. Isso é uma consequência direta do sistema de restrições do Flutter — o pai impõe, o filho obedece.
O que deve ser entregue: um arquivo g_ex7.dart com a implementação completa da tela de login, onde g é o nome do seu grupo.
Exercício 8
Implementando o catálogo de produtos com filtragem por categoria
Uma árvore com estado, interatividade e dois eixos de rolagem
Quando uma árvore de widgets inclui interatividade — como seleção de categorias que filtra uma grade de produtos —, a implementação exige que você identifique quais estados precisam existir, onde cada um deles deve viver e como eles se propagam para os widgets descendentes. Este exercício apresenta uma árvore com dois eixos de rolagem independentes e um StatefulWidget que coordena a lógica de filtragem.
A árvore a seguir descreve a tela de catálogo de produtos com filtragem por categoria.
graph TD
A["TelaCatalogo<br/>(StatefulWidget)<br/>estado: _categoriaSelecionada"] --> B["Scaffold"]
B --> BA["AppBar<br/>(title: 'Cardapio')<br/>(actions: IconButton filtro)"]
B --> C["Column<br/>(crossAxisAlignment: stretch)"]
C --> D["Container<br/>(height: 56)<br/>(sombra inferior)"]
D --> E["ListView.builder<br/>(scrollDirection: horizontal)<br/>(itemCount: categorias.length)"]
E --> F["[xN] GestureDetector<br/>(onTap: selecionar categoria)"]
F --> G["AnimatedContainer<br/>(decoracao muda se selecionado)<br/>(duration: 200ms)<br/>(borderRadius: 20)"]
G --> H["Padding<br/>(h: 16, v: 8)"]
H --> I["Text<br/>(categoria.nome)<br/>(cor muda se selecionado)"]
C --> J["Expanded"]
J --> K["GridView.builder<br/>(crossAxisCount: 2)<br/>(padding: 12)"]
K --> L["[xN] CartaoProduto<br/>(StatelessWidget)<br/>(produto, onAdicionarAoCarrinho)"]
L --> M["Card<br/>(clipBehavior: antiAlias)"]
M --> N["Column"]
N --> O["Stack"]
O --> O1["Image.network<br/>(produto.imagemUrl)<br/>(height: 120, fit: cover)"]
O --> O2["Positioned<br/>(top: 8, right: 8)"]
O2 --> O3["CircleAvatar<br/>(radius: 16, bg: white)"]
O3 --> O4["Icon<br/>(favorite_border)<br/>(size: 18, color: primary)"]
N --> P["Padding<br/>(all: 12)"]
P --> Q["Column<br/>(crossAxisAlignment: start)"]
Q --> R["Text<br/>(produto.nome)<br/>(titleSmall, bold, maxLines: 2)"]
Q --> S["SizedBox<br/>(height: 4)"]
Q --> T["Row<br/>(mainAxisAlignment: spaceBetween)"]
T --> U["Row"]
U --> U1["Icon<br/>(star, size: 14, amber)"]
U --> U2["Text<br/>(produto.avaliacao)<br/>(labelSmall)"]
T --> V["Text<br/>(produto.preco formatado)<br/>(titleSmall, primary)"]
Q --> W["SizedBox<br/>(height: 8)"]
Q --> X["SizedBox<br/>(width: double.infinity)"]
X --> Y["FilledButton.tonal<br/>'Adicionar'<br/>(onPressed: onAdicionarAoCarrinho)"]
A partir desta árvore, implemente a tela completa. Você precisará criar a classe Produto com os campos id (String), nome (String), categoria (String), preco (double), avaliacao (double) e imagemUrl (String). Crie ao menos doze produtos de exemplo, distribuídos entre as categorias “Pizza”, “Hamburguer”, “Sushi” e “Bebidas”. O widget CartaoProduto deve ser implementado como StatelessWidget conforme especificado na árvore, recebendo o produto e o callback por parâmetro. A TelaCatalogo deve ser StatefulWidget, mantendo como estado a categoria selecionada. A lista filtrada não deve ser armazenada como estado — ela deve ser um getter que deriva seu valor de _categoriaSelecionada e da lista completa de produtos a cada reconstrução.
O comportamento esperado é o seguinte: quando o usuário toca em uma categoria, o AnimatedContainer anima suavemente para uma decoração com cor de fundo primária e texto branco, enquanto os demais retornam ao estado padrão. A grade de produtos exibe apenas os produtos da categoria selecionada. Uma categoria especial chamada “Todos” deve exibir todos os produtos independentemente da categoria. O botão “Adicionar” de cada card deve imprimir o nome do produto no console por enquanto.
O AnimatedContainer interpola suavemente entre os valores de sua decoração quando eles mudam de uma reconstrução para a seguinte. Para que isso funcione, você precisa chamar setState ao selecionar uma categoria — o que desencadeia uma reconstrução do widget, e o AnimatedContainer detecta a mudança na decoração e anima automaticamente entre os dois estados. Defina uma duration razoável, como Duration(milliseconds: 200), para que a animação seja perceptível sem ser lenta.
A filtragem dos produtos deve acontecer fora do método build, em um getter privado chamado _produtosFiltrados, que retorna a lista filtrada com base em _categoriaSelecionada. Isso mantém o build limpo e torna a lógica de filtragem isolada e testável. Evite calcular a lista filtrada diretamente no parâmetro itemCount do GridView.builder ou dentro de uma chamada where aninhada na construção da grade.
O que deve ser entregue: um arquivo g_ex8.dart com a implementação completa da tela de catálogo, onde g é o nome do seu grupo.
Exercício 9
Implementando a tela de finalização de pedido
Uma árvore com interatividade complexa, estado interdependente e widgets compostos
A tela de finalização de pedido é uma das mais ricas em interatividade de um aplicativo de delivery: o usuário pode remover itens deslizando, alterar quantidades com botões de incremento e decremento, escolher o método de pagamento e confirmar o pedido. Implementar essa tela a partir de uma árvore exige que você gerencie múltiplos estados interdependentes e mantenha a consistência do valor total, que precisa se atualizar automaticamente a cada mudança de quantidade ou remoção de item.
A árvore a seguir descreve a tela de finalização de pedido. Os widgets marcados como StatefulWidget gerenciam estado próprio; os marcados como StatelessWidget recebem dados por parâmetro e expõem callbacks para o pai.
graph TD
A["TelaFinalizarPedido<br/>(StatefulWidget)<br/>estado: _itens, _metodoPagamento"] --> B["Scaffold"]
B --> BA["AppBar<br/>(title: 'Finalizar Pedido')"]
B --> C["Column"]
C --> D["Expanded"]
D --> E["ListView<br/>(shrinkWrap: false)"]
E --> F["SecaoPedido<br/>(StatelessWidget)<br/>titulo: 'Itens do pedido', child: lista"]
F --> G["[xN] Dismissible<br/>(key: ValueKey(item.id))<br/>(direction: endToStart)<br/>(background: vermelho + icone lixeira)"]
G --> H["CartItemTile<br/>(StatelessWidget)<br/>(item, onAlterarQuantidade)"]
H --> I["Card<br/>(margin: h 12, v 4)"]
I --> J["Padding"]
J --> K["Row"]
K --> L["ClipRRect<br/>(borderRadius: 8)"]
L --> L1["Image.network<br/>(item.imagemUrl)<br/>(80x80, fit: cover)"]
K --> M["SizedBox<br/>(width: 12)"]
K --> N["Expanded"]
N --> O["Column<br/>(crossAxisAlignment: start)"]
O --> O1["Text<br/>(item.nome)<br/>(titleSmall, bold)"]
O --> O2["Text<br/>(item.observacao)<br/>(bodySmall, muted)"]
O --> O3["SizedBox<br/>(height: 8)"]
O --> O4["Row<br/>(mainAxisSize: min)"]
O4 --> O5["IconButton<br/>(remove_circle_outline)<br/>(onPressed: decrementar)"]
O4 --> O6["Text<br/>(item.quantidade)"]
O4 --> O7["IconButton<br/>(add_circle_outline)<br/>(onPressed: incrementar)"]
K --> P["Text<br/>(item.subtotal formatado)<br/>(titleSmall, primary, bold)"]
E --> Q["SecaoPedido<br/>titulo: 'Endereco de entrega'"]
Q --> R["Card<br/>(margin: 12)"]
R --> S["ListTile"]
S --> S1["leading: Icon<br/>(location_on_outlined)"]
S --> S2["title: Text<br/>(endereco.logradouro)"]
S --> S3["subtitle: Text<br/>(endereco.complemento)"]
S --> S4["trailing: TextButton<br/>'Alterar'"]
E --> T["SecaoPedido<br/>titulo: 'Forma de pagamento'"]
T --> U["Column"]
U --> V1["RadioListTile<br/>(cartaoCredito)<br/>(title: 'Cartao de credito')"]
U --> V2["RadioListTile<br/>(cartaoDebito)<br/>(title: 'Cartao de debito')"]
U --> V3["RadioListTile<br/>(pix)<br/>(title: 'Pix')"]
C --> W["Container<br/>(borda superior + sombra)<br/>(padding: 16)"]
W --> X["Column<br/>(mainAxisSize: min)"]
X --> X1["Row — Subtotal + valor"]
X --> X2["Row — Taxa de entrega + valor"]
X --> X3["Divider"]
X --> X4["Row — Total (bold) + valor (bold, primary)"]
X --> X5["SizedBox<br/>(height: 12)"]
X --> X6["SizedBox<br/>(width: double.infinity)"]
X6 --> X7["ElevatedButton<br/>'Confirmar pedido — R$ total'<br/>(onPressed: _confirmarPedido)"]
A partir desta árvore, implemente a tela completa. Você precisará definir as seguintes classes de modelo: ItemCarrinho com os campos id, nome, observacao, imagemUrl, preco e quantidade, além de um getter subtotal que retorna preco * quantidade; Endereco com os campos logradouro e complemento; e enum MetodoPagamento com os valores cartaoCredito, cartaoDebito e pix. Inicialize a TelaFinalizarPedido com pelo menos três itens de exemplo no carrinho.
O widget SecaoPedido é um StatelessWidget simples que recebe um titulo (String) e um child (Widget), e os exibe com um Padding de 12 pixels ao redor, com o título no estilo titleMedium acima do conteúdo. Extraia-o como classe separada no mesmo arquivo.
O comportamento esperado para cada interação é o seguinte. Quando o usuário desliza um CartItemTile para a esquerda, o item é removido da lista via setState e o total é recalculado automaticamente. Quando o usuário toca nos botões de incremento ou decremento, a quantidade do item é atualizada — nunca abaixo de 1 — e o total é recalculado. Quando o usuário seleciona um método de pagamento via RadioListTile, o estado _metodoPagamento é atualizado. O botão “Confirmar pedido” exibe o total atual em seu próprio label e, ao ser pressionado, imprime no console um resumo com os itens, o método de pagamento escolhido e o valor total.
O total exibido na seção inferior e no label do botão deve ser um getter calculado no State da TelaFinalizarPedido, não uma variável armazenada em campo. Um getter garante que o valor seja sempre derivado dos dados atuais dos itens, sem risco de ficar desincronizado após remoções ou alterações de quantidade. Toda vez que build for chamado — o que acontece após cada setState —, o getter será invocado e o total exibido estará sempre correto.
O Dismissible exige uma key única para cada item — use ValueKey(item.id). Sem a key, o Flutter não consegue identificar corretamente qual item foi deslizado quando a lista é reconstruída após a remoção, o que pode causar comportamento indefinido, incluindo a remoção visual do item errado. Lembre-se também de implementar o callback onDismissed para de fato remover o item da lista via setState — o Dismissible só cuida da animação de saída; a remoção dos dados é responsabilidade do seu código.
O que deve ser entregue: um arquivo g_ex03.dart com a implementação completa da tela de finalização de pedido, onde g é o nome do seu grupo.