Flutter - alandrade21/docsCompartilhados GitHub Wiki

Instalação

A instalação do ecossistema flutter é feita seguindo as instruções da página oficial.

Atualizações do flutter são feitas pelo comando flutter upgrade.

No vsCode usa a flutter extension.

A documentação pode ser encontrada em Flutter documentation.

Pacotes para flutter podem ser encontrados em pub.dev. A instalação pode ser feita via flutter pub add <nome-do-pacote>. Esse comando baixa o pacote e insere a entrada do pacote no pubspec.yaml na seção dependencies.

Novo projeto

Cria uma pasta (nomes separados por underscore) como raiz dos projetos.

flutter create <nome_projeto>

Esse comando cria a pasta nome_projeto junto com o novo projeto.

Os arquivos de código ficam na pasta /lib. O arquivo principal é o main.dart.

pubspec.yaml é onde fica a declaração dos pacotes e recursos em uso no projeto.

Debug

Mensagens de erro completas podem ser encontradas no Debug Console, no painel inferior de VsCode.

Além disso, via command palette, é possível abrir Flutter: Open DevTools. Principalmente utilizar a opção que abre o devtools num browser.

Linguagem Dart

Nome dos arquivos

Se a classe se chama GradientContainer, o arquivo que a contém deve se chamar gradient_container.dart.

Formatando o código

Coloca uma vírgula depois de cada fecha parenteses, com exceção do último (que tem o ponto e vírgula depois). Com isso o código é formatado de forma automática (ou utilizando o comando format do vscode via command palette menu - crtl + shift + p).

Impressão no console

Usa o método print() passando a mensagem como parâmetro. Imprime no debug console.

Named parameters

void nome_function({int param1, required int param2}) {...}
...
nome_function(param2: 345);

Wrapping widgets

Durante a codificação vai precisar fazer o wrapping de um widget por outro.

Dá pra fazer isso usando botão direito no widget. Opção refactor. Opção wrap with widget ou direto wrap with com o widget que quiser.

Listas e Mapas

Listas são criadas com [] com elementos separados por vírgula.

Elementos de mapas são criados com {} com chave e valor separados por :. Então um mapa M<String, String> teria seus elementos criados com {'chave':'valor'}.

Variáveis

Uma variável pode ser criada com var ao invés de um tipo. Neste caso, o tipo da variável é inferido pela sua inicialização. Mas se a variável não for imediatamente inicializada, ela fica com o tipo dynamic. Não é recomendado esse tipo de uso. Variáveis que não são imediatamente inicializadas devem ter seu tipo definido em sua declaração.

Variáveis que tem um tipo mas não são imediatamente inicializadas geram um erro, indicando que seu conteúdo pode ser nulo. Para dizer que null é um valor aceitável, o tipo deve terminar com uma ?: Aligment? startAlignment;.

Ao passar uma variável como parâmetro para uma função, o compilador pode reclamar que aquele valor pode estar nulo. Se vc tem certeza que o valor nunca será nulo, coloque um ponto de exclamação, !, após o nome da variável:

Exemplo(title: _meuTitle!);

Uma variável que será inicializada antes do seu primeiro uso (no initState, por exemplo), pode ser declarada com late ao invés de var. Neste caso ainda tem que ter um tipo:

late AnimationController _animationController;

Const

Declara coisas que são constantes em tempo de compilação. Por isso podem ser reutilizadas sempre que aparecerem no código.

Constantes que não são constantes em tempo de compilação, mas em tempo de execução, devem ser declaradas com final.

Checagem de nulidade

O operador ?? faz essa checagem de nulidade:

<variável> ?? <o que fazer se for nula>

Construtores

Com lista de inicialização no construtor.

class MinhaClasse {
  MinhaClasse(String dado): this.dado = dado;

  String dado;
  ...
}

Uma maneira mais curta de escrever a mesma coisa.

class MinhaClasse {
  MinhaClasse(this.dado);

  String dado;
  ...
}

Métodos construtores podem ser criados com comportamentos padrão:

const GradientContainer(this.color1, this.color2, {super.key});

const GradientContainer.purple({super.key})
    : this.color1 = Colors.deepPurple, // : define uma lista de inicialização
      this.color2 = Colors.indigo;

final Color color1;
final Color color2;

Imagens e Assets

Cria uma pasta assets na raiz do projeto e copia as imagens para a subpasta images (os nomes não são padrão).

Vai no arquivo pubspec.yaml, acha a seção assets (descomenta se estiver comentada), e insere os caminhos relativos para as imagens como itens aninhados ao assets (iniciando com -).

assets:
  - assets/images/image1.png'

Para acessar essas imagens usa o widget Image na forma Image.asset(name), onde o name é o caminho relativo para a imagem, o mesmo caminho fornecido no arquivo pubspec.yaml.

Esse construtor tem um parâmetro width que permite ajustar o tamanho da imagem.

Injetar valores em strings

Usa $nome_variavel dentro da string:

var diceRoll = //algum valor
activeDiceImage = 'assets/images/dice-$diceRoll.png';

Stateless widgets

Serve quando vc precisa receber algum dado e cuspir um widget.

class MeuWidget extends StatelessWidget {
  MeuWidget(this.color1, this.color2, {super.key});

  MeuWidget.purple({super.key}):
    color1 = Colors.deepPurple,
    color2 = Colors.indigo;
  ...
  final Color color1; // constantes. Podia ser const. const é constante em tempo de 
  final Color color2; // compilação.
  ...
  @override
  Widget build(BuildContext context) {
    return //<<código de construção do widget>>
  }
}

Stateful widget

Serve quando uma variável interna muda e, por isso, a renderização da UI deve mudar.

class MeuWidget extends StatefulWidget {

  const MeuWidget({super.key});
  ...
  @override
  State<MeuWidget> createState() {
    return _MeuWidgetState();
  }
}

class _MeuWidgetState extends State<MeuWidget> {

  var controle = "x";

  void trocaControle() {
    setState(() { // Set state atualiza a UI com as alterações de estado.
      controle = "y";
    });
  }

  @override
  Widget build(BuildContext context) {
    return //<<código de construção do widget>>
  }
}

Elementos cujo nome começa com underscore são elementos privados.

Stateful widgets tem um ciclo de vida que dispara automaticamente alguns métodos:

  • initState(): Executa logos após a inicialização do objeto.
  • build(): Executa uma única vez logo após o término da inicialização do objeto e toda vez que setState() é chamado.
  • dispose(): Executado logo antes da deleção do objeto.

Quando é necessário receber uma variável no widget e utilizá-la na classe interna:

class MeuWidget extends StatefulWidget {

  const MeuWidget({super.key, required this.dadoRecebido,});

  final String dadoRecebido;
  ...
  @override
  State<MeuWidget> createState() {
    return _MeuWidgetState();
  }
}

class _MeuWidgetState extends State<MeuWidget> {

  ...

  void trocaControle() {
    widget.dadoRecebido; // Acessa elementos definidos no widget assim

    setState(() { // Set state atualiza a UI com as alterações de estado.
      ...
    });
  }

  @override
  Widget build(BuildContext context) {
    return //<<código de construção do widget>>
  }
}

Mantenha esse tipo de widget o menor possível, somente com aquilo que, de fato, precisa mudar. Todo o resto vai para um stateless.

Lifting State

Imagine que temos uma tela principal, StartScreen, e a partir dela precisamos navegar para uma segunda tela, SecondScreen.

// main.dart
void main() {
  runApp(const MeuApp());
}

```dart
//meu_app.dart
class MeuApp extends StatefulWidget {

  const MeuApp({super.key});
  ...
  @override
  State<MeuApp> createState() {
    return _MeuAppState();
  }
}

class _MeuAppState extends State<MeuApp> {

  Widget activeScreen = const StartScreen();

  void switchScreen() {
    setState(() { 
      activeScreen = const SecondScreen();
    });
  }

  @override
  Widget build(context) {
    return MaterialApp(
      home: Scaffold(
        body: Container(
          ...
          child: activeScreen,
        ),
      ),
    );
  }
}

Essa é uma maneira de renderização condicional.

Para fazer a troca de telas, temos que as duas telas dependem de um mesmo estado (qual é a tela visível). A solução para isso é criar um widget que gerencie esse estado. Esse widget é MeuApp, que faz a renderização condicional.

Para resolver esse problema é necessário fazer com que StartScreen tenha acesso ao método switchScreen. Para isso, devemos passar uma referência (um ponteiro) desse método para StartScreen, fazendo algo parecido com alterar a linha Widget activeScreen = const StartScreen(); para Widget activeScreen = const StartScreen(switchScreen);.

Vamos ver como fica a implementação de StartScreen.

class StartScreen extends StatelessWidget {

  const StartScreen(this.trocaTela, {super.key});

  final void Function() trocaTela; // O tipo void Function() informa que aqui é esperada
                                   // uma função sem argumentos que retorna void.

  @override
  Widget build(context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ...
          OutlinedButton.icon(
            onPressed: trocaTela,
            }),
            ...
          )
        ];
      ),
    );
  }
}

Nesse ponto, não é possível fazerWidget activeScreen = const StartScreen(switchScreen);. Vai dar um erro. Não é possível inicializar uma variável de instância com um ponteiro para um método da própria classe. Para resolver esse problema usamos o método initState.

//meu_app.dart
class _MeuAppState extends State<MeuApp> {

  Widget? activeScreen;

  @override
  void initState() {
    super.initState();
    activeScreen = StartScreen(switchScreen);
  }

  void switchScreen() {
    setState(() { 
      activeScreen = const SecondScreen();
    });
  }

  ...

Funções de Listas

Map

É uma função presente nos objetos de lista que permite converter cada elemento de uma lista em alguma outra coisa.

Assim, digamos que temos uma lista de strings onde cada elemento é uma opção de resposta a uma pergunta, e queremos que cada elemento dessa lista de respostas seja um botão a ser colocado na tela:

perguntaCorrente.respostas.map((item){
  return BotaoResposta(item);
})

map retorna um objeto do tipo Iterable. Para obter os elementos em forma de lista, usa o operador ...: ...perguntaCorrente.respostas.map(...).

Where

Retorna uma nova lista filtrando objetos da lista original.

final correctQuestions = summaryData.where((data){
  return data['user_answer'] == data['correct_answer'];
});

Pode ser escrita como uma arrow function:

summaryData.where(
  (data) => data['user_answer'] == data['correct_answer'],
)

Fontes

A página Use a custom font explica como inserir um arquivo de fontes e usar no projeto.

Ou é possível utilizar google fontes usando o pacote google_fonts. As fontes podem ser pesquisadas na página do google fonts.

Lato é uma boa fonte.

Type Casting

Object meuObjeto;
...
(meuObjeto as int) + 1;

Getters e Setters

String _minhaPropriedade;
...
String get minhaPropriedade {
  return _minhaPropriedade;
}

Não acessa como um método, mas como uma variável var x = minhaPropriedade;.

Enums

enum Categoria {food, travel, leisure, work}

Funções de Data

Dta atual:

DateTime.now();

Para formatação de datas usa o pacote de terceiros Intl. Usa a classe DateFormat.

Keys

São usadas para conectar a árvore de widgets com a árvore de elementos, quando existirem estados ligados aos elementos e for necessário mudar a ordem dos widgets. Nesse caso é necessário setar uma key nos dados do widget.

É uma boa prática sempre aceitar {super.key} no construtor dos widgets. Isso facilita a vida qdo uma key precisar ser utilizada.

class MeuWidget extends StatelessWidget {
  const MeuWidget(this.text, {super.key});

  final String text;

  @override
  Widget build (context) {
    ...
  }
}
...

// Criando um novo MeuWidget
var texto = 'lalala';

MeuWidget(
  key: ValueKey(texto), // O valor usado aqui deve ser um identificador único.
  texto,
)

Outra opção é usar um key: ObjectKey(objeto) para criar a chave, passando todo um objeto de identificador.

Navegação entre screens

class ScreenA extends StatelessWidget {
  const ScreenA({super.key});

  void _gotToB(BuildContext context) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (ctx) => ScreenB(),
      ),
    );
  }
}

Para substituir a janela ao invés de empilhar, usa pushReplacement ao invés de push.

Para retornar valores entre janelas, digamos, de uma tela de parâmetros de configuração para a tela anterior, ao apertar o voltar, encapsula os componentes da tela com o widget abaixo:

PopScope(
  canPop: false,
  onPopInvokedWithResult: (bool didPop, dynamic result) {
    if(didPop) return;
      Navigator.of(context).pop({ // Valores a serem retornados
        valorA: _variavelValorA,
        valorB: _variavelValorB,
      });
    },
    child: Column(...) // componentes que manipulam os valores acima
),

Para receber esses valores, o empilhamento da janela muda:

final result = await Navigator.of(context).push<Map<string, bool>>(...);

Widegets

Há um catálogo de widgets na documentação do flutter.

Para organizar, separa widgets de tela (chamados screens) de widgets que contém componentes que formarão uma tela. Guarde esses dois em pastas separadas (widgets e screens).

Material design

Flutter por default usa google material design.

No main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      theme: ThemeData(useMaterial3: true), // Em versões mais novas, que já usam material 3 
                                            // por padrão, não usa isso.
      home: Scaffold(
        backgroundColor: Colors.deepPurple,
        body: Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }
}

Para mais detalhes sobre uso de temas, veja a seção Temas.

Scaffold

Fornece vários suportes ao design da aplicação.

Barra no topo da aplicação:

Scaffold(
  appBar: AppBar(
    title: cont Text('Meu título'),
    actions: [ // lista de botões no topo da aplicação, tipicamente.
      IconButton(
        onPressed: (){},
        icon: Icon(Icons.add),
      ),
    ],
  ),
  body: ...
)

Barra na base da aplicação:

class ...
...
int _selectedPageIndex = 0;
...
void _selectPage(int index) {
  setState(() {
    _selectedPageIndex = index;
  });
}
...
Scaffold(
  appBar: AppBar(...),
  body: ...,
  bottomNavigationBar: bottomNavigationBar(
    onTap: (index) {...},
    currentIndex: _selectedPageIndex,
    items: const [
      bottomNavigationBarItem(icon: Icon(Icons.set_meal), label:''),
      bottomNavigationBarItem(icon: icon, label:''),
    ],
  ),
)

Side drawer:

Scaffold(
  appBar: AppBar(...),
  drawer: Drawer(
    child: Column(children: [
      DrawerHeader(
        padding: const EdgeInsets.all(20),
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [
              Theme.of(context).colorScheme.primaryContainer,
              Theme.of(context).colorScheme.primaryContainer.withOpacity(0.8),
            ],
            begin: Alignment.topLeft,
            end: Aligment.bottomRight,
          ),
        ),
        child: Row(
          Children: [
            Icon(
              Icons.fastfood, 
              size: 48, 
              color: Theme.of(context).colorScheme.primary,
            ),
            const SizedBox(width: 18),
            Text(
              'Meu título',
              style: Theme.of(context).textTheme.titleLarge!.copyWith(
                color:Theme.of(context).colorScheme.primary,
              ),
            ),
          ],
        ),
    ],),
    ListTile(
      leading: Icon(
        Icons.restaurant,
        size: 26, 
        color: Theme.of(context).colorScheme.onBackground,
      ),
      title: Text(
        'Meals',
        style: Theme.of(context).colorScheme.onBackground,
        fontsize:24,
      ),
      onTap: () {},
    ),
    ListTile(
      leading: Icon(
        Icons.settings,
        size: 26, 
        color: Theme.of(context).colorScheme.onBackground,
      ),
      title: Text(
        'Filters',
        style: Theme.of(context).colorScheme.onBackground,
        fontsize:24,
      ),
      onTap: () {},
    ),
  ),
  body: ...,
  bottomNavigationBar: bottomNavigationBar(...),
)

Container

Serve para aplicar um padrão visual a todas as telas da aplicação.

MaterialApp(
  home: Scaffold(
    body: Container(
      decoration: const BoxDecoration(
        gradient: LinearGradient(
          colors: [
            Color.fromARGB(...),
            Color.fromARGB(...),
          ],
          begin: Aligment.topLeft,
          end: Aligment.bottomRight,
        ),
      ),
    child: const StartScreen(), // custom widget
    ),
  ),
)

Também serve para aplicar efeitos em partes da aplicação, por exemplo, números dentro de círculos coloridos:

Container(
  width: 30,
  heigh: 30,
  alignment: Alignment.center,
  decoration: BoxDecoration(
    color: const Color.fromARGB(...),
    borderRadius: BorderRadius.circular(100),
  ),
  child: Text(...),
)

Outro efeito que fica bom é fazer um gradiente da mesma cor com opacidades diferentes. Por exemplo:

gradient: LinearGradient(
  colors: [
    Color.fromARGB(...).withOpacity(0.55),
    Color.fromARGB(...).withOpacity(0.9),
  ],
  begin: Aligment.topLeft,
  end: Aligment.bottomRight,
),

InkWell - Fazendo com que elementos de um container sejam clicáveis

InkWell (
  onTap: (){}, // Isso aqui é um listener
  splashColor: Theme.of(context).primaryColor,
  borderRadius: BorderRadius.circular(16),
  child: Container(...)
)

### Center

Ocupa toda a tela, e costuma ser um bom primeiro componente de uma arvore, dentro do Scaffold/Container.

```dart
Center(
  child: ...,
)

Column

Column(
  mainAxisSize: MainAxisSize.min, // Provoca a centralização vertical do conteúdo.
  children: [
    ... // Lista de widgets a serem empilhados
  ],
);

Para centralização horizontal, coloca Column dentro de um Center.

Center(
  child: Column(
    ...
  ),
);

Ou dentro de uma SizedBox com a config abaixo:

SizedBox(
  width: double.infinity,
  child: Column(
    ...
  ),
);

Agora imagine uma coluna contendo botões. Para que a largura de cada botão ocupe toda a largura da coluna faz:

Column(
  mainAxisSize: MainAxisSize.min, // Provoca a centralização vertical do conteúdo.
  crossAxisAlignments: CrossAxisAlignment.stretch,
  children: [
    ... // Lista de widgets a serem empilhados
  ],
);

Para colocar espaços entre a coluna e as bordas da tela, coloca a coluna dentro de um Container:

Container(
  margin: const EdgeInsets.all(40),
  child: Column(
    ...
  ),
);

Column dentro de column dá problema. Para evitar o problema tem que usar Expanded:

Column(
  children: [
    ...,
    Expanded(
      child: Column(...),
    ), 
  ],
);

Column renderiza todos os elementos que aparecerão na coluna em tempo de criação do objeto, mesmo que nem todos os elementos estejam visíveis na tela. Isso não é adequado para mostrar uma lista com muitos elementos (ou com uma quantidade desconhecida de elementos). Nestes casos use o widget ListView.

Row

Row(children:[
  Text(...),
  Expanded( // Column dentro de Row tem que ficar dentro desse cara pra não dar pau.
    child: Column(...),
  ),
],)

SizedBox - Espaçamento entre Widgets

Pode ser feito com a propriedade padding presente na maior parte dos widgets.

padding: EdgeInsets.all(10),

padding: const EdgeInsets.only(top: 10,),

O mais comum é usar o widget SizedBox entre os widgets onde o espaço deve ficar.

children: [
  Image.asset(
    ...
  ),
  const SizedBox(
    height: 20, // Tb pode setar width.
  ),
  TextButton(
    ...
  ),
]

SizedBox - Scrollable Area

SizedBox(
  height: 300,
  child: SingleChildScrollView(
    child: Column(...),
  ),
)

Spacer

Row(
          children: [
            Text('\$${despesas.amount.toStringAsFixed(2)}'),
            const Spacer(), // Pega todo o espaço que sobrar, empurrando o próximo conteúdo o máximo 
                            // possível para o fum da linha
            Row(
              children: [
                Icon(Icons.alarm),
                const SizedBox(width: 8),
                Text(despesas.date.toString()),
              ],
            ),
          ],
        ),

Expanded

Sempre que for utilizar uma lista dentro de outra lista dá problema de renderização. Para resolver esse problema, usa esse widget.

Row(children:[
  Text(...),
  Expanded( // Column dentro de Row tem que ficar dentro desse cara pra não dar pau.
    child: Column(...),
  ),
],)

ou

Column(
  children: [
    ...,
    Expanded(
      child: Column(...),
    ), 
  ],
);

TextField dentro de Row tb precisa de Expanded.

O problema aqui é chamado de size constraint. Por exemplo, um Column ocupa de altura o máximo que ele puder, e de largura só o suficiente para acomodar os widgets filhos. Isso significa que o Column tem que estar dentro de um parent que restrinja a altura, senão ele fura os limites da tela. Por isso a raiz da árvore de widget é um Scaffold, cuja altura é a altura máxima do aparelho, e a largura é a largura máxima do aparelho.

Nessa situação, a altura do Column será limitada pela altura do Scaffold de forma que a altura do Column será a altura máxima do aparelho.

No caso de um widget que ocupe o máximo espaço possível, como o Column, dentro de outro que também tem essa mesma característica, o resultado é o estouro dos limites da tela, e a UI não é renderizada.

O uso do Expanded resolve esse problema, fornecendo os limites necessários para a renderização dos widgets que ocupam o máximo espaço possível.

Text

Text(
  'Meu texto',
  style: TextStyle(
    color: Colors.white,
    fontSize: 24,
  ),
  textAlign: TextAlign.center,
)

Image

Image.asset(
  'assets/images/imagem.png', // Tem que carregar a imagem no pubspec.yaml
  width: 300,
  color: const Color.fromARGB(150, 255, 255, 255), // Essa é uma técnica pra fazer uma imagem 
                                                   // ficar translúcida. Qto maior o número da 
                                                   // transparência, maior a solidez.
)

Buttons

TextButton

TextButton(
  onPressed: fazAlgumaCoisa, // Ponteiro para uma função definida na classe. Pode ser (){}.
  style: TexButton.styleFrom(
    foregroundColor: Colors.white,
    textStyle: const TextStyle(
      color: Colors.white, // exemplo, é redundante com o de cima.
      fontSize: 28,
    )
  ),
  child: const Text('Button Text'),
);

OutlinedButton com ícone

OutlinedButton.icon(
  onPressed: fazAlgumaCoisa, // Ponteiro para uma função definida na classe. Pode ser (){}.
  style: OutlinedButton.styleFrom(
    foregroundColor: Colors.white,
    textStyle: const TextStyle(
      fontSize: 28,
    )
  ),
  icon: const Icon(Icons.arrow_right_alt),
  label: const Text('Button Text'),
);

ElevatedButton com bordas arredondadas

ElevatesButton(
  onPressed: fazAlgumaCoisa, // Ponteiro para uma função definida na classe. Pode ser (){}.
  style: ElevatedButton.styleFrom(
    padding: const EdgeInsets.symmetric( // Espaços em volta do texto do botão
      vertical: 10,
      horizontal: 40,
    ),
    foregroundColor: Colors.white,
    backgroundColor: Colors.fromARGB(...),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(40),
    ),
  ),
  child: const Text('Button Text'),
);

DropdownButton

MeuEnum _selectedEnum = MeuEnum.algumaCoisa;

...

DropdownButton(
  items: MeuEnum.values.map(
    (meuEnum) => DropdownMenuItem(
      value: meuEnum,
      child: Text(
        meuEnum.name,
      ),
    ),
  ).toList(),
  value: _selectedEnum,
  onChange: (value) {
    ...

    if (value == null) {
      return;
    }
    setState(() {
      _selectedEnum = value;
    });
  },
)

ListView

Funciona como uma column, mas já vem scrollable por padrão. Ao usar como abaixo, vai renderizar todos os elementos na carga do widget, como uma Column, o que não é adequado para muitos elementos ou para uma quantidade desconhecida deles.

ListView(
  children: [
    ...
  ],
)

Para uma quantidade muito grande ou desconhecida de elementos, usa como abaixo:

ListView.builder(itemBuilder: (ctx, index){
  return ...
})

ou

ListView.builder(
  itemCount: despesas.length, 
  itemBuilder: (ctx, index) => Text(...),
)

Se um item da lista puder ser removido com uma puxada lateral, veja o elemento Dismissable.

GridView

GridView(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2 // Coloca duas colunas na grid.
    childAspectRatio: 3/2,
    crossAxisSpacing: 20,
    mainAxisSpacing: 20,
  ),
  children: [ 
    //widgets da grid
  ],
)

Dismissable

Se um item da lista puder ser removido com uma puxada lateral:

ListView.builder(
  itemCount: despesas.length, 
  itemBuilder: (ctx, index) => Dismissable(
    key: ValueKey(despesas[index]), // forma de construir um identificador único para o elemento
    onDismissed: (direction) { // direction é a direção do arraste usado pelo usuário.
      // aqui remove despesas[index] do banco.
    },
    child: ..., 
  ),
)

Se quiser mostrar com a opção de desfazer a remoção por puxada, usa uma snack bar.

Padding

Padding(
  padding: const EdgeInsets.all(16),
  child: ...,
)
Padding(
  padding: const EdgeInsets.symmetric(
    horizontal: 20,
    vertical: 16,
  ),
  child:...,
)
Padding(
  padding: const EdgeInsets.fromLTRB(14, 48, 16, 16),
  child: ...,
)

Card

Colocar espaços entre os elementos de um card, coloca o elemento dentro de um Padding.

Card(
  child: Padding(
    margin: const EdgeInsets.all(8),
    shape: RoundRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    clipBehavior: Clip.hardEdge, // Força a aplicação do shape no card.
    elevation: 2,
    padding: cont EdgeInsets.symmetric(
      horizontal: 20,
      vertical: 16,
    ),
    child: Column(
      children: [
        Text(despesas.title),
        const SizedBox(heigh: 4),
        Row(
          children: [
            Text('\$${despesas.amount.toStringAsFixed(2)}'),
            const Spacer(), // Pega todo o espaço que sobrar, empurrando o próximo conteúdo o máximo 
                            // possível para o fum da linha
            Row(
              children: [
                Icon(Icons.alarm),
                const SizedBox(width: 8),
                Text(despesas.date.toString()),
              ],
            ),
          ],
        ),
      ],
    ),
  ),
)

Modal Overlay

Mostra um com o método showModalBottomSheet(context: context, builder: builder).

showModalBottomSheet(
  context: context, 
  builder: (ctx) => // Conteúdo da modal
)

Para fechar o overlay, em algum lugar do código do overlay chama Navigator.pop(context).

No showModalBottomSheet há um parâmetro isScrollControlled que se setado para true faz o overlay ocupar toda a tela. Se, nessa situação, alguns elementos da tela ficarem cobertos por elementos do aparelho, como câmera, por exemplo, é só colocar um padding em volta do widget construído dentro do overlay, algo como:

Padding(
  padding: const EdgeInsets.fromLTRB(14, 48, 16, 16),
  child: ...,
)

Dialogs

showDialog(
  context: context,
  builder: (ctx) => AlertDialog(
    title: const Text('Título da dialog'),
    content: ..., // widget de conteúdo
    actions: [
      TextButton(
        onPressed: () {
          Navigator.pop(ctx);
        },
        child: const Text('Okay'),
      ),
    ],
  ),
)

ScaffoldMessenger SnackBar

Faz aparecer uma mensagem informativa flutuante em algum lugar do scaffold.

ScaffoldMessenger.of(context).clearSnackBars(); // Impede o empilhamento de várias mensagens.
ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    duration: const Duration(seconds: 3),
    content: const Text('Coisa foi apagada.'),
    action: SnackBarAction(
      label: 'Desfazer',
      onPressed: () {
        ... // desfaz a remoção, possivelmente com setState().
      },
    ),
  ),
)

### TextField

```dart
final _meuCampoController = TextEditingController();

@override // apenas em statefull widgets
void dispose() {
  _meuCampoController.dispose();
  super.dispose();
}
TextField(
  onChanged: () {}, // Responde a cada tecla digitada
  controller: _meuCampoController, // Forma adequada de obter o valor do campo. 
                                   // Acima apenas um exemplo.
  maxlength: 50,
  decoration: InputDecoration(
    prefixText: '\$ ',
    label: Text(...) // Label do campo
  ),
)

Quando for necessário, faz um _meuCampoController.text para pegar o valor do campo.

DatePicker

Row(
  children: [
    const Text('Selecione a data'),
    IconButton(
      onPressed: _mostrarDatePicker,
      icon: const Icon(
        Icons.calendar_month,
      ),
    ),
  ],
)
void _presentDatePicker() {
  ...
  showDatePicker(
    context: context, 
    initialDate:initialDate, 
    firstDate: firstDate, 
    lastDate: lastDate
  ).then((value) {
    ... // Faz alguma coisa com o valor da data escolido no picker
  },);
  ...
}

Outra forma de trabalhar é com async/await

...
DateTime? _selectedDate;
...
void _presentDatePicker() async{
  ...
  final dataEscolhida = await showDatePicker(
    context: context, 
    initialDate:initialDate, 
    firstDate: firstDate, 
    lastDate: lastDate
  );
  ...
  setState(() {
    _selectedDate = dataEscolhida;
  });
}

A Tela, na versão final:

Row(
  children: [
    const Text(_selectedDate == null ? 'Nenhuma data selecionada': formatter.format(_selectedDate!),),
    IconButton(
      onPressed: _mostrarDatePicker,
      icon: const Icon(
        Icons.calendar_month,
      ),
    ),
  ],
)

A ! no fim de _selectedDate! é para endereçar o erro que diz que a variável não pode ser nula. A ! garante ao compilador que nesse ponto essa variável estará preenchida.

Stack

Serve para colocar widgets empilhados no eixo z.

Stsack(
  children:[// Componentes no início da lista estão na base da pilha
    FadeInImage(
      placeholder: MemoryImage(KTransparentImage), 
      //KTransparentImage vem do pacote transparent_image instalado do repositório
      image: NetworkImage(url),
      fit: BoxFit.cover, // evita q a imagem seja distorcida.
      height: 200,
      width: double.infinity,
    ),
    Positioned( // Diz a posição relativa desse widget sobre o anterior.
      bottom: 0,
      left: 0,
      right: 0,
      child: Container(
        color: Colors.black54, //Coloca um fundo preto levemente transparente atrás do texto
        child: Column(
          children: [
            Text(
              '...',
              maxLines: 2,
              textAlign: TextAlign.center,
              softWrap: true,
              overflow: TextOverflow.ellipsis,
              style: const TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
            const SizedBox(height: 12),
            ...
          ],
        ),
      ),
    ),
  ],
)

ListTile

Widget de conveniência para contar itens de uma lista, sem precisar usar row;

Column(children: [
  ListTile(
    leading: Icon(
      Icons.restaurant,
      size: 26, 
      color: Theme.of(context).colorScheme.onBackground,
    ),
    title: Text(
      'Meals',
      style: Theme.of(context).colorScheme.onBackground,
      fontsize:24,
    ),
    onTap: () {},
  ),
  ListTile(
    leading: Icon(
      Icons.settings,
      size: 26, 
      color: Theme.of(context).colorScheme.onBackground,
    ),
    title: Text(
      'Filters',
      style: Theme.of(context).colorScheme.onBackground,
      fontsize:24,
    ),
    onTap: () {},
  ),
)

SwitchListTile

Widget de conveniência para montar itens de uma lista onde cada item da lista pode ser selecionado.

class...
...
var _glutenFreeFilterSet = false;
...
Column(children: [
  SwitchListTile(
    value: _glutenFreeFilterSet,
    onChanged: (newValue) {
      setState(() {
        _glutenFreeFilterSet = newValue;
      });
    },
    title: Text(
      'Gluten-free',
      style: Theme.of(context).textTheme.titleLarge!.copyWith(
        color: Theme.of(context).colorScheme.onBackground,
      ),
    ),
    subtitle: Text(
      'Little explanation...',
      style: Theme.of(context).textTheme.labelMedium!.copyWith(
        color: Theme.of(context).colorScheme.onBackground,
      ), 
    ),
    activeColor: Theme.of(context).colorScheme.tertiary,
    contentPadding: const EdgeInsets.only(left: 34, right: 22),
  ),
  ...
)

Forms

...
final _formKey = GlobalKey<FormState>();
...
Form(
  key: _formKey;
  child: Column(
    children: [
      TextFormField(
        maxLength: 50,
        decoration: const InputDecoration(
          label: Text('Name'),
        ),
        initialValue: 'alguma coisa',
        validator: (value) {
          if (value == null || 
              value.isEmpty) || 
              value.trim().length <= 1 || 
              value.trim().length > 50){
            return 'Must be between 1 and 50 characters.';
          }
          return null;
        },
        onSaved: (value) {
          // Aqui tira do form e coloca nas variáveis.
          _enteredName = value!;
        }
      ),
      Row(
        crossAxisAlignment: CrossAxisAlignment.end,
        Children: [
          Expanded(
            child: TextFormField(
              decoration: const InputDecoration(
                label: Text('Quantity'),
              ),
              keyboardType: TextInputType.number,
              initialValue: '1',
              validator: (value) {
                if (value == null || 
                    value.isEmpty) || 
                    int.tryParse(value) == null || 
                    int.tryParse(value)! <= 0) {
                  return 'Must be a valid, positive number.';
                }
                return null;
              },
            ),
          ),
          const SizedBox(width: 8),
          Expanded(
            child: DropdownButtonFormField(
              items: [
                for (final category in categories.entries)
                  DropdownMenuItem(
                    value: category.value,
                    child: Row(
                      children: [
                        Container(
                          width: 16,
                          heigh: 16,
                          color: category.value.color,
                        ),
                        const SizedBox(width: 6),
                        Text(category.value.title),
                      ],
                    ),
                  ),
              ], 
              onChanged: (value) {},
            ),
          ),
        ],
      ),
      const SizedBox(heigh: 12),
      Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          TextButton(
            onPressed: (){
              _formKey.currentState!.reset();
            },
            child: const Text('Reset'),
          ),
          ElevatedButton(
            onPressed: (){
              if(_formKey.currentState!.validate()) {
                _formKey.currentState!.save();
              }
            },
            child: const Text('Submit'),
          ),
        ],
      ),
    ],
  ),
)

Temas

Durante a criação da janela principal do app, fizemos algo assim:

return const MaterialApp(
  theme: ThemeData(useMaterial3: true), // Em versões mais novas, que já usam material 3 
                                        // por padrão, não usa isso.
  home: Scaffold(...),
),

O uso de um ThemeData sobrescreve todo o sistema de temas, usando um tema do zero, ou escolhendo um tema padrão, como no caso acima, o materia 3.

Em versões futuras do flutter, material 3 já pode vir como o tema padrão, então e4ssa linha não será necessária, ainda assim, seria mais interessante fazer algo como:

theme: ThemeData().copyWith(useMaterial3: true),

Esse uso não sobrescreve o sistema de temas, ao contrário, ele combina o tema padrão do flutter com as definições em uso no tema escolhido ou configurado, resultando num efeito mais agradável que apenas o tema escolhido puro.

Dessa forma é possível fazer o override de alguns elemento específicos, como, por exemplo:

theme: ThemeData().copyWith(
  useMaterial3: true,
  scaffoldBackgroundColor: Color.RED,
),

Color Scheme

// k é um padrão de prefixo para variáveis globais.
// Color Scheme from seed usa uma cor padrão para suprir todas as variações de cores necessárias
// para os widgets.
var kColorScheme = ColorScheme.fromSeed(seedColor: const Color.fromARGB(255, 96, 59, 181),);

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData.copyWith(
        useMaterial3: true,
        colorScheme: kColorScheme,
        appBarTheme: const AppBarTheme().copyWith(
          backgroundColor: kColorScheme.onPrimaryContainer,
          foregroundColor: kColorScheme.primaryContainer,
        ),
        cardTheme: const CardTheme().copyWith(
          color: kColorScheme.secondaryContainer,
          margin: const EdgeInsets.symmetric(
            horizontal: 16,
            vertical: 8,
          ),
        ),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom( // infelizmente ele não segue o padrão copyWith...
            backgroundColor: kColorScheme.primaryContainer,
          ),
        ),
      ),
      home: const MeuWidget(),
    ),
  );
}

Temas para texto

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData.copyWith(
        useMaterial3: true,
        colorScheme: kColorScheme,
        appBarTheme: const AppBarTheme().copyWith(
          backgroundColor: kColorScheme.onPrimaryContainer,
          foregroundColor: kColorScheme.primaryContainer,
        ),

        ...

        textTheme: ThemeData().textTheme.copyWith(
          titleLarge: ThemeData().textTheme.copyWith(
            fontWeight: FontWeight.normal,
            color: kColorScheme.onSecondaryContainer,
            fontSize: 14,
            // algumas configs não vão funcionar, por exemplo background color, pq ela vem da
            // config de outros widgets, como, por exemplo, o background color da app bar.
          ),
        ),
      ),
      home: const MeuWidget(),
    ),
  );
}

Tema de texto com google fonts:

void main() {
  runApp(
    MaterialApp(
      theme: ThemeData.copyWith(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          brightness: Brightness.dark,
          seedColor: const Color.fromARGB(255, 131, 57, 0),
        ),
        textTheme: GoogleFonts.latoTextTheme(), // Tem que instalar o pacote google_fonts
      ),
      home: const MeuWidget(),
    ),
  );

Estilizando widgets com elementos do tema

Estilização normal:

Text('meu texto', style: TextStyle(...),),

Usando elementos do tema:

Text('meu texto', style: Theme.of(context).textTheme.titleLarge,),

Exemplo de uso:

background: Container(
  color: Theme.of(context).colorScheme.error.withOpacity(0.75),
  margin: EdgeInsets.symmetric(
    horizontal: Theme.of(context).cardTheme.margin!.horizontal,
  ),
),

Dark Mode

var kColorScheme = ColorScheme.fromSeed(seedColor: const Color.fromARGB(255, 96, 59, 181),);

var kDarkColorScheme = ColorScheme.fromSeed(
  brightness: Brightness.dark,
  seedColor: const Color.fromARGB(255, 5, 99, 125),
);

void main() {
  runApp(
    MaterialApp(
      darkTheme: ThemeData.dark().copyWith(
        useMaterial3: true,
        colorScheme: kDarkColorScheme,
      ),
      theme: ThemeData.copyWith(
        ...
      ),
      // themeMode: ThemeMode.system, //default. Não precisa setar.
      home: const MeuWidget(),
    ),
  );
}

Responsividade

Travar a orientação

import 'package:flutter/services.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.portraitUp,
  ])then(() {
    (fn) {
      runApp(...);
    }
  });
}

Update a UI com base no espaço disponíve

@override
Widget build(BuildContext context) {
  final width = MediaQuery.of(context).size.width;

  ...

  return Scaffold(
    ...

    body: width < 600 ? Column(
      ...
    ) : Row(
      ...
    ),
  );
}

Problema com teclado cobrindo UI

@override
Widget build(BuildContext context) {
  final keyboardSpace = MediaQuery.of(context).viewInsets.bottom;
  return SizedBox( // Para evitar um bug estranho depois que o teclado fecha.
    height: double.infinity,
    child: SingleChildScrollView( // Para fazer com que a área fora do teclado seja scrollable.
      child: Padding(
        padding: EdgeInsets.fromLTRB(16, 48, 16, keyboardSpace + 16),
        ...
      )
    )
  );
}

Safe area

Evita a área opcupada por dispositivos que ocupem a tela, como câmeras.

showModalBottomSheet(
  useSafeArea: true;
  ...
);

Layout builder

Usa apenas quando um widget precisa estar ciente do tamanho do parent, e não da tela toda.

@override
Widget build(BuildContext context) {
  return LayoutBuilder(builder: (ctx, constraints) {
    final width = constraints.maxWidth;

    return MeuWidget(
      ...

      if (width >= 600) // sintaxe sem chaves
        Row(...) // Renderiza de um jeito
      else
        Text(...) // Renderiza de outro
    );
  });
}

Descobrir a plataforma

import 'dart:io';

...

if(Platform.isIOS) {
  ...
} else {
  ...
}
⚠️ **GitHub.com Fallback** ⚠️