graph TD
A["Scaffold"] --> B["AppBar<br/>(título: 'Meu Perfil')"]
A --> C["Column<br/>(corpo da tela)"]
C --> D["CircleAvatar<br/>(foto)"]
C --> E["Text<br/>(nome em negrito)"]
C --> F["Text<br/>(biografia)"]
style A fill:#cfe2ff,stroke:#0d6efd
style B fill:#d1ecf1,stroke:#17a2b8
style C fill:#d1ecf1,stroke:#17a2b8
style D fill:#d4edda,stroke:#28a745
style E fill:#d4edda,stroke:#28a745
style F fill:#d4edda,stroke:#28a745
Módulo 03 — Widgets Fundamentais e Árvore de Widgets
Chegamos ao momento em que o seu aplicativo ganha rosto. Nos dois módulos anteriores você preparou o terreno: configurou o ambiente, aprendeu Dart com profundidade e escreveu os primeiros modelos de domínio do Projeto Integrador. Agora é hora de transformar esses dados em algo que pode ser visto, tocado e usado. Neste módulo, você vai entender como o Flutter organiza e exibe qualquer elemento visual — do mais simples texto até uma tela completa com barra de navegação, botões e imagens. Tudo isso gira em torno de um único conceito central: o widget. Estude este material com calma, experimente cada exemplo no seu computador e, ao chegar à aula, você já estará pronto para construir a tela inicial real do seu projeto.
No Flutter, Tudo é um Widget
Se você perguntar a qualquer desenvolvedor Flutter experiente qual é a afirmação mais repetida dentro da comunidade, a resposta será quase unânime: “No Flutter, tudo é um widget.” Essa frase parece simples — talvez até óbvia —, mas ela esconde uma consequência profunda que vai moldar a forma como você pensa sobre desenvolvimento de interfaces pelo resto da disciplina e da sua carreira.
Em outros frameworks de interface, você costuma ter conceitos separados para cada tipo de elemento: um componente para um botão, um layout para organizar elementos, um estilo para definir aparência, uma animação para dar movimento. No Flutter, todos esses conceitos são representados por widgets. Um botão é um widget. Uma linha que organiza outros elementos lado a lado é um widget. A margem ao redor de um texto é um widget. O tema de cores da aplicação inteira é um widget. Até a própria aplicação é um widget.
Isso significa que quando você aprende a trabalhar com widgets, você aprende a trabalhar com tudo no Flutter. Não há exceções.
Para que essa filosofia faça sentido na prática, você precisa entender o que, tecnicamente, um widget é. Em Dart, um widget é simplesmente uma classe que estende Widget — e, na grande maioria dos casos, você vai trabalhar com subclasses mais específicas como StatelessWidget ou StatefulWidget. O que distingue um widget de uma classe Dart qualquer é um único método obrigatório chamado build. Esse método recebe um BuildContext e retorna outro widget — que por sua vez pode conter outros widgets dentro dele. É essa capacidade de widgets conterem widgets que cria toda a riqueza visual do Flutter.
A Árvore de Widgets: A Estrutura Que Organiza Tudo
Como o Flutter organiza os elementos da sua tela
Quando um aplicativo Flutter é executado, o framework constrói uma estrutura em árvore com todos os widgets da tela. Cada widget é um nó dessa árvore. O widget raiz (a própria aplicação) está no topo, e os widgets mais simples — textos, ícones, imagens — ficam nas folhas, na base da árvore. Essa estrutura é chamada de árvore de widgets (widget tree).
A metáfora da árvore não é apenas visual: ela descreve com precisão como os dados fluem no Flutter. Um widget pai passa informações para seus filhos por meio de parâmetros. Um widget filho não pode “falar” diretamente com outro widget irmão — toda comunicação desce pela árvore a partir de um ancestral comum. Essa disciplina na comunicação é o que torna o código Flutter previsível e fácil de entender.
Imagine a seguinte situação: você está construindo uma tela de perfil de usuário. Ela tem uma barra de navegação no topo com o título “Meu Perfil”, uma foto circular no centro, abaixo dela o nome do usuário em negrito, e logo abaixo uma linha de biografia. Como isso se organiza em árvore?
O Scaffold é o widget raiz da tela. Ele contém um AppBar (a barra superior) e uma Column (que organiza elementos verticalmente). Dentro da Column, há três widgets filhos: o CircleAvatar com a foto, e dois widgets Text para o nome e a biografia. Cada widget sabe exatamente o que fazer com o espaço que seu pai lhe concede.
Três Árvores Internas: O que Acontece por Baixo
Um detalhe importante sobre o funcionamento interno do Flutter
Aqui vou revelar algo que a maioria dos tutoriais ignora, mas que vai fazer você entender comportamentos do Flutter que parecem misteriosos à primeira vista. Por baixo dos panos, o Flutter não mantém apenas uma árvore — ele mantém três árvores simultâneas.
A primeira é a Widget Tree, que é a que você escreve. Ela é imutável: cada vez que o estado muda, o Flutter constrói uma nova árvore de widgets. A segunda é a Element Tree, que é o “cérebro” do Flutter. Os elementos são objetos mutáveis que vivem enquanto o widget correspondente existe na tela. Eles são responsáveis por conectar widgets com sua renderização. A terceira é a Render Tree, que representa os objetos que realmente calculam tamanho, posição e aparecem desenhados na tela.
Você não vai manipular a Element Tree nem a Render Tree diretamente. Mas saber que elas existem explica por que o Flutter pode ser tão eficiente: quando o estado muda, o Flutter não joga fora tudo e reconstrói do zero. Ele compara a nova Widget Tree com os elementos existentes na Element Tree e atualiza apenas o que mudou. Esse processo é chamado de reconciliação.
graph LR
subgraph WT["Widget Tree (você escreve)"]
W1["Column"] --> W2["Text"]
W1 --> W3["Icon"]
end
subgraph ET["Element Tree (gerenciada pelo Flutter)"]
E1["ColumnElement"] --> E2["TextElement"]
E1 --> E3["IconElement"]
end
subgraph RT["Render Tree (desenhada na tela)"]
R1["RenderFlex"] --> R2["RenderParagraph"]
R1 --> R3["RenderCustomPaint"]
end
WT -->|cria/atualiza| ET
ET -->|gerencia| RT
style WT fill:#cfe2ff,stroke:#0d6efd
style ET fill:#d4edda,stroke:#28a745
style RT fill:#f5c6cb,stroke:#dc3545
StatelessWidget: Widgets Sem Memória
O StatelessWidget é o tipo mais simples de widget que existe no Flutter. Ele é chamado de “sem estado” porque não tem memória: uma vez construído com determinados parâmetros, ele sempre produz exatamente o mesmo resultado visual para os mesmos parâmetros. Ele não pode mudar sua aparência por conta própria — para que ele mude, alguém de fora precisa reconstruí-lo com parâmetros diferentes.
Pense em um cartão de visitas impresso. Uma vez impresso, ele não muda. Se você quiser um cartão diferente, precisa imprimir outro. O StatelessWidget funciona assim: é imutável, previsível e simples.
Quando você deve usar um StatelessWidget? A resposta é: sempre que o widget não precisar guardar nenhuma informação que mude ao longo do tempo. Textos que sempre exibem o mesmo valor, ícones, imagens, containers decorativos, botões que apenas chamam uma função externa — todos esses são candidatos naturais ao StatelessWidget.
Exemplo — Um cartão de apresentação como StatelessWidget
import 'package:flutter/material.dart';
class CartaoDeApresentacao extends StatelessWidget {
final String nome;
final String cargo;
final String imagemUrl;
// O construtor recebe os dados que o widget precisa para se construir.
// Esses dados nunca vão mudar enquanto este widget existir.
const CartaoDeApresentacao({
super.key,
required this.nome,
required this.cargo,
required this.imagemUrl,
});
@override
Widget build(BuildContext context) {
// O método build recebe o BuildContext e retorna a interface visual.
// Toda vez que esse método for chamado com os mesmos parâmetros,
// o resultado será idêntico.
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
CircleAvatar(
radius: 32,
backgroundImage: NetworkImage(imagemUrl),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nome,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
cargo,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
);
}
}
// Para usar o widget em outra parte da aplicação:
// CartaoDeApresentacao(
// nome: 'Ana Lima',
// cargo: 'Desenvolvedora Flutter',
// imagemUrl: 'https://exemplo.com/foto.jpg',
// )Perceba que CartaoDeApresentacao é declarado com const no construtor. Isso não é coincidência: quando todos os parâmetros de um StatelessWidget são conhecidos em tempo de compilação, você pode usar const, e o Flutter vai criar o widget uma única vez e reutilizá-lo sem reconstruir. Isso melhora o desempenho da aplicação de forma significativa.
Hábito importante: Sempre que possível, declare seus widgets com const. O compilador Dart vai te avisar quando isso é possível — e esse aviso é uma oportunidade de ouro para melhorar o desempenho.
StatefulWidget: Widgets Com Memória
O StatefulWidget é o tipo de widget que pode mudar ao longo do tempo. Ele tem estado — uma “memória” que armazena informações que podem variar enquanto o widget está na tela. Quando esse estado muda, o Flutter reconstrói o widget para refletir a mudança visualmente.
Pense em um placar de jogo. Ele começa zerado, mas cada gol muda os números na tela. O placar precisa “lembrar” a pontuação atual e atualizar a exibição toda vez que ela muda. Isso é exatamente o que o StatefulWidget faz.
A estrutura de um StatefulWidget é um pouco diferente do StatelessWidget e costuma confundir no primeiro contato. Em vez de uma classe, você vai escrever duas: a classe do widget em si (que é imutável, assim como o StatelessWidget) e uma classe de estado separada, chamada de State. Essa separação é intencional: o widget define a configuração, e o State guarda e gerencia os dados que mudam.
Exemplo — Um contador simples com StatefulWidget
import 'package:flutter/material.dart';
// A classe do widget: imutável, recebe parâmetros de configuração.
class ContadorSimples extends StatefulWidget {
final String titulo;
const ContadorSimples({super.key, required this.titulo});
// createState cria o objeto de estado associado a este widget.
// O Flutter chama esse método uma única vez ao inserir o widget na árvore.
@override
State<ContadorSimples> createState() => _ContadorSimplesState();
}
// A classe de estado: mutável, guarda os dados que podem mudar.
// O underscore no início torna a classe privada ao arquivo — uma convenção
// importante do Flutter, pois o State não deve ser acessado de fora.
class _ContadorSimplesState extends State<ContadorSimples> {
// Esta variável é o "estado" do widget — ela pode mudar ao longo do tempo.
int _contagem = 0;
// O método _incrementar muda o estado usando setState.
void _incrementar() {
// setState notifica o Flutter de que o estado mudou e dispara uma
// reconstrução do widget. SEM setState, a variável muda mas a tela não.
setState(() {
_contagem++;
});
}
@override
Widget build(BuildContext context) {
// O Flutter chama build sempre que setState for chamado.
// Note que podemos acessar widget.titulo para ler o parâmetro
// que foi passado para o StatefulWidget.
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.titulo,
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 16),
Text(
'$_contagem',
style: const TextStyle(
fontSize: 64,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _incrementar,
child: const Text('Incrementar'),
),
],
);
}
}Atenção: Nunca chame setState de dentro do método build. Isso causaria um loop infinito: o build chama setState, que dispara um novo build, que chama setState novamente, indefinidamente. O setState deve ser chamado apenas em resposta a eventos — como um toque num botão ou o término de uma operação assíncrona.
O Ciclo de Vida do StatefulWidget
O que acontece desde a criação até a destruição de um widget
Se você vai usar StatefulWidget — e vai, frequentemente —, precisa entender seu ciclo de vida. O ciclo de vida descreve a sequência de métodos que o Flutter chama automaticamente desde o momento em que o widget é inserido na árvore até o momento em que é removido. Usar o método errado no momento errado é uma das fontes mais comuns de bugs no Flutter.
stateDiagram-v2
[*] --> createState: Widget inserido na árvore
createState --> initState: State criado
initState --> didChangeDependencies: Dependências iniciais
didChangeDependencies --> build: Primeira construção
build --> Ativo: Widget na tela
Ativo --> setState: Evento ocorre
setState --> build: Reconstrução
Ativo --> didUpdateWidget: Widget pai rebuilda
didUpdateWidget --> build: Reconstrução com novos parâmetros
Ativo --> deactivate: Widget temporariamente removido
deactivate --> dispose: Widget permanentemente removido
dispose --> [*]
Cada método do ciclo de vida tem um propósito específico. Entender quando cada um é chamado e o que você deve (e não deve) fazer nele é o que separa o código Flutter bem escrito do código problemático.
initState: O Momento do Nascimento
O método initState é chamado uma única vez, logo depois que o objeto State é criado. É o lugar certo para inicializar dados que dependem do contexto do widget: criar controladores de animação, fazer a primeira requisição de dados para preencher a tela, iniciar listeners de streams. Tudo que precisa acontecer uma única vez quando o widget “nasce” vai aqui.
@override
void initState() {
super.initState(); // Sempre chame super.initState() primeiro!
// Exemplo: inicializar um controlador de animação
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
// Exemplo: iniciar o carregamento de dados
_carregarDados();
}didChangeDependencies: Quando as Dependências Mudam
Este método é chamado logo após initState e depois toda vez que um InheritedWidget do qual este widget depende for atualizado. Se você acessa o Theme.of(context) ou MediaQuery.of(context) dentro do widget, o Flutter sabe que seu widget depende dessas informações e vai chamar didChangeDependencies quando elas mudarem. Na maioria dos casos, você não vai precisar sobrescrever esse método, mas é importante saber que ele existe.
build: O Coração do Widget
O método build é o mais importante e o mais frequentemente chamado. É aqui que você descreve como o widget deve aparecer na tela. O Flutter pode chamar build muitas vezes por segundo, especialmente durante animações. Por isso, o método build nunca deve fazer operações lentas — sem requisições de rede, sem leituras de banco de dados, sem cálculos pesados. Apenas construção de widgets.
didUpdateWidget: Quando o Pai Muda
Imagine que você tem um StatefulWidget filho que recebe parâmetros do widget pai. Quando o pai é reconstruído (por exemplo, após um setState no pai), o Flutter pode reaproveitar o mesmo objeto State do filho — mas vai chamar didUpdateWidget com o novo widget pai. Você sobrescreve esse método quando precisa reagir às mudanças nesses parâmetros.
@override
void didUpdateWidget(covariant MeuWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// Verificar se algum parâmetro importante mudou
if (widget.parametroImportante != oldWidget.parametroImportante) {
// Reagir à mudança — por exemplo, cancelar uma operação anterior
// e iniciar uma nova
_reiniciarOperacao();
}
}dispose: O Momento da Despedida
O método dispose é chamado quando o widget é removido permanentemente da árvore. É aqui que você libera os recursos que foram criados em initState: cancela controladores de animação, fecha streams, cancela timers, remove listeners. Não fazer isso é um vazamento de memória. Recursos que não são liberados continuam consumindo memória e processamento mesmo depois que o widget desapareceu da tela.
Regra de ouro: Tudo que você cria em initState, você destrói em dispose. Se você criou um AnimationController, um TextEditingController, um Timer ou uma assinatura de Stream — você é responsável por cancelar/descartar em dispose. O Dart não faz isso por você.
O BuildContext: O Passaporte do Widget na Árvore
Todo método build recebe um parâmetro chamado BuildContext. Esse objeto é frequentemente passado mas raramente explicado com profundidade nos materiais introdutórios. Aqui vou corrigir isso.
O BuildContext representa a localização do widget dentro da árvore de elementos. Ele é essencialmente um ponteiro que diz ao Flutter “este widget está aqui, neste nível, com estes ancestrais acima dele”. Com esse ponteiro, o widget pode fazer perguntas ao Flutter: qual é o tema atual? Qual é o tamanho disponível? Qual é o idioma configurado? Essas informações vêm dos ancestrais na árvore, e o BuildContext é o meio de acessá-las.
Exemplo — Acessando informações via BuildContext
@override
Widget build(BuildContext context) {
// Acessar o tema da aplicação (cores, tipografia, etc.)
final tema = Theme.of(context);
// Acessar o tamanho e orientação da tela
final tamanhoTela = MediaQuery.of(context).size;
final orientacao = MediaQuery.of(context).orientation;
// Usar as informações do tema na construção da interface
return Container(
width: tamanhoTela.width * 0.8, // 80% da largura da tela
decoration: BoxDecoration(
color: tema.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: tema.shadowColor.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: Text(
'Olá, Flutter!',
style: tema.textTheme.headlineMedium,
),
);
}Existe uma armadilha clássica com o BuildContext que você vai encontrar cedo ou tarde: usar um context depois que o widget foi removido da árvore. Isso acontece com frequência em operações assíncronas — você inicia uma requisição de rede, o usuário navega para outra tela (removendo o widget da árvore), a requisição termina e você tenta usar context para navegar ou exibir um SnackBar. O Flutter vai lançar um erro porque o context já não é válido.
A solução é verificar se o widget ainda está “montado” antes de usar o context após uma operação assíncrona:
Os Widgets Estruturais: O Esqueleto de Toda Tela
Existem alguns widgets que você vai usar em praticamente todas as telas que construir. Eles formam o “esqueleto” que sustenta todo o resto. Dominar esses widgets não é opcional — é o pré-requisito para qualquer trabalho real em Flutter.
Scaffold: A Estrutura Padrão de Qualquer Tela
O Scaffold é o widget que implementa a estrutura visual padrão do Material Design. Ele sabe que uma tela típica de aplicativo tem uma área para a barra de navegação no topo, um conteúdo principal no centro, um botão de ação flutuante no canto, e opcionalmente uma gaveta lateral e uma barra de navegação inferior. Você não precisa posicionar nada manualmente: o Scaffold cuida disso por você.
Exemplo — Um Scaffold completo
import 'package:flutter/material.dart';
class TelaPrincipal extends StatelessWidget {
const TelaPrincipal({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
// AppBar: a barra de navegação no topo
appBar: AppBar(
title: const Text('Meu Aplicativo'),
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.search),
onPressed: () {
// ação de busca
},
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
// menu de opções
},
),
],
),
// body: o conteúdo principal da tela
body: const Center(
child: Text('Conteúdo principal aqui'),
),
// drawer: gaveta que desliza da esquerda
drawer: Drawer(
child: ListView(
children: const [
DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text(
'Menu',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
ListTile(
leading: Icon(Icons.home),
title: Text('Início'),
),
ListTile(
leading: Icon(Icons.person),
title: Text('Perfil'),
),
],
),
),
// floatingActionButton: botão de ação flutuante
floatingActionButton: FloatingActionButton(
onPressed: () {
// ação principal da tela
},
child: const Icon(Icons.add),
),
// bottomNavigationBar: barra de navegação inferior
bottomNavigationBar: BottomNavigationBar(
currentIndex: 0,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Início'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Buscar'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Perfil'),
],
onTap: (index) {
// mudar a tela ativa
},
),
);
}
}AppBar: Mais do Que Apenas um Título
A AppBar é a barra que aparece no topo da tela. Ela pode conter um título, um botão de voltar automático, ícones de ação e uma aba de navegação. O Flutter cuida automaticamente de exibir o botão de voltar quando há uma rota anterior na pilha de navegação — você não precisa implementar isso.
Os Widgets de Conteúdo: Mostrando Informação
Com o esqueleto montado, você precisa preencher o corpo da tela com conteúdo real. Os widgets de conteúdo são os mais variados: textos, imagens, ícones, avatares, divisores. Cada um tem suas próprias propriedades e comportamentos.
Text: Muito Mais do Que Apenas Palavras
O widget Text exibe um texto na tela. Parece simples, mas ele oferece um nível de customização considerável. Você pode definir o estilo completo de uma vez usando o parâmetro style, que recebe um objeto TextStyle.
Quando você precisa de um texto com partes em estilos diferentes — por exemplo, uma palavra em negrito dentro de uma frase normal —, o widget RichText com TextSpan é a solução:
Image: Exibindo Imagens de Diferentes Fontes
O widget Image exibe imagens e suporta múltiplas fontes. Para imagens da internet, use Image.network. Para imagens que você incluiu no seu projeto (assets), use Image.asset. Para imagens geradas em memória como bytes, use Image.memory.
// Imagem da internet
Image.network(
'https://exemplo.com/produto.jpg',
width: 200,
height: 150,
fit: BoxFit.cover, // redimensionar para cobrir o espaço disponível
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
// Exibir um indicador de progresso enquanto a imagem carrega
return CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
);
},
errorBuilder: (context, error, stackTrace) {
// Exibir um ícone de erro se a imagem falhar ao carregar
return const Icon(Icons.broken_image, size: 48, color: Colors.grey);
},
)Para imagens locais (assets), primeiro você precisa declará-las no arquivo pubspec.yaml:
Depois, você as acessa com:
Icon e CircleAvatar: Complementos Visuais
O widget Icon exibe um ícone do conjunto Icons do Material Design, que tem centenas de ícones prontos para uso. O CircleAvatar exibe um avatar circular — pode ser uma imagem ou, quando a imagem não está disponível, um círculo colorido com iniciais de texto.
// Ícone com cor e tamanho personalizados
const Icon(
Icons.favorite,
color: Colors.red,
size: 32,
)
// Avatar com imagem da rede
CircleAvatar(
radius: 40,
backgroundImage: NetworkImage('https://exemplo.com/foto.jpg'),
)
// Avatar com iniciais (fallback quando não há imagem)
CircleAvatar(
radius: 40,
backgroundColor: Colors.deepPurple,
child: const Text(
'AL',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
)Os Widgets de Entrada: Coletando Dados do Usuário
Uma interface só vai em uma direção se o usuário não puder interagir com ela. Os widgets de entrada são os meios pelos quais o usuário comunica suas escolhas e informa dados ao aplicativo. No Módulo 06, você vai se aprofundar em formulários e validação — mas já neste módulo você precisa conhecer os principais widgets de entrada para poder construir a tela inicial do projeto.
TextField: A Caixa de Texto
O TextField é o widget de entrada de texto livre. Ele é altamente configurável e quase sempre é controlado por um TextEditingController, que permite ler e modificar o texto programaticamente.
class _MinhaTela extends State<MinhaTela> {
// O TextEditingController guarda o texto atual e é usado para
// ler o valor quando necessário.
final TextEditingController _buscaController = TextEditingController();
@override
void dispose() {
// IMPORTANTE: sempre dispose do controller!
_buscaController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _buscaController,
decoration: InputDecoration(
labelText: 'Buscar produtos',
hintText: 'Digite o nome do produto...',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _buscaController.clear(),
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onSubmitted: (texto) {
// Chamado quando o usuário pressiona Enter/confirmar no teclado
_realizarBusca(texto);
},
);
}
void _realizarBusca(String termo) {
// lógica de busca
}
}Checkbox, Switch e Radio: Escolhas Booleanas e Únicas
Esses três widgets capturam diferentes tipos de escolha do usuário. O Checkbox aceita ou recusa uma única opção. O Switch liga ou desliga algo (conceitualmente idêntico ao Checkbox, mas visualmente diferente). O Radio apresenta um grupo de opções mutuamente exclusivas.
// Checkbox
bool _aceitouTermos = false;
Checkbox(
value: _aceitouTermos,
onChanged: (novoValor) {
setState(() {
_aceitouTermos = novoValor ?? false;
});
},
)
// Switch
bool _notificacoesAtivas = true;
Switch(
value: _notificacoesAtivas,
activeColor: Colors.green,
onChanged: (novoValor) {
setState(() {
_notificacoesAtivas = novoValor;
});
},
)
// Radio (grupo de opções)
String _tamanhoSelecionado = 'M';
Column(
children: ['P', 'M', 'G', 'GG'].map((tamanho) {
return RadioListTile<String>(
title: Text(tamanho),
value: tamanho,
groupValue: _tamanhoSelecionado,
onChanged: (novoTamanho) {
setState(() {
_tamanhoSelecionado = novoTamanho!;
});
},
);
}).toList(),
)Slider: Seleção de Valores em uma Faixa
O Slider permite que o usuário selecione um valor dentro de um intervalo contínuo, deslizando um indicador sobre uma trilha.
Keys: Quando o Flutter Precisa de Ajuda Para Identificar Widgets
Um conceito avançado que evita bugs silenciosos em listas dinâmicas
O mecanismo de reconciliação do Flutter — aquele processo de comparar a Widget Tree antiga com a nova para decidir o que atualizar — funciona comparando widgets pelo tipo e pela posição na árvore. Na maioria dos casos, isso funciona perfeitamente. Mas há uma situação em que ele pode falhar silenciosamente: listas dinâmicas de StatefulWidget.
Imagine que você tem uma lista com três itens, cada um sendo um StatefulWidget com estado local (por exemplo, um Checkbox que o usuário pode marcar). Se você reordenar essa lista, o Flutter pode emparelhar o estado antigo com o widget errado, porque ele compara por posição. O resultado é um comportamento visual incorreto e difícil de depurar.
A solução são as Keys: um identificador único que você fornece ao widget para que o Flutter possa rastrear sua identidade mesmo quando ele muda de posição na árvore.
// SEM keys: se a lista for reordenada, o estado pode ficar
// associado ao widget errado.
ListView.builder(
itemCount: itens.length,
itemBuilder: (context, index) {
return ItemDaLista(item: itens[index]); // PROBLEMA potencial!
},
)
// COM keys: o Flutter consegue identificar cada widget
// pela sua chave e manter o estado correto após reordenação.
ListView.builder(
itemCount: itens.length,
itemBuilder: (context, index) {
return ItemDaLista(
key: ValueKey(itens[index].id), // id único do item como chave
item: itens[index],
);
},
)Existem três tipos principais de Key que você vai usar: ValueKey (para identificar pelo valor de um dado, como um ID), ObjectKey (para identificar pelo próprio objeto) e UniqueKey (que gera uma chave única a cada construção — útil para forçar a recriação de um widget).
Regra prática: Se você está construindo uma lista de StatefulWidget onde os itens podem ser reordenados, adicionados ou removidos, use sempre ValueKey com o identificador único de cada item. Para listas de StatelessWidget, as keys são desnecessárias na maioria dos casos.
InheritedWidget: A Fundação do Provider
Existe um problema clássico no desenvolvimento com Flutter: como passar dados para um widget que está muito profundo na árvore, sem ter que repassar o mesmo parâmetro por cada widget intermediário? Se um dado precisa chegar ao nível 8 da árvore a partir do nível 1, você não quer passar esse dado por 7 widgets que não têm nenhum interesse nele.
A solução nativa do Flutter para esse problema é o InheritedWidget. Ele é um tipo especial de widget que disponibiliza dados para todos os seus descendentes na árvore, sem que nenhum widget intermediário precise saber que eles existem. Quando os dados mudam, apenas os descendentes que realmente leram esses dados são reconstruídos.
Você provavelmente não vai criar InheritedWidgets diretamente com muita frequência. O pacote Provider — que você vai estudar no Módulo 07 — é construído sobre essa fundação e oferece uma API muito mais amigável. Mas entender o InheritedWidget é importante porque ele explica por que o Provider funciona da forma que funciona.
Veja um exemplo simplificado de como um InheritedWidget é estruturado:
// 1. Definir o InheritedWidget com os dados a serem compartilhados
class DadosDoUsuario extends InheritedWidget {
final String nomeDoUsuario;
final String emailDoUsuario;
const DadosDoUsuario({
super.key,
required this.nomeDoUsuario,
required this.emailDoUsuario,
required super.child,
});
// Método estático para acessar os dados de qualquer descendente
static DadosDoUsuario of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<DadosDoUsuario>()!;
}
// Determina se os widgets dependentes devem reconstruir quando os dados mudam
@override
bool updateShouldNotify(DadosDoUsuario oldWidget) {
return nomeDoUsuario != oldWidget.nomeDoUsuario ||
emailDoUsuario != oldWidget.emailDoUsuario;
}
}
// 2. Envolver parte da árvore com o InheritedWidget
DadosDoUsuario(
nomeDoUsuario: 'Ana Lima',
emailDoUsuario: 'ana@exemplo.com',
child: MinhaAplicacao(),
)
// 3. Acessar os dados em qualquer descendente, sem passar por parâmetros
class BarraDeNomeWidget extends StatelessWidget {
const BarraDeNomeWidget({super.key});
@override
Widget build(BuildContext context) {
// Acessar os dados diretamente, de qualquer profundidade na árvore
final dados = DadosDoUsuario.of(context);
return Text('Bem-vindo, ${dados.nomeDoUsuario}!');
}
}Temas: Consistência Visual sem Repetição
Um dos maiores problemas de código Flutter sem organização é a repetição de estilos. Quando cada Text tem seu TextStyle inline, cada Container tem sua cor definida diretamente, e cada botão tem suas próprias dimensões hardcoded, o código vira um pesadelo de manutenção. Mudar a cor primária da aplicação significa encontrar e alterar centenas de lugares. Há uma solução elegante para isso: os temas.
O sistema de temas do Flutter centraliza todas as definições visuais em um único lugar — o ThemeData — que é passado ao MaterialApp. Qualquer widget descendente pode acessar essas definições via Theme.of(context).
Exemplo — Definindo um tema completo para o aplicativo
// lib/theme/app_theme.dart
// Este é exatamente o arquivo que você vai criar no Projeto Integrador!
import 'package:flutter/material.dart';
class AppTheme {
// Cores principais da aplicação — defina uma vez, use em todo lugar
static const Color _corPrimaria = Color(0xFF5C6BC0); // roxo-azulado
static const Color _corSecundaria = Color(0xFF26A69A); // verde-água
static const Color _corErro = Color(0xFFEF5350); // vermelho
// Tema claro
static ThemeData get temaClaro {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: _corPrimaria,
secondary: _corSecundaria,
error: _corErro,
brightness: Brightness.light,
),
// Estilo padrão para todos os AppBars
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
backgroundColor: _corPrimaria,
foregroundColor: Colors.white,
),
// Estilo padrão para todos os ElevatedButtons
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: _corPrimaria,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
// Estilo padrão para todos os Cards
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
// Tipografia padrão
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
headlineMedium: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
),
bodyLarge: TextStyle(fontSize: 16),
bodyMedium: TextStyle(fontSize: 14),
),
);
}
}Depois, você aplica o tema no MaterialApp:
// lib/main.dart
void main() {
runApp(const MeuApp());
}
class MeuApp extends StatelessWidget {
const MeuApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Meu Projeto Integrador',
theme: AppTheme.temaClaro, // aplicar o tema centralizado
home: const TelaInicial(),
);
}
}E em qualquer widget, você acessa as cores e estilos do tema sem repetir nada:
Construindo a Tela Inicial: Juntando Tudo
Chegou a hora de juntar todos os conceitos deste módulo em algo concreto. Vamos construir uma tela inicial realista — do tipo que você vai implementar no seu Projeto Integrador. Ela terá um Scaffold com AppBar, uma lista de cards com produtos, um campo de busca e um FloatingActionButton. Alguns elementos terão estado local (a busca), outros serão puramente estáticos.
Exemplo — Tela inicial completa com busca e lista de produtos
import 'package:flutter/material.dart';
// Modelo simples de produto (virá dos seus modelos de domínio do Módulo 02)
class Produto {
final String id;
final String nome;
final double preco;
final String imagemUrl;
final String categoria;
const Produto({
required this.id,
required this.nome,
required this.preco,
required this.imagemUrl,
required this.categoria,
});
}
// Dados de exemplo — no projeto real, virão do repositório/Provider
const List<Produto> _produtosExemplo = [
Produto(
id: '1',
nome: 'Tênis Esportivo Pro',
preco: 299.90,
imagemUrl: 'https://picsum.photos/id/26/200',
categoria: 'Calçados',
),
Produto(
id: '2',
nome: 'Camiseta Premium',
preco: 89.90,
imagemUrl: 'https://picsum.photos/id/27/200',
categoria: 'Vestuário',
),
Produto(
id: '3',
nome: 'Mochila Tática',
preco: 199.90,
imagemUrl: 'https://picsum.photos/id/28/200',
categoria: 'Acessórios',
),
];
// Tela inicial — StatefulWidget porque tem o estado da busca
class TelaInicial extends StatefulWidget {
const TelaInicial({super.key});
@override
State<TelaInicial> createState() => _TelaInicialState();
}
class _TelaInicialState extends State<TelaInicial> {
final TextEditingController _buscaController = TextEditingController();
String _termoBusca = '';
@override
void dispose() {
_buscaController.dispose();
super.dispose();
}
// Filtra os produtos com base no termo de busca
List<Produto> get _produtosFiltrados {
if (_termoBusca.isEmpty) return _produtosExemplo;
return _produtosExemplo
.where((p) =>
p.nome.toLowerCase().contains(_termoBusca.toLowerCase()) ||
p.categoria.toLowerCase().contains(_termoBusca.toLowerCase()))
.toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Produtos'),
actions: [
IconButton(
icon: const Icon(Icons.shopping_cart),
onPressed: () {
// navegar para o carrinho (Módulo 05)
},
),
],
),
body: Column(
children: [
// Campo de busca
Padding(
padding: const EdgeInsets.all(16),
child: TextField(
controller: _buscaController,
decoration: InputDecoration(
hintText: 'Buscar produtos ou categorias...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _termoBusca.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_buscaController.clear();
setState(() => _termoBusca = '');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onChanged: (valor) {
setState(() => _termoBusca = valor);
},
),
),
// Contador de resultados
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Text(
'${_produtosFiltrados.length} produto(s) encontrado(s)',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
),
),
const SizedBox(height: 8),
// Lista de produtos
Expanded(
child: _produtosFiltrados.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'Nenhum produto encontrado',
style: TextStyle(color: Colors.grey, fontSize: 16),
),
],
),
)
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _produtosFiltrados.length,
itemBuilder: (context, index) {
return CardDeProduto(
key: ValueKey(_produtosFiltrados[index].id),
produto: _produtosFiltrados[index],
);
},
),
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
// adicionar novo produto (funcionalidade futura)
},
icon: const Icon(Icons.add),
label: const Text('Adicionar'),
),
);
}
}
// Card de produto — StatelessWidget porque não tem estado próprio
class CardDeProduto extends StatelessWidget {
final Produto produto;
const CardDeProduto({super.key, required this.produto});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () {
// navegar para detalhe do produto (Módulo 05)
},
borderRadius: BorderRadius.circular(12),
child: Row(
children: [
// Imagem do produto
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
child: Image.network(
produto.imagemUrl,
width: 100,
height: 100,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 100,
height: 100,
color: Colors.grey[200],
child: const Icon(Icons.image, color: Colors.grey),
),
),
),
// Informações do produto
Expanded(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
produto.nome,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(4),
),
child: Text(
produto.categoria,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
const SizedBox(height: 8),
Text(
'R\$ ${produto.preco.toStringAsFixed(2)}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
),
],
),
),
);
}
}Decomposição de Widgets: A Arte de Dividir para Conquistar
Olhando o exemplo da tela inicial acima, você talvez tenha percebido que o código do build do _TelaInicialState é longo. Isso é proposital no exemplo, para mostrar tudo em um lugar. Na prática, código assim se torna difícil de manter.
A solução é a decomposição de widgets: identificar partes da interface que fazem sentido como unidades independentes e extraí-las para seus próprios widgets. O CardDeProduto no exemplo já é um resultado dessa decomposição — em vez de ter o código do card inline dentro do ListView.builder, ele foi extraído para uma classe separada.
Como decidir o que extrair para um widget separado? Algumas perguntas guiam essa decisão. Essa parte da interface se repete em mais de um lugar? Se sim, extraia. Essa parte tem lógica própria complexa o suficiente para merecer seu próprio escopo? Se sim, extraia. Essa parte é um componente visual conceitualmente distinto — um card, um header, um item de lista? Se sim, extraia.
graph TD
A["TelaInicial (StatefulWidget)"] --> B["AppBar"]
A --> C["CampoDeBusca (StatelessWidget)"]
A --> D["ContadorDeResultados (StatelessWidget)"]
A --> E["ListaDeProdutos (StatelessWidget)"]
E --> F["CardDeProduto (StatelessWidget)<br/>× N itens"]
F --> G["ImagemDoProduto (StatelessWidget)"]
F --> H["InformacoesDoProduto (StatelessWidget)"]
A --> I["FloatingActionButton"]
style A fill:#cfe2ff,stroke:#0d6efd
style B fill:#d1ecf1,stroke:#17a2b8
style C fill:#d1ecf1,stroke:#17a2b8
style D fill:#d1ecf1,stroke:#17a2b8
style E fill:#d1ecf1,stroke:#17a2b8
style F fill:#d4edda,stroke:#28a745
style G fill:#f5c6cb,stroke:#dc3545
style H fill:#f5c6cb,stroke:#dc3545
style I fill:#d1ecf1,stroke:#17a2b8
Esse diagrama de árvore de widgets é exatamente o que você vai criar e incluir na documentação do seu Projeto Integrador neste módulo. Ele serve como projeto arquitetural da interface: antes de escrever uma linha de código, você planeja quais serão os widgets, quais terão estado e como eles se relacionam.
Widgets Importantes que Você Vai Usar Toda Semana
Além dos widgets estruturais e de conteúdo que já vimos em detalhe, há um conjunto de widgets utilitários que aparecem em praticamente toda tela Flutter e merecem atenção específica.
Container: O Widget Canivete-Suíço
O Container é o widget mais versátil do Flutter. Ele pode definir tamanho, cor de fundo, borda, arredondamento de cantos, sombra, margem interna e externa, transformação visual e muito mais. Quando você precisa de um espaço decorado ao redor de outro widget, Container provavelmente é o que você quer.
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(16), // espaço externo
padding: const EdgeInsets.all(12), // espaço interno
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade300),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const Text('Conteúdo dentro do container'),
)SizedBox: Espaçamento Simples e Preciso
O SizedBox é o widget para adicionar espaçamentos precisos entre elementos. Você vai usá-lo constantemente para criar respiração visual entre componentes.
Padding: Espaço Interno sem Container
Quando você só precisa de padding e não de decoração visual, Padding é mais simples e semântico do que Container.
Center: Centralizando com Facilidade
O widget Center centraliza seu filho tanto horizontal quanto verticalmente dentro do espaço disponível.
Visibility e Opacity: Controle de Visibilidade
O Visibility mostra ou esconde seu filho mantendo ou não seu espaço na tela. O Opacity controla a transparência de 0.0 (invisível) a 1.0 (totalmente opaco).
GestureDetector e InkWell: Capturando Toques
O GestureDetector transforma qualquer widget em algo que responde a gestos do usuário — toque, toque longo, arrastar, pinçar. O InkWell faz o mesmo, mas com o efeito visual de ondulação (ripple effect) do Material Design.
GestureDetector(
onTap: () => print('Tocado!'),
onLongPress: () => print('Toque longo!'),
child: const Text('Toque em mim'),
)
// InkWell precisa de Material ancestral para mostrar o ripple
InkWell(
onTap: () => print('Tocado com ripple!'),
borderRadius: BorderRadius.circular(8),
child: const Padding(
padding: EdgeInsets.all(8),
child: Text('Toque com efeito visual'),
),
)Sua Conexão com o Projeto Integrador
O que você vai construir nesta semana
Todo o conteúdo deste módulo converge para três tarefas concretas que você e sua equipe vão realizar nas aulas práticas desta semana.
Na primeira tarefa, você vai criar o arquivo lib/theme/app_theme.dart com o tema completo da aplicação: paleta de cores, tipografia, estilo dos botões, cards e AppBars. Essa decisão de design é importante e vai moldar toda a aparência do aplicativo pelo restante do semestre. Discuta com o grupo quais cores e estilos fazem sentido para o domínio do aplicativo que vocês estão construindo.
Na segunda tarefa, você vai construir a tela inicial do aplicativo com widgets reais — não placeholders. Essa tela deve exibir dados dos modelos de domínio que o grupo criou no Módulo 02. Não se preocupe se os dados ainda são estáticos (listas fixas no código): o importante é que a interface mostre a estrutura final da tela, com os cards corretos, os textos corretos e a organização visual correta. A integração com um backend real virá nos módulos seguintes.
Na terceira tarefa, você vai revisar a tela inicial e identificar quais partes dela têm estado local — um campo de busca, um contador, uma seleção de filtro — e vai refatorar esses componentes para StatefulWidget com uso adequado de setState. Além disso, você vai criar o diagrama de árvore de widgets da tela inicial e incluí-lo na documentação do repositório.
Entregável desta semana: A tela inicial funcional rodando, o arquivo app_theme.dart com o tema definido, e o diagrama de árvore de widgets incluído no arquivo README.md do repositório (como um diagrama Mermaid, como você aprendeu neste material).
Boas Práticas de Organização de Widgets
Saber usar widgets individualmente é apenas metade da habilidade. A outra metade é saber organizá-los de forma que o código do projeto seja legível, manutenível e escalável. Projetos Flutter que crescem sem organização tornam-se rapidamente um problema para toda a equipe: ninguém sabe onde encontrar determinado widget, os arquivos ficam enormes e qualquer mudança pequena exige navegar por centenas de linhas de código. A boa notícia é que existe um conjunto de convenções amplamente adotadas pela comunidade Flutter que resolve isso.
Uma Classe por Arquivo
A convenção mais importante é: cada widget vive em seu próprio arquivo Dart, e o nome do arquivo reflete o nome do widget em snake_case. Se você criou uma classe chamada CardDeProduto, o arquivo se chama card_de_produto.dart. Essa convenção torna a navegação no projeto trivial — você sempre sabe onde encontrar um widget pelo nome do arquivo.
No contexto do Projeto Integrador, que adota a organização Feature-First, cada funcionalidade tem sua própria pasta de widgets. O widget CardDeProduto ficaria em lib/features/produtos/presentation/widgets/card_de_produto.dart. Essa localização diz tudo: ele é um widget (widgets/) da camada de apresentação (presentation/) da funcionalidade de produtos (produtos/).
Separar Estado de Apresentação
Um padrão que você vai encontrar repetidamente em projetos Flutter profissionais é a separação entre widgets que gerenciam estado e widgets que apenas apresentam dados. O widget que gerencia estado (o StatefulWidget ou o widget conectado ao Provider) nunca deveria ter código de layout elaborado dentro dele. Ele cuida do estado e delega a apresentação visual para um widget filho que recebe os dados como parâmetros.
Essa separação tem um benefício enorme: o widget de apresentação se torna testável e reutilizável. Você pode exibir o mesmo CardDeProduto em diferentes contextos — na lista principal, nos favoritos, nos resultados de busca — simplesmente passando um produto diferente para ele.
Constantes de Estilo Fora dos Widgets
Outra prática valiosa é extrair constantes de estilo para fora dos métodos build. Em vez de escrever EdgeInsets.all(16) em dezenas de lugares, você define uma constante uma vez e a usa em todo lugar. Quando o designer pede para aumentar o espaçamento padrão de 16 para 20 pixels, você muda em um único lugar e o efeito se propaga por toda a aplicação.
// lib/theme/app_spacing.dart
class AppSpacing {
static const double xs = 4;
static const double sm = 8;
static const double md = 16;
static const double lg = 24;
static const double xl = 32;
static const double xxl = 48;
}
// Uso nos widgets
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: ...
)Depurando Problemas Comuns com Widgets
Todo desenvolvedor Flutter, não importa o nível de experiência, já se deparou com as mensagens de erro mais comuns do framework. Conhecê-las e saber o que elas significam vai economizar horas de frustração ao longo do semestre.
O Erro de “Overflow”
Esse é provavelmente o erro mais frequente para quem está começando. Ele aparece como uma faixa amarela com linhas pretas na tela durante o desenvolvimento e a mensagem A RenderFlex overflowed by X pixels on the bottom/right. O que está acontecendo: um widget filho é maior do que o espaço que o widget pai disponibilizou para ele.
A causa mais comum é um Column dentro de outro Column, ou um Column dentro de um SingleChildScrollView sem a configuração correta. A solução depende do contexto: às vezes você precisa de um Expanded para distribuir o espaço disponível; às vezes precisa de um Flexible para deixar o widget se adaptar; às vezes precisa de um SingleChildScrollView para permitir rolagem quando o conteúdo excede o espaço.
// PROBLEMA: Column com filhos que podem exceder o espaço da tela
Column(
children: [
WidgetAlto(),
WidgetAlto(),
WidgetAlto(), // overflow!
],
)
// SOLUÇÃO 1: Envolver em SingleChildScrollView para permitir rolagem
SingleChildScrollView(
child: Column(
children: [
WidgetAlto(),
WidgetAlto(),
WidgetAlto(),
],
),
)
// SOLUÇÃO 2: Usar Expanded para um dos filhos ocupar o espaço restante
Column(
children: [
const HeaderFixo(),
Expanded( // ocupa todo o espaço restante
child: ListaRolavel(),
),
],
)O Erro de “Unbounded Height”
Esse erro aparece quando você coloca um ListView (ou GridView, ou qualquer widget de rolagem) dentro de uma Column sem envolvê-lo com Expanded ou dar a ele uma altura fixa. O problema é que a Column diz ao ListView “você tem espaço ilimitado verticalmente” — e o ListView não sabe o quanto espaço renderizar.
// PROBLEMA: ListView dentro de Column sem restrição de tamanho
Column(
children: [
const Text('Título'),
ListView.builder( // ERRO: unbounded height
itemCount: 10,
itemBuilder: (_, i) => Text('Item $i'),
),
],
)
// SOLUÇÃO: Envolver o ListView com Expanded
Column(
children: [
const Text('Título'),
Expanded(
child: ListView.builder(
itemCount: 10,
itemBuilder: (_, i) => Text('Item $i'),
),
),
],
)O Widget Inspector: Sua Ferramenta de Diagnóstico Visual
O Flutter SDK inclui, integrado ao VS Code e ao Android Studio, um Widget Inspector que é extremamente útil para entender a estrutura da árvore de widgets em tempo real. Você pode abrir o Widget Inspector durante a execução do aplicativo e clicar em qualquer elemento da tela para ver qual widget o representa, quais são seus parâmetros, qual é seu BuildContext e quais são suas restrições de tamanho.
Para abrir o Widget Inspector no VS Code, vá ao painel “Flutter DevTools” durante uma sessão de depuração. Essa ferramenta vai te salvar muitas horas de depuração ao longo do semestre.
O Papel dos Widgets Imutáveis na Performance
Um aspecto do Flutter que parece contraintuitivo à primeira vista é que os widgets são criados e descartados com muita frequência. Cada setState, cada rolagem na lista, cada animação pode disparar dezenas ou centenas de reconstruções de widgets por segundo. Se a criação de objetos fosse cara, isso seria um problema de desempenho sério.
O Flutter resolve isso de duas formas complementares. Primeiro, os widgets são objetos extremamente leves: eles carregam apenas configuração (os parâmetros do construtor) e o método build. Não há renderização, não há estado pesado. Segundo, o mecanismo de reconciliação com a Element Tree significa que mesmo quando a Widget Tree é recriada, os objetos de renderização subjacentes só são atualizados quando realmente necessário.
O const desempenha um papel importante aqui. Quando você marca um widget como const, o compilador Dart pode criar esse objeto uma única vez e reutilizá-lo para sempre, sem recriação. Isso é particularmente poderoso para widgets que nunca mudam — textos de rótulo, ícones decorativos, separadores — que sem const seriam recriados a cada reconstrução do pai.
// Sem const: Text é recriado a cada rebuild do pai
Text('Bem-vindo')
// Com const: Text é criado uma vez e reutilizado
const Text('Bem-vindo')
// Uma boa prática é declarar o método build de um StatelessWidget
// com const nos filhos que nunca mudam:
@override
Widget build(BuildContext context) {
return const Column(
children: [
Text('Título fixo'), // const implícito dentro de const Column
Icon(Icons.star), // const implícito
Divider(), // const implícito
],
);
}Existe uma regra de ouro que você pode aplicar sempre: se o Dart reclamar que um widget poderia ser const mas não é, trate esse aviso como um erro e corrija imediatamente. O VS Code e o Android Studio fazem isso automaticamente com a opção “Add const” — use-a sempre.
Widgets Adaptativos: Respondendo a Diferentes Contextos
O Flutter oferece um conjunto de widgets que se adaptam automaticamente à plataforma onde o aplicativo está rodando. Em vez de exibir o visual padrão do Material Design em todos os contextos, esses widgets adaptam sua aparência para seguir as convenções visuais de cada sistema operacional. No Android, eles se parecem com Material Design; no iOS, se parecem com Cupertino (o design da Apple).
Para esta disciplina, você vai trabalhar principalmente com Material Design — que é o padrão do Flutter e do Android. Mas é importante saber que existem widgets com o prefixo Cupertino que imitam o estilo iOS: CupertinoButton, CupertinoAlertDialog, CupertinoSwitch, entre outros.
O Flutter também oferece a família de widgets Adaptive para algumas situações específicas. O Switch.adaptive, por exemplo, renderiza um Material Switch no Android e um CupertinoSwitch no iOS automaticamente, sem que você precise escrever código condicional.
// Switch que se adapta à plataforma automaticamente
Switch.adaptive(
value: _notificacoesAtivas,
onChanged: (valor) {
setState(() => _notificacoesAtivas = valor);
},
)
// Diálogo que se adapta à plataforma
AlertDialog.adaptive(
title: const Text('Confirmar ação'),
content: const Text('Tem certeza que deseja continuar?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirmar'),
),
],
)Widgets de Feedback: Informando o Usuário sobre o que Está Acontecendo
Uma interface que não dá feedback ao usuário é uma interface frustrante. Quando o usuário toca um botão e nada acontece visivelmente por dois segundos, ele não sabe se o toque foi registrado, se a aplicação travou ou se está processando. O feedback visual adequado é um dos marcadores mais importantes de qualidade em interfaces móveis.
O Flutter oferece vários mecanismos de feedback que você deve incorporar às telas do Projeto Integrador.
CircularProgressIndicator: “Por Favor, Aguarde”
O CircularProgressIndicator é o indicador de carregamento circular clássico. Você o usa quando uma operação está em andamento e o tempo total é desconhecido. Quando o tempo total é conhecido (por exemplo, um upload com progresso mensurável), use o LinearProgressIndicator com o parâmetro value.
// Indicador de carregamento indeterminado
const CircularProgressIndicator()
// Indicador com progresso conhecido (0.0 a 1.0)
LinearProgressIndicator(
value: _progresso, // null para indeterminado, 0.0-1.0 para determinado
backgroundColor: Colors.grey[200],
color: Theme.of(context).colorScheme.primary,
)SnackBar: Mensagens Rápidas e Discretas
O SnackBar é a forma correta de exibir mensagens curtas ao usuário — confirmações, erros, avisos — sem interromper o fluxo da aplicação. Ele aparece na parte inferior da tela por alguns segundos e desaparece automaticamente.
// Exibir uma SnackBar com mensagem de sucesso
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Produto adicionado ao carrinho!'),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
),
);
// SnackBar com botão de desfazer
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Item removido'),
action: SnackBarAction(
label: 'Desfazer',
onPressed: () {
// desfazer a remoção
},
),
),
);AlertDialog: Confirmações Importantes
Para ações irreversíveis ou decisões importantes que requerem confirmação explícita do usuário, use o AlertDialog. Ele bloqueia a interação com o resto da tela até o usuário fazer uma escolha.
Future<bool?> _confirmarExclusao(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirmar exclusão'),
content: const Text(
'Esta ação não pode ser desfeita. Deseja realmente excluir este item?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Excluir'),
),
],
),
);
}
// Uso:
final confirmado = await _confirmarExclusao(context);
if (confirmado == true && mounted) {
_excluirItem();
}Conexões com os Módulos Seguintes
O que você aprendeu neste módulo não é um destino — é um ponto de partida. Cada conceito que você dominou aqui vai ser aprofundado, expandido e combinado nos módulos seguintes, e é importante que você veja essas conexões desde já.
No Módulo 04, você vai aprender os widgets de layout com profundidade: Row, Column, Stack, ListView, GridView e os Slivers. O Scaffold que você aprendeu aqui é o container; o Módulo 04 vai te ensinar como organizar o conteúdo dentro dele com precisão e elegância.
No Módulo 05, você vai aprender navegação com go_router. As telas que você constrói agora são isoladas — o Módulo 05 vai conectar tudo, criando o fluxo de navegação completo da aplicação.
No Módulo 06, você vai se aprofundar nos widgets de formulário que foram introduzidos aqui: TextField, Checkbox, Radio. Você vai aprender validação, controle de foco e submissão de formulários de forma profissional.
No Módulo 07, você vai entender como o estado deixa de ser local (gerenciado por setState) e passa a ser compartilhado entre múltiplos widgets com o Provider. O InheritedWidget que você aprendeu aqui é a base sobre a qual o Provider está construído — esse conhecimento vai fazer o Módulo 07 fazer sentido imediatamente.
Cada conceito que você aprende tem uma vida longa pela frente. Invista bem neste módulo.
Resumo: O Que Você Aprendeu Neste Módulo
Chegamos ao final de um módulo denso e cheio de novidades. Vamos recapitular o que você dominou até aqui.
Você entendeu que no Flutter tudo é um widget — não como slogan, mas como consequência arquitetural real. Aprendeu que o Flutter mantém três árvores internas — Widget Tree, Element Tree e Render Tree — e que a reconciliação entre elas é o que torna o Flutter eficiente.
Você distinguiu com clareza StatelessWidget de StatefulWidget: o primeiro é imutável e simples, o segundo guarda estado e pode se reconstruir. Você conheceu o ciclo de vida completo do StatefulWidget — initState, didChangeDependencies, build, didUpdateWidget, dispose — e sabe o que pertence a cada um.
Você entendeu o BuildContext como localizador do widget na árvore e aprendeu a usá-lo para acessar temas, tamanhos e dados de ancestrais. Aprendeu sobre Keys e quando usá-las para evitar bugs silenciosos em listas dinâmicas. E entendeu o InheritedWidget como a fundação sobre a qual o Provider foi construído.
Você conheceu os widgets estruturais que formam toda tela Flutter (Scaffold, AppBar, BottomNavigationBar, Drawer), os widgets de conteúdo (Text, Image, Icon, CircleAvatar) e os widgets de entrada (TextField, Checkbox, Switch, Radio, Slider). E aprendeu a centralizar a identidade visual da aplicação em um ThemeData.
Por fim, você viu como tudo isso se junta em uma tela inicial real, com decomposição de widgets em componentes menores e reutilizáveis — e com o diagrama de árvore de widgets como ferramenta de projeto arquitetural.
Na próxima aula, você trará tudo isso para o seu projeto real. Quando chegar ao laboratório, abra o arquivo app_theme.dart, decida as cores com seu grupo e comece a construir. O seu aplicativo está esperando por você.
Antes da próxima aula: Execute todos os exemplos de código deste módulo no seu computador. Modifique-os: troque cores, adicione widgets, quebre coisas e conserte-as. A melhor forma de fixar o que você aprendeu é experimentar. Se travar em algum ponto, anote a dúvida — a aula teórica é o momento ideal para esclarecê-la com o professor.