Layout básico
No Flutter, layout é a forma como os widgets são posicionados na tela. Como a interface é uma árvore de widgets, normalmente um widget fica dentro de outro. Alguns widgets mostram conteúdo, como Text, e outros widgets organizam esse conteúdo, como Column, Row, Padding, Center e Expanded.
Agora vamos entender estes widgets:
- Column
- Row
- Container
- Padding
- SizedBox
- Center
- Expanded
Column
O Column organiza widgets na vertical, um abaixo do outro.
Column(
children: [
Text('Servidor Web'),
Text('IP: 192.168.0.10'),
Text('Status: Ativo'),
],
)
A tela ficaria assim:
Servidor Web
IP: 192.168.0.10
Status: Ativo
A Column é usada quando a interface precisa empilhar elementos de cima para baixo. Exemplos práticos:
- título
- campo de texto
- botão
- mensagem de erro
Row
Uma Row organiza widgets na horizontal, um ao lado do outro.
Row(
children: [
Text('Status:'),
Text('Ativo'),
],
)
A tela ficaria assim:
Status: Ativo
A Row é usada quando os elementos precisam ficar na mesma linha.
Flex
O Flex é um widget usado para organizar filhos em uma direção flexível, que pode ser horizontal ou vertical. Na prática, Row e Column são formas mais específicas de usar Flex.
- Row → Flex na horizontal
- Column → Flex na vertical
Então o exemplo abaixo:
Row(
children: [
Text('Servidor Web'),
Text('Ativo'),
],
)
É algo mais próximo do exemplo abaixo:
Flex(
direction: Axis.horizontal,
children: [
Text('Servidor Web'),
Text('Ativo'),
],
)
E até mesmo de Column:
Column(
children: [
Text('Servidor Web'),
Text('IP: 192.168.0.10'),
Text('Status: Ativo'),
],
)
A diferença principal é que Row e Column já deixam a direção definida. Por isso, no dia a dia, normalmente usamos mais Row e Column.
Center
O Center é usado para centralizar um widget dentro do espaço disponível.
Center(
child: Text('Nenhum servidor cadastrado'),
)
O Center recebe apenas um filho, usando child. Isso é diferente de Column e Row, que recebem vários filhos usando children.
- Center usa
child. - Column usa
children. - Row usa
children.
A diferença existe porque Center centraliza apenas um widget. Se for necessário centralizar vários textos, primeiro colocamos os textos dentro de uma Column, depois colocamos a Column dentro do Center, exemplo:
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Servidor Web'),
Text('IP: 192.168.0.10'),
],
),
)
Padding
O Padding adiciona espaço ao redor de um widget.
Padding(
padding: EdgeInsets.all(16),
child: Text('Servidor Web'),
)
Aqui o texto recebe um espaço de 16 em todos os lados. O Padding não mostra nada sozinho. Ele só cria espaço ao redor do filho, também dá para definir lados específicos:
Padding(
padding: EdgeInsets.only(top: 16, left: 24),
child: Text('Servidor Web'),
)
Ou espaço horizontal e vertical:
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text('Servidor Web'),
)
Isso é muito usado para a interface não ficar grudada nas bordas da tela.
SizedBox
O SizedBox é usado para criar espaço fixo ou definir tamanho. Para criar espaço vertical:
SizedBox(height: 16)
Para criar espaço horizontal:
SizedBox(width: 8)
Um exemplo um pouco mais completo:
Column(
children: [
Text('Servidor Web'),
SizedBox(height: 8),
Text('IP: 192.168.0.10'),
],
)
A tela fica com um espaço entre o nome do servidor e o IP. O SizedBox também pode definir tamanho para um widget:
SizedBox(
width: 200,
child: ElevatedButton(
onPressed: null,
child: Text('Salvar'),
),
)
Nesse caso, o botão fica com largura de 200.
Container
Um Container é um widget mais flexível. Ele pode ser usado para criar uma área com tamanho, espaçamento, cor, borda e alinhamento.
Container(
padding: EdgeInsets.all(16),
child: Text('Servidor Web'),
)
Aqui o Container adiciona espaço interno ao redor do texto, também pode ter cor:
Container(
padding: EdgeInsets.all(16),
color: Colors.blueGrey,
child: Text('Servidor Web'),
)
E pode ter largura e altura:
Container(
width: 300,
height: 100,
child: Text('Servidor Web'),
)
Um uso comum é criar uma "caixa" visual:
Container(
padding: EdgeInsets.all(16),
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(),
borderRadius: BorderRadius.circular(8),
),
child: Text('Servidor Web'),
)
Nesse exemplo:
padding → espaço interno
margin → espaço externo
border → borda
child → conteúdo dentro do Container
A diferença entre Padding e Container é que Padding só cuida de espaço. O Container pode cuidar de espaço, tamanho, cor, borda e alinhamento. Para algo simples, Padding costuma ser mais direto, mas para uma caixa visual mais completa, Container faz mais sentido.
Expanded
O Expanded é usado dentro de Column ou Row para ocupar o espaço restante disponível, exemplo com Row:
Row(
children: [
Text('Servidor Web'),
Expanded(
child: Text('192.168.0.10'),
),
Text('Ativo'),
],
)
O Expanded faz o widget filho ocupar o espaço livre entre os outros widgets. Isso é útil quando uma parte da linha precisa crescer para preencher o espaço disponível.
Um exemplo mais fácil de visualizar:
Row(
children: [
Text('Nome:'),
SizedBox(width: 8),
Expanded(
child: Text('Servidor Web de Produção'),
),
],
)
Sem Expanded, um texto muito grande pode estourar a largura da tela. Com Expanded, o Flutter tenta encaixar o conteúdo dentro do espaço disponível, mas o Expanded só funciona dentro de widgets do tipo flexível, principalmente com:
- Column
- Row
- Flex
Não faz sentido colocar Expanded solto dentro de um Center, por exemplo.
Entrada de dados
No Flutter, a entrada de dados normalmente começa com o widget TextField. Ele cria um campo onde a pessoa pode digitar texto, parecido com um input em HTML ou um Entry em algumas bibliotecas gráficas do Python.
TextField()
O código acima já cria um campo de texto na tela, mas ainda não temos uma forma organizada de pegar o valor digitado. Para isso, usamos o TextEditingController.
TextField
O TextField é o widget usado para digitar texto.
TextField(
decoration: InputDecoration(
labelText: 'Nome do servidor',
),
)
O labelText mostra uma descrição do campo. Na tela, isso aparece como um campo com o texto: Nome do servidor. O TextField cuida da parte visual e da digitação, mas sozinho ele não guarda o valor de um jeito fácil para usar depois.
TextEditingController
Já o TextEditingController é o objeto usado para controlar o conteúdo de um TextField. Com ele nós podemos:
- Ler o texto digitado
- Alterar o texto do campo
- Limpar o campo
- Acompanhar mudanças
Exemplo:
final nomeController = TextEditingController();
Depois ligamos esse controller ao TextField:
TextField(
controller: nomeController,
decoration: InputDecoration(
labelText: 'Nome do servidor',
),
)
Agora o campo está conectado ao controller. Para ler o texto digitado:
String nome = nomeController.text;
A propriedade .text guarda o valor atual do campo.
dispose
Quando usamos TextEditingController, estamos criando um objeto que fica ligado ao TextField.
final nomeController = TextEditingController();
final ipController = TextEditingController();
Esses controllers não são apenas variáveis comuns com texto. Eles acompanham o campo enquanto a tela existe, guardam o valor digitado e também podem manter recursos internos usados pelo Flutter para controlar aquele campo.
Por isso, quando a tela deixa de existir, os controllers também precisam ser finalizados. Isso é feito no método dispose():
@override
void dispose() {
nomeController.dispose();
ipController.dispose();
super.dispose();
}
O dispose() faz parte do ciclo de vida de um StatefulWidget. Ele é chamado automaticamente pelo Flutter quando aquele estado será removido da tela e não será mais usado. No caso da tela de cadastro, a ideia é:
- A tela foi criada
- Os controllers foram criados
- Os campos usaram esses controllers
- A tela foi fechada
- Os controllers precisam ser liberados
O build() pode rodar várias vezes durante a vida da tela, porque ele reconstrói a interface quando algo muda. O dispose(), por outro lado, roda apenas no final, quando o estado daquela tela está sendo destruído.
Por isso os controllers ficam fora do build():
class _CadastroServidorState extends State<CadastroServidor> {
final nomeController = TextEditingController();
final ipController = TextEditingController();
@override
Widget build(BuildContext context) {
...
}
}
Eles pertencem ao estado da tela, não à construção visual de cada renderização. Se fossem criados dentro do build(), poderiam ser recriados várias vezes sem necessidade.
Quando a tela acaba, o Flutter chama:
dispose()
E dentro dele liberamos o que foi criado manualmente:
nomeController.dispose();
ipController.dispose();
Depois chamamos:
super.dispose();
Essa chamada permite que a classe pai também finalize o que ela precisa finalizar. Como a nossa classe herda de State, o Flutter também tem uma parte interna para limpar.
A comparação com Python seria algo como abrir um arquivo e depois fechar:
arquivo = open("log.txt")
# usa o arquivo
arquivo.close()
No Flutter, a lógica é parecida:
final nomeController = TextEditingController();
// usa o controller no TextField
nomeController.dispose();
Não é o TextField que precisa ser finalizado manualmente, porque ele é apenas um widget da interface. Quem precisa ser liberado é o TextEditingController, porque ele foi criado pela tela e continua existindo enquanto o estado da tela existir.
Lendo o texto sem controller
Também existe outra forma, usando onChanged:
TextField(
onChanged: (valor) {
print(valor);
},
)
Nesse caso, a função roda toda vez que o texto muda. Isso pode ser útil para busca em tempo real, filtro de lista ou validação enquanto a pessoa digita.
Mas para formulário simples, como cadastro de servidor, o TextEditingController costuma ser mais prático, porque podemos ler tudo apenas quando o botão de salvar for clicado.
Botões
No Flutter, botões também são widgets, eles são usados quando a interface precisa executar alguma ação depois de um clique, como salvar um cadastro, alterar o status de um servidor, limpar campos ou navegar para outra tela.
Neste tópico, o foco é entender dois botões básicos:
- ElevatedButton
- TextButton
E também como executar uma função quando o botão for clicado.
ElevatedButton
O ElevatedButton é um botão com mais destaque visual. Ele normalmente aparece com fundo preenchido e é usado para ações principais da tela.
ElevatedButton(
onPressed: salvarServidor,
child: const Text('Salvar'),
)
Esse código vai criar um botão destacado, quando clicar, vai executar salvarServidor e dentro do botão, mostrar o texto Salvar.
O onPressed define a ação do botão, enquanto que o child define o conteúdo visual do botão, que nesse caso é o conteúdo Text.
TextButton
O TextButton é um botão mais simples, geralmente sem fundo preenchido. Ele é usado para ações secundárias, como cancelar, limpar, voltar ou abrir uma opção menos importante.
TextButton(
onPressed: limparCampos,
child: const Text('Limpar'),
)
A ideia é a mesma do ElevatedButton, a diferença principal é apenas visual.
Executando uma ação ao clicar
Para executar algo ao clicar, passamos uma função para onPressed.
void salvarServidor() {
print('Servidor salvo');
}
Depois usamos essa função no botão:
ElevatedButton(
onPressed: salvarServidor,
child: const Text('Salvar'),
)
Repare que usamos:
onPressed: salvarServidor
E não:
onPressed: salvarServidor()
Isso é muito importante, quando usamos sem parênteses, estamos passando a função para o botão executar depois, quando houver clique.
Quando usamos com parênteses, estamos chamando a função imediatamente enquanto a tela está sendo construída.
onPressed: salvarServidor()
Botão desabilitado
Se onPressed for null, o botão fica desabilitado.
ElevatedButton(
onPressed: null,
child: const Text('Salvar'),
)
Isso é útil quando uma ação ainda não pode ser executada. Por exemplo, um botão de salvar poderia ficar desabilitado enquanto os campos obrigatórios estão vazios.
Usando função anônima
Também podemos passar uma função diretamente no botão.
ElevatedButton(
onPressed: () {
print('Botão clicado');
},
child: const Text('Testar'),
)
Esse formato é útil quando a ação é pequena ou quando precisa chamar uma função passando argumentos.
ElevatedButton(
onPressed: () {
alterarStatus('Inativo');
},
child: const Text('Inativar'),
)
Nesse caso, usamos uma função anônima porque queremos chamar alterarStatus passando o valor 'Inativo'. Se escrevêssemos assim:
onPressed: alterarStatus('Inativo')
A função seria executada na hora da construção da tela, não no clique, por isso usamos:
onPressed: () {
alterarStatus('Inativo');
}
Isso significa que, quando clicar, vai executar esse bloco de código.
Listas na tela
No Flutter, ListView é usado para mostrar uma lista de widgets na tela. Isso é útil quando temos vários itens parecidos, como:
- servidores cadastrados
- usuários
- produtos
- logs
- contas
- backups
Por exemplo, em um app de controle de servidores, podemos ter uma lista assim:
- Servidor Web - 192.168.0.10 - Ativo
- Servidor Banco - 192.168.0.20 - Ativo
- Servidor Backup - 192.168.0.30 - Inativo
No Flutter, cada item dessa lista também será um widget.
ListView
O ListView cria uma lista rolável.
ListView(
children: const [
Text('Servidor Web'),
Text('Servidor Banco'),
Text('Servidor Backup'),
],
)
Aqui usamos children, porque estamos passando uma lista pronta de widgets. O ListView já permite rolagem, mas se a lista tiver muitos itens e não couber na tela, podemos rolar para baixo.
Exemplo com ListTile
Para listas, é comum usar ListTile, que é um widget pronto para mostrar um item com título, subtítulo e informação lateral.
ListView(
children: const [
ListTile(
title: Text('Servidor Web'),
subtitle: Text('IP: 192.168.0.10'),
trailing: Text('Ativo'),
),
ListTile(
title: Text('Servidor Banco'),
subtitle: Text('IP: 192.168.0.20'),
trailing: Text('Ativo'),
),
ListTile(
title: Text('Servidor Backup'),
subtitle: Text('IP: 192.168.0.30'),
trailing: Text('Inativo'),
),
],
)
Nesse caso:
- title → texto principal
- subtitle → texto secundário
- trailing → conteúdo no lado direito
O ListTile não é obrigatório, mas ajuda bastante para listas simples.
Quando usar ListView?
Usamos o ListView direto quando a lista é pequena, fixa e conhecida no código, como:
- menu com poucas opções
- lista fixa de configurações
- lista pequena de botões
- itens estáticos de uma tela
ListView(
children: const [
ListTile(title: Text('Perfil')),
ListTile(title: Text('Configurações')),
ListTile(title: Text('Sobre')),
],
)
Aqui a lista provavelmente não vai mudar muito. Então ListView com children é simples e suficiente.
ListView.builder
O ListView.builder é usado quando a lista vem de dados. Em vez de escrever cada item manualmente, criamos uma lista de dados e o Flutter monta os widgets conforme necessário.
final servidores = [
'Servidor Web',
'Servidor Banco',
'Servidor Backup',
];
Agora usamos ListView.builder:
ListView.builder(
itemCount: servidores.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(servidores[index]),
);
},
)
A ideia é:
- itemCount → quantos itens existem na lista
- itemBuilder → como cada item será montado
- index → posição atual da lista
Se index for 0, usamos o primeiro servidor, se index for 1, usamos o segundo, se index for 2, usamos o terceiro.
ListView dentro de Column
Um erro comum é colocar ListView dentro de Column sem limitar o espaço. Exemplo ruim:
Column(
children: [
Text('Servidores cadastrados'),
ListView.builder(
itemCount: servidores.length,
itemBuilder: (context, index) {
return Text(servidores[index].nome);
},
),
],
)
Isso pode dar erro porque a Column organiza widgets na vertical, mas o ListView tenta ocupar todo o espaço possível. O Flutter precisa saber qual altura aquela lista pode usar. Para resolver, usamos Expanded:
Column(
children: [
const Text('Servidores cadastrados'),
Expanded(
child: ListView.builder(
itemCount: servidores.length,
itemBuilder: (context, index) {
final servidor = servidores[index];
return ListTile(
title: Text(servidor.nome),
subtitle: Text('IP: ${servidor.ip}'),
);
},
),
),
],
)
A ideia é:
- Text → ocupa só o espaço necessário
- Expanded → entrega o espaço restante para o ListView
- ListView → rola dentro desse espaço
Isso é muito comum em telas com cabeçalho, campo de busca, botão e lista.
Quando usar cada um
Use ListView com children quando a lista é pequena e fixa:
ListView(
children: const [
ListTile(title: Text('Configurações')),
ListTile(title: Text('Sobre')),
ListTile(title: Text('Sair')),
],
)
Use ListView.builder quando a lista vem de dados:
ListView.builder(
itemCount: servidores.length,
itemBuilder: (context, index) {
final servidor = servidores[index];
return ListTile(
title: Text(servidor.nome),
);
},
)
Na prática, para aplicação com cadastro, banco SQLite ou dados vindos de API, ListView.builder é o mais comum.