Seu primeiro app do Flutter

1. Introdução

O Flutter é o kit de ferramentas de interface do Google para criar aplicativos para dispositivos móveis, Web e computadores com uma única base de código. Neste codelab, você vai criar este aplicativo do Flutter:

1d26af443561f39c.gif

O aplicativo gera nomes legais, como "newstay", "lightstream", "mainbrake" ou "graypine". O usuário pode pedir o próximo nome, marcar o atual como um favorito e conferir a lista de nomes favoritos em uma página separada. O app é responsivo em diferentes tamanhos de tela.

O que você vai aprender

  • Noções básicas de como o Flutter funciona.
  • Como criar layouts no Flutter.
  • Como conectar as interações do usuário, como pressionamentos de botão, ao comportamento do app.
  • Como manter o código do Flutter organizado.
  • Como tornar o app responsivo em diferentes telas.
  • Como criar uma aparência consistente no app.

Você vai começar com um scaffolding básico para poder ir direto às partes interessantes.

d6e3d5f736411f13.png

Confira este tutorial do codelab em vídeo com o Filip (em inglês).

Clique em Avançar para iniciar o codelab.

2. Configurar seu ambiente do Flutter

Editor

Para simplificar ao máximo este codelab, presumimos que você vai usar o Visual Studio Code (VS Code) como ambiente de desenvolvimento. Ele não tem custos financeiros e funciona em todas as principais plataformas.

Você também pode usar qualquer editor que quiser: Android Studio, outros ambientes de desenvolvimento integrado IntelliJ, Emacs, Vim ou Notepad++. Todos eles funcionam com o Flutter.

Recomendamos usar o VS Code para este codelab porque as instruções são padronizadas para atalhos específicos do VS Code. É mais fácil dizer "clique aqui" ou "pressione esta tecla" do que algo como "realize a ação adequada no seu editor para fazer X".

15961a28a4500ac1.png

Escolher uma plataforma para desenvolvimento

O Flutter é um kit de ferramentas multiplataforma. Seu app pode ser executado em qualquer um destes sistemas operacionais:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • Web

No entanto, é uma prática comum escolher um único sistema operacional em que você fará o desenvolvimento principal. Ele é sua "plataforma de desenvolvimento", ou seja, o sistema operacional em que seu app é executado durante o desenvolvimento.

d105428cb3aae7d5.png

Por exemplo, vamos supor que você esteja usando um laptop Windows para desenvolver um app do Flutter. Se você escolher o Android como plataforma de desenvolvimento, normalmente conectará um dispositivo Android ao laptop Windows com um cabo USB, e o app será executado nesse dispositivo. Mas você também pode escolher o Windows como a plataforma de desenvolvimento do app, que será então executado como um aplicativo do Windows no seu editor.

Pode ser tentador selecionar a Web como plataforma de desenvolvimento. A desvantagem dessa escolha é que você perde um dos recursos de desenvolvimento mais úteis do Flutter: a recarga automática com estado. O Flutter não faz a recarga automática de aplicativos da Web.

Faça sua escolha agora. Não se esqueça: você pode executar seu app em outros sistemas operacionais depois. Ter uma plataforma de desenvolvimento clara em mente só torna a próxima etapa mais fácil.

Instalar o Flutter

As instruções mais atualizadas sobre como instalar o SDK do Flutter estão sempre em docs.flutter.dev (link em inglês).

As instruções no site do Flutter abrangem não apenas a instalação do próprio SDK, mas também das ferramentas relacionadas à plataforma de desenvolvimento e dos plug-ins do editor. Não se esqueça que, para este codelab, você só precisa instalar o seguinte:

  1. SDK do Flutter.
  2. Visual Studio Code com o plug-in do Flutter.
  3. O software necessário para a plataforma de desenvolvimento escolhida, por exemplo: Visual Studio para Windows ou Xcode para macOS.

Na próxima seção, você vai criar seu primeiro projeto do Flutter.

Se você teve problemas até agora, pode achar algumas destas perguntas e respostas do StackOverflow úteis para a solução de problemas.

Perguntas frequentes (links em inglês)

3. Criar um projeto

Criar seu primeiro projeto do Flutter

Inicie o Visual Studio Code e abra a paleta de comandos com F1, Ctrl+Shift+P ou Shift+Cmd+P. Comece a digitar "flutter novo". Selecione o comando Flutter: novo projeto.

58e8487afebfc1dd.gif

Depois, selecione Aplicativo e uma pasta para criar o projeto. Pode ser o diretório inicial ou algo como C:\src\.

Por fim, dê um nome ao projeto. Por exemplo, namer_app ou my_awesome_namer.

260a7d97f9678005.png

Agora, a pasta do projeto será criada pelo Flutter e aberta pelo VS Code.

Agora você vai substituir o conteúdo de três arquivos por um scaffolding básico do app.

Copiar e colar o aplicativo inicial

No painel esquerdo do VS Code, verifique se Explorer está selecionado e abra o arquivo pubspec.yaml.

e2a5bab0be07f4f7.png

Substitua o conteúdo do arquivo pelo indicado abaixo.

pubspec.yaml (link em inglês)

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: '>=2.19.4 <4.0.0'

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

O arquivo pubspec.yaml especifica informações básicas sobre o app, como a versão atual, as dependências e os recursos que ele terá.

Em seguida, abra outro arquivo de configuração no projeto, analysis_options.yaml.

a781f218093be8e0.png

Substitua o conteúdo dele por este:

analysis_options.yaml (link em inglês)

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_const_constructors: false
    prefer_final_fields: false
    use_key_in_widget_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_const_constructors_in_immutables: false
    avoid_print: false

Esse arquivo determina o quanto o Flutter será rigoroso ao analisar seu código. Como este é seu primeiro trabalho no Flutter, você está dizendo ao analisador para pegar leve. É possível ajustar isso mais tarde. Na verdade, conforme você se aproxima da publicação de um app de produção real, é quase certeza que vai querer usar o analisador mais rigoroso do que isso.

Por fim, abra o arquivo main.dart no diretório lib/.

e54c671c9bb4d23d.png

Substitua o conteúdo do arquivo pelo indicado abaixo.

lib/main.dart (link em inglês)

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
        ],
      ),
    );
  }
}

Essas 50 linhas de código são todo o app até agora.

Na próxima seção, execute o aplicativo no modo de depuração e comece o desenvolvimento.

4. Adicionar um botão

Nesta etapa, vamos adicionar um botão Next (avançar) para gerar um novo pareamento de palavras.

Iniciar o app

Primeiro, abra o arquivo lib/main.dart e verifique se o dispositivo de destino está selecionado. No canto inferior direito do VS Code, você encontra um botão que mostra o dispositivo de destino atual. Clique nele para mudar.

6c4474b4b5e92ffb.gif

Enquanto lib/main.dart estiver aberto, procure o botão "play" b0a5d0200af5985d.png no canto superior direito da janela do VS Code e clique nele.

9b7598a38a6412e6.gif

Após cerca de um minuto, o app é iniciado no modo de depuração. Ele ainda está com a aparência incompleta:

f96e7dfb0937d7f4.png

Primeira recarga automática

Na parte de baixo de lib/main.dart, adicione algo à string no primeiro objeto Text e salve o arquivo (com Ctrl+S ou Cmd+S). Por exemplo:

lib/main.dart (link em inglês)

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

Observe como o aplicativo muda imediatamente, mas a palavra aleatória permanece a mesma. Essa é a famosa recarga automática com estado do Flutter em ação. A recarga automática é acionada quando você salva as mudanças em um arquivo de origem.

1b05b00515b3ecec.gif

Perguntas frequentes (links em inglês)

Como adicionar um botão

Em seguida, adicione um botão na parte de baixo da Column, logo abaixo da segunda instância do Text.

lib/main.dart (link em inglês)

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

Quando você salvar a mudança, o app será atualizado de novo: um botão vai aparecer e, quando você clicar nele, o Console de depuração no VS Code mostra a mensagem botão pressionado!.

8d86426a01e28011.gif

Um curso intensivo do Flutter em cinco minutos

Por mais divertido que seja observar o Console de depuração, você quer o botão faça algo mais significativo. Mas, antes disso, analise o código em lib/main.dart para entender como ele funciona.

lib/main.dart (link em inglês)

// ...

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

// ...

No início do arquivo, você encontra a função main(). Na forma atual, ele apenas informa ao Flutter para executar o app definido em MyApp.

lib/main.dart (link em inglês)

// ...

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

A classe MyApp estende StatelessWidget. Os widgets são os elementos que servem como base para a criação de apps do Flutter. Como você pode notar, até o próprio app é um widget.

O código em MyApp configura todo o app. Ele cria o estado geral (falaremos mais sobre isso depois), nomeia o app e define o tema visual e o widget "inicial", ou seja, o ponto de partida do app.

lib/main.dart (link em inglês)

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

Em seguida, a classe MyAppState define o estado do app. Este é seu primeiro trabalho no Flutter, então este codelab manterá tudo simples e focado. Existem muitas maneiras eficazes de gerenciar o estado do app no Flutter. Uma das mais fáceis de explicar é ChangeNotifier, a abordagem adotada por este app.

  • O MyAppState define os dados necessários para o app funcionar. No momento, ele contém apenas uma variável com o par de palavras aleatórias atual. Você vai adicionar outras opções mais tarde.
  • A classe "state" estende o ChangeNotifier, o que significa que ela pode emitir notificações sobre suas próprias mudanças. Por exemplo, se o par de palavras atual mudar, alguns widgets no app precisarão saber disso.
  • O estado é criado e fornecido a todo o app usando um ChangeNotifierProvider (confira o código acima em MyApp). Todo widget no app tem acesso ao estado. d9b6ecac5494a6ff.png

lib/main.dart (link em inglês)

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           // ← 1
    var appState = context.watch<MyAppState>();  // ← 2

    return Scaffold(                             // ← 3
      body: Column(                              // ← 4
        children: [
          Text('A random AWESOME idea:'),        // ← 5
          Text(appState.current.asLowerCase),    // ← 6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       // ← 7
      ),
    );
  }
}

// ...

Por fim, há MyHomePage, o widget que você já modificou. Cada linha numerada abaixo é mapeada para um comentário de número de linha no código acima:

  1. Cada widget define um método build() que é chamado automaticamente sempre que as circunstâncias do widget mudam, para que ele fique sempre atualizado.
  2. O widget MyHomePage rastreia mudanças no estado atual do app usando o método watch.
  3. Cada método build precisa retornar um widget ou (o que é mais comum) uma árvore aninhada de widgets. Nesse caso, o widget de nível superior é Scaffold. Você não vai trabalhar com Scaffold neste codelab, mas ele é um widget útil e pode ser encontrado na grande maioria dos apps do Flutter do mundo real.
  4. A Column é um dos widgets de layout mais básicos do Flutter. Ela recebe qualquer número de filhos e os coloca em uma coluna de cima para baixo. Por padrão, a coluna coloca os filhos visualmente no topo. Em breve, você vai mudar isso para que a coluna fique centralizada.
  5. Você mudou o widget Text na primeira etapa.
  6. Este segundo widget Text usa o appState e acessa o único membro dessa classe, current (que é um WordPair). O WordPair fornece vários getters úteis, como asPascalCase ou asSnakeCase. Aqui, usamos asLowerCase, mas você pode alterar isso agora caso prefira uma das alternativas.
  7. Observe como o código do Flutter faz um uso intenso de vírgulas à direita. Essa vírgula específica não precisa estar ali, porque children é o último (e também o único) membro dessa lista de parâmetros Column. No entanto, costuma ser uma boa ideia usar vírgulas à direita: elas facilitam adicionar mais membros e também servem como uma dica para o autoformatador do Dart colocar uma nova linha ali. Para mais informações, consulte Formatação de código (link em inglês).

Em seguida, você vai conectar o botão ao estado.

Seu primeiro comportamento

Role até MyAppState e adicione um método getNext.

lib/main.dart (link em inglês)

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  // ↓ Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

O novo método getNext() reatribui o widget current a um novo WordPair aleatório. Ele também chama notifyListeners() (um método de ChangeNotifier) que envia uma notificação a qualquer elemento que esteja observando MyAppState.

Só falta chamar o método getNext pelo callback do botão.

lib/main.dart (link em inglês)

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

Salve e teste o app agora. Ele precisa gerar um novo par de palavras aleatórias toda vez que você pressionar o botão Next.

Na próxima seção, você vai deixar a interface mais bonita.

5. Deixar o app mais bonito

Confira abaixo como o app está no momento.

3dd8a9d8653bdc56.png

Não está legal. É preciso deixar o elemento principal do app, o par de palavras gerado aleatoriamente, mais visível. Afinal, essa é a principal razão para os usuários usarem esse app. Além disso, o conteúdo do app está estranhamente descentralizado, e tudo em preto e branco deixa o app chato.

Esta seção resolve esses problemas trabalhando no design do app. O objetivo final da seção é criar algo parecido com isto:

2bbee054d81a3127.png

Extrair um widget

A linha responsável por mostrar o par de palavras atual está assim no momento: Text(appState.current.asLowerCase). Para transformá-la em algo mais complexo, é uma boa ideia extraí-la em um widget separado. Ter widgets separados para diferentes partes lógicas da interface é uma maneira importante de gerenciar a complexidade no Flutter.

O Flutter fornece um auxiliar de refatoração para extrair widgets, mas antes de usá-lo, verifique se a linha que está sendo extraída acessa apenas o necessário. No momento, a linha acessa appState, mas só precisa saber qual é o par de palavras atual.

Por esse motivo, reescreva o código do widget MyHomePage desta forma:

lib/main.dart (link em inglês)

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;                 // ← Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                // ← Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Legal. O widget Text não se refere mais a todo o appState.

Agora, acesse o menu Refatorar. No VS Code, isso é feito de duas maneiras:

  1. Clique com o botão direito do mouse no trecho de código que você quer refatorar (Text, neste caso) e selecione Refatorar… no menu suspenso,

OU

  1. Mova o cursor até a parte do código que você quer refatorar (Text, neste caso) e pressione Ctrl+. (Win/Linux) ou Cmd+. (Mac).

9e18590d82a6900.gif

No menu Refatorar, selecione Extrair Widget. Atribua um nome, como BigCard, e clique em Enter.

Isso cria automaticamente uma nova classe, BigCard, no fim do arquivo atual. A classe vai ficar assim:

lib/main.dart (link em inglês)

// ...

class BigCard extends StatelessWidget {
  const BigCard({
    super.key,
    required this.pair,
  });

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

Observe como o app continua funcionando mesmo com essa refatoração.

Adicionar um card

Agora é hora de transformar esse novo widget na parte chamativa da interface que imaginamos no início desta seção.

Encontre a classe BigCard e o método build() dentro dela. Como antes, acesse o menu Refatorar no widget Text. No entanto, desta vez você não vai extrair o widget.

Em vez disso, selecione Unir com padding. Isso cria um novo widget pai em torno do widget Text, chamado Padding. Depois de salvar, vai notar que agora há mais espaço para a palavra aleatória.

6b585b43e4037c65.gif

Aumente o valor do padding, que é 8.0 por padrão. Por exemplo, você pode usar 20 para ter mais espaço.

Em seguida, vamos nos aprofundar mais. Posicione o cursor no widget Padding, abra o menu Refatorar e selecione Unir com widget….

Isso permite especificar o widget pai. Digite "Card" e pressione Enter.

523425642904374.gif

Isso encapsula o widget Padding e, portanto, também o Text, com um widget Card.

6031adbc0a11e16b.png

Tema e estilo

Para destacar mais o card, pinte-o com uma cor mais marcante. E como é sempre uma boa ideia manter um esquema de cores consistente, use o Theme do app para escolher a cor.

Faça as alterações abaixo no método build() do BigCard.

lib/main.dart (link em inglês)

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       // ← Add this.

    return Card(
      color: theme.colorScheme.primary,    // ← And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

Essas duas novas linhas fazem muitas coisas:

  • Primeiro, o código solicita o tema atual do app com Theme.of(context).
  • Depois, o código define a cor do card para ser a mesma da propriedade colorScheme do tema. O esquema de cores é muito variado, e primary é a cor de mais destaque do app.

O card agora está usando a cor primária do app:

a136f7682c204ea1.png

Para mudar essa cor e o esquema de cores de todo o app, role até MyApp e mude a cor original do ColorScheme ali.

5bd5a50b5d08f5fb.gif

Observe como a animação da cor acontece suavemente. Isso é chamado de animação implícita. Muitos widgets do Flutter interpolam suavemente entre os valores para que a interface não apenas "pule" entre os estados.

O botão elevado abaixo do card também muda de cor. Esse é o poder de usar um Theme em todo o app em vez de valores codificados.

TextTheme

O card ainda tem um problema: o texto é muito pequeno e a cor dele é difícil de ler. Para corrigir isso, faça as alterações abaixo no método build() do BigCard.

lib/main.dart (link em inglês)

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    // ↓ Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        // ↓ Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

O que está por trás dessa mudança:

  • Ao usar theme.textTheme,, você acessa o tema da fonte do app. Essa classe inclui membros como bodyMedium para texto padrão de tamanho médio, caption para legendas de imagens ou headlineLarge para títulos grandes.
  • A propriedade displayMedium é um estilo grande destinado a texto de exibição. A palavra exibição é usada no sentido tipográfico aqui, como em fonte de exibição (link em inglês). A documentação de displayMedium diz que "estilos de exibição são reservados para textos curtos e importantes", o que é exatamente nosso caso de uso.
  • Teoricamente, a propriedade displayMedium do tema pode ser null. O Dart, a linguagem de programação com que você está criando este app, tem segurança contra valores nulos, por isso não permite que você chame métodos de objetos que podem talvez ser null. Nesse caso, porém, é possível usar o operador ! ("operador bang") para garantir ao Dart que você sabe o que está fazendo. (O displayMedium definitivamente não é nulo neste caso. Como sabemos disso está além do escopo deste codelab).
  • Chamar copyWith() em displayMedium retorna uma cópia do estilo de texto com as mudanças definidas. Neste caso, você está apenas mudando a cor do texto.
  • Para usar a nova cor, acesse novamente o tema do app. A propriedade onPrimary do esquema de cores define uma cor adequada para uso na cor primária do app.

Agora, o app está parecido com este exemplo:

2405e9342d28c193.png

Se você quiser, mude o card ainda mais. Confira algumas ideias:

  • O método copyWith() permite mudar muito mais o estilo do texto do que apenas a cor. Para conferir a lista completa de propriedades que você pode mudar, coloque o cursor em qualquer lugar dentro dos parênteses de copyWith() e pressione Ctrl+Shift+Space (Win/Linux) ou Cmd+Shift+Space (Mac).
  • Da mesma forma, é possível fazer mais mudanças no widget Card. Por exemplo, amplie a sombra do card aumentando o valor do parâmetro elevation.
  • Teste outras cores. Além de theme.colorScheme.primary, há também .secondary, .surface e muitas outras. Todas essas cores têm seus equivalentes onPrimary.

Melhorar a acessibilidade

O Flutter torna os aplicativos acessíveis por padrão. Por exemplo, todos os elementos de texto e interativos em qualquer app do Flutter são apresentados corretamente para leitores de tela, como TalkBack e VoiceOver.

96e3f6d9d36615dd.png

Às vezes, porém, é necessário fazer algum trabalho extra. No caso deste app, o leitor de tela pode ter problemas para pronunciar alguns pares de palavras gerados. Embora os humanos não tenham problemas para identificar as duas palavras em cheaphead, um leitor de tela pode pronunciar o ph no meio da palavra como f.

Uma solução simples é substituir pair.asLowerCase por "${pair.first} ${pair.second}". O segundo caso usa a interpolação de strings para criar uma string (como "cheap head") das duas palavras contidas em pair. O uso de duas palavras separadas em vez de uma palavra composta garante que os leitores de tela as identifiquem adequadamente e oferece uma experiência melhor para usuários com deficiência visual.

No entanto, convém manter a simplicidade visual de pair.asLowerCase. Use a propriedade semanticsLabel de Text para substituir o conteúdo visual do widget de texto por um conteúdo semântico mais apropriado para leitores de tela:

lib/main.dart (link em inglês)

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        // ↓ Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

Agora, os leitores de tela pronunciam corretamente cada par de palavras gerado, mas a interface permanece a mesma. Teste isso usando um leitor de tela no seu dispositivo (link em inglês).

Centralizar a interface

Agora que o par de palavras aleatórias está com um toque visual aprimorado, é hora de colocá-lo no centro da janela/tela do app.

Não se esqueça que BigCard faz parte de uma Column. Por padrão, as colunas agrupam os filhos na parte de cima, mas podemos substituir isso facilmente. Acesse o método build() de MyHomePage e faça a seguinte mudança:

lib/main.dart (link em inglês)

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  // ← Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Isso centraliza os filhos dentro da Column ao longo do eixo principal (vertical).

b555d4c7f5000edf.png

Os filhos já estão centralizados ao longo do eixo transversal da coluna, ou seja, na orientação horizontal. Mas a Column em si não está centralizada dentro do Scaffold. Podemos conferir isso usando o Widget Inspector.

27c5efd832e40303.gif

O Widget Inspector está fora do escopo deste codelab, mas observe que, quando a Column é destacada, ela não ocupa toda a largura do app, mas apenas o espaço horizontal necessário para os filhos dela.

É possível centralizar a coluna. Coloque o cursor em Column, abra o menu Refatorar (com Ctrl+. ou Cmd+.) e selecione Unir ao centro.

56418a5f336ac229.gif

Agora, o app está parecido com este exemplo:

455688d93c30d154.png

Se você quiser, ajuste isso um pouco mais.

  • Você pode remover o widget Text acima do BigCard. É possível argumentar que o texto descritivo, "A random AWESOME idea:" (uma ideia aleatória INCRÍVEL:), não é mais necessário, porque a interface faz sentido e fica mais elegante mesmo sem ele.
  • Você também pode adicionar um widget SizedBox(height: 10) entre BigCard e ElevatedButton. Dessa forma, há um pouco mais de separação entre os dois widgets. O widget SizedBox apenas ocupa espaço e não renderiza nada sozinho. Ele costuma ser usado para criar "lacunas" visuais.

Com as mudanças opcionais, MyHomePage contém este código:

lib/main.dart (link em inglês)

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

E o aplicativo vai ficar parecido com o seguinte:

3d53d2b071e2f372.png

Na próxima seção, você vai acrescentar a possibilidade de adicionar as palavras geradas como favoritas (ou "curti-las").

6. Adicionar funcionalidade

O aplicativo funciona e, ocasionalmente, até fornece pares de palavras interessantes. Mas sempre que o usuário clica em Next, os pares de palavras desaparecem e não voltam mais. Seria melhor ter uma maneira de "lembrar" as melhores sugestões: como um botão "Like" (curtir).

e6b01a8c90df8ffa.png

Adicionar a lógica de negócios

Role até MyAppState e adicione este código:

lib/main.dart (link em inglês)

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  // ↓ Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

Examine as mudanças:

  • Você adicionou uma nova propriedade ao MyAppState chamada favorites. Essa propriedade é inicializada com uma lista vazia: [].
  • Você também especificou que a lista só pode conter pares de palavras: <WordPair>[], usando generics (link em inglês). Isso ajuda a tornar seu app mais robusto. O Dart vai se recusar a executá-lo se você tentar adicionar algo diferente de WordPair a ele. Por sua vez, você pode usar a lista favorites sabendo que nunca poderá haver objetos indesejados (como null) escondidos nela.
  • Você também adicionou um novo método, toggleFavorite(), que remove o par de palavras atual da lista de favoritos (se já estiver lá) ou o adiciona (se ainda não estiver). Em ambos os casos, o código chama notifyListeners(); depois.

Adicionar o botão

Com a "lógica de negócios" resolvida, é hora de trabalhar novamente na interface. Colocar o botão "Curtir" à esquerda do botão "Avançar" exige uma Row. O widget Row é o equivalente horizontal da Column, que abordamos antes.

Primeiro, una o botão existente em uma Row. No método build() do widget MyHomePage, coloque o cursor no ElevatedButton, chame o menu Refatorar com Ctrl+. ou Cmd+. e selecione Unir com linha.

7b9d0ea29e584308.gif

Ao salvar, você vai notar que a Row age de forma semelhante à Column, ou seja, agrupa os filhos à esquerda por padrão (a Column colocou os filhos dela na parte de cima). Para corrigir isso, você pode usar a mesma abordagem de antes, mas com mainAxisAlignment. No entanto, para fins de aprendizado, use mainAxisSize. Isso informa a Row para não ocupar todo o espaço horizontal disponível.

Faça esta mudança:

lib/main.dart (link em inglês)

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   // ← Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

A interface voltou para onde estava antes.

3d53d2b071e2f372.png

Em seguida, adicione o botão Like e conecte-o ao método toggleFavorite(). Como desafio, primeiro tente fazer isso por conta própria, sem consultar o bloco de código abaixo.

e6b01a8c90df8ffa.png

Tudo bem se você não fizer exatamente da mesma maneira mostrada abaixo. Na verdade, não se preocupe com o ícone do coração, a menos que você queira mesmo um grande desafio.

Também não há problema se não funcionar, afinal, esta é sua primeira vez trabalhando com o Flutter.

252f7c4a212c94d2.png

Esta é uma maneira de adicionar o segundo botão ao widget da MyHomePage. Desta vez, use o construtor ElevatedButton.icon() para criar um botão com um ícone. Na parte de cima do método build, escolha o ícone adequado considerando se o par de palavras atual já está nos favoritos ou não. Além disso, observe o uso de SizedBox novamente, para manter os dois botões um pouco separados.

lib/main.dart (link em inglês)

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    // ↓ Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                // ↓ And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

O app vai ficar assim:

11981147e3497c77.gif

Infelizmente, os favoritos não ficam visíveis para o usuário. É hora de adicionar uma tela totalmente separada ao nosso app. Até a próxima seção!

7. Adicionar uma coluna de navegação

A maioria dos apps não consegue mostrar todo o conteúdo em uma única tela. Este app em particular provavelmente caberia, mas para fins explicativos, você vai criar uma tela separada para os favoritos do usuário. Para alternar entre as duas telas, você vai implementar seu primeiro StatefulWidget.

9320e50cad339e7b.png

Para chegar ao objetivo desta etapa o quanto antes, divida MyHomePage em dois widgets separados.

Selecione todo o conteúdo de MyHomePage, exclua-o e substitua por este código:

lib/main.dart (link em inglês)

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

Após salvar, o lado visual da interface estará pronto, mas não funciona ainda. Clicar no "♥︎" (coração) na coluna de navegação não faz nada.

5a5a8e3a04789ce5.png

Examine as mudanças.

  • Primeiro, observe que todo o conteúdo de MyHomePage é extraído para um novo widget, GeneratorPage. A única parte do antigo widget MyHomePage que não foi extraída é o Scaffold.
  • O novo widget da MyHomePage contém uma Row com dois filhos. O primeiro widget é a SafeArea, e o segundo é Expanded.
  • A SafeArea garante que os filhos não sejam ocultos por um entalhe de hardware ou uma barra de status. Neste app, o widget une a NavigationRail para evitar que os botões de navegação sejam ocultos por uma barra de status móvel, por exemplo.
  • É possível mudar a linha extended: false na NavigationRail para true. Isso mostra os rótulos ao lado dos ícones. Em uma etapa futura, você vai aprender como fazer isso de forma automática quando o app tiver espaço horizontal suficiente.
  • A coluna de navegação tem dois destinos, Home (inicial) e Favorites (favoritos), com os respectivos ícones e rótulos. Ela também define o selectedIndex atual. O índice zero selecionado indica o primeiro destino, o índice um selecionado indica o segundo destino e assim por diante. Por enquanto, o índice está codificado para zero.
  • A coluna de navegação também define o que acontece quando o usuário seleciona um dos destinos com onDestinationSelected. No momento, o app apenas gera o valor de índice solicitado com print().
  • O segundo filho da Row é o widget Expanded. Os widgets expandidos são extremamente úteis em linhas e colunas. Eles permitem expressar layouts em que alguns filhos ocupam apenas o espaço necessário (NavigationRail, nesse caso) e outros widgets precisam ocupar o máximo possível do espaço restante (Expanded, nesse caso). Uma maneira de pensar nos widgets Expanded é que eles são "gananciosos". Se você quiser ter uma ideia melhor da função desse widget, tente unir o widget NavigationRail com outro Expanded. O layout resultante ficará parecido com este:

d80b73b692fb04c5.png

  • Dois widgets Expanded dividem todo o espaço horizontal disponível entre si, embora a coluna de navegação precise apenas de uma pequena fração à esquerda.
  • Dentro do widget Expanded, há um Container colorido e dentro do contêiner, a GeneratorPage.

Widgets sem estado versus com estado

Até agora, o MyAppState cobria todas as suas necessidades de estado. É por isso que todos os widgets que você programou até agora são sem estado. Eles não contêm nenhum estado mutável próprio. Nenhum dos widgets pode mudar a si mesmo. Eles precisam passar pelo MyAppState.

Isso está prestes a mudar.

Você precisa armazenar o valor do selectedIndex da coluna de navegação de alguma forma. Além disso, vai mudar esse valor no callback onDestinationSelected.

Você poderia adicionar selectedIndex como outra propriedade de MyAppState. E funcionaria. Mas você pode imaginar que o estado do app cresceria rapidamente fora de controle se cada widget armazenasse os respectivos valores nesse widget.

e52d9c0937cc0823.jpeg

Um estado relevante apenas para um único widget precisa permanecer com esse widget.

Insira o StatefulWidget, um tipo de widget que tem um State. Primeiro, converta MyHomePage em um widget com estado.

Posicione o cursor na primeira linha de MyHomePage (que começa com class MyHomePage...) e abra o menu Refatorar usando Ctrl+. ou Cmd+.. Depois, selecione Converter em widget com estado.

238f98bceeb0de3a.gif

O ambiente de desenvolvimento integrado cria uma nova classe para você, _MyHomePageState. Essa classe estende State e, portanto, pode gerenciar os próprios valores. Ela pode mudar a si mesma. Além disso, o método build do antigo widget sem estado mudou para _MyHomePageState (em vez de permanecer no widget). Ele foi movido sem qualquer mudança. Nada dentro do método build mudou. Ele apenas se encontra em outro lugar agora.

setState

O novo widget com estado só precisa rastrear uma variável: selectedIndex. Faça as três mudanças abaixo em _MyHomePageState:

lib/main.dart (link em inglês)

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     // ← Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    // ← Change to this.
              onDestinationSelected: (value) {

                // ↓ Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

Examine as mudanças:

  1. Você introduz uma nova variável, selectedIndex, e a inicializa como 0.
  2. Você usa essa nova variável na definição de NavigationRail em vez do 0 codificado que estava lá até agora.
  3. Quando o callback onDestinationSelected é chamado, em vez de simplesmente mostrar o novo valor no console, você o atribui ao selectedIndex dentro de uma chamada setState(). Essa chamada é semelhante ao método notifyListeners() usado antes. Ela garante a atualização da interface.

2b31dd91c5ba6766.gif

A coluna de navegação agora responde à interação do usuário. Mas a área expandida à direita permanece a mesma. Isso ocorre porque o código não está usando selectedIndex para determinar qual tela é mostrada.

Usar selectedIndex

Coloque o código abaixo na parte de cima do método build do _MyHomePageState, logo antes de return Scaffold:

lib/main.dart (link em inglês)

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

Confira este código:

  1. O código declara uma nova variável, page, do tipo Widget.
  2. Em seguida, uma instrução switch atribui uma tela a page, de acordo com o valor atual em selectedIndex.
  3. Como ainda não há uma FavoritesPage, use um Placeholder, um widget útil que desenha um retângulo cruzado onde quer que você o coloque, marcando essa parte da interface como incompleta.

5685cf886047f6ec.png

  1. Com o princípio fail-fast (link em inglês), a instrução switch também apresenta um erro caso selectedIndex não seja 0 nem 1. Isso ajuda a evitar erros na linha. Se você adicionar um novo destino à coluna de navegação e esquecer de atualizar esse código, o programa vai falhar no desenvolvimento, em vez de deixar você descobrir o motivo do problema ou publicar um código com bugs durante a produção.

Agora que a page contém o widget que você quer mostrar à direita, provavelmente você sabe qual outra mudança é necessária.

Confira como o _MyHomePageState fica após essa única mudança restante:

lib/main.dart (link em inglês)

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  // ← Here.
            ),
          ),
        ],
      ),
    );
  }
}

// ...

O app agora alterna entre nosso GeneratorPage e o marcador de posição que em breve se tornará a página Favorites.

4122ee1c4830e0eb.gif

Responsividade

Em seguida, torne a coluna de navegação responsiva. Ou seja, mostre os rótulos automaticamente (usando extended: true) quando houver espaço suficiente para eles.

bef3378cb73f9a40.png

O Flutter fornece vários widgets que ajudam a tornar os apps responsivos automaticamente. Por exemplo, Wrap é um widget semelhante à Row ou Column que une automaticamente os filhos na próxima "linha" (chamada "run") quando não há espaço vertical ou horizontal suficiente. Existe também o FittedBox, um widget que ajusta automaticamente os filhos ao espaço disponível de acordo com as especificações.

Mas a NavigationRail não mostra rótulos automaticamente quando há espaço o bastante porque não consegue saber o que é espaço suficiente em cada contexto. Cabe a você, como responsável pelo desenvolvimento, fazer essa chamada.

Digamos que você decida mostrar rótulos apenas se a MyHomePage tiver pelo menos 600 pixels de largura.

O widget a ser usado neste caso é o LayoutBuilder. Com ele, você pode mudar sua árvore de widgets dependendo do espaço disponível.

Mais uma vez, use o menu Refatorar do Flutter no VS Code para fazer as mudanças necessárias. Desta vez, porém, é um pouco mais complicado:

  1. Dentro do método build de _MyHomePageState, coloque o cursor no Scaffold.
  2. Acesse o menu Refatorar com Ctrl+. (Windows/Linux) ou Cmd+. (Mac).
  3. Selecione Unir com builder e pressione Enter.
  4. Mude o nome do Builder recém-adicionado para LayoutBuilder.
  5. Mude a lista de parâmetros de callback de (context) para (context, constraints).

52d18742c54f1022.gif

O callback builder de LayoutBuilder é chamado sempre que as restrições mudam. Isso acontece nestes casos, por exemplo:

  • O usuário redimensiona a janela do app.
  • O usuário gira o smartphone do modo retrato para o modo paisagem ou vice-versa.
  • Algum widget próximo à MyHomePage aumenta de tamanho, tornando menores as restrições de MyHomePage.
  • E assim por diante.

Agora, seu código pode consultar as constraints atuais para decidir se mostra o rótulo. Faça a seguinte mudança de linha única no método build de _MyHomePageState:

lib/main.dart (link em inglês)

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  // ← Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}

// ...

Agora, o app se adequa ao ambiente, como tamanho da tela, orientação e plataforma. Em outras palavras, ele é responsivo.

6223bd3e2dc157eb.gif

O único trabalho que resta é substituir esse Placeholder por uma tela Favorites real. Isso será abordado na próxima seção.

8. Adicionar uma nova página

Lembra do widget Placeholder que usamos em vez da página Favorites?

4122ee1c4830e0eb.gif

É hora de corrigir isso.

Você pode tentar fazer esta etapa por conta própria. Seu objetivo é mostrar a lista de favorites em um novo widget sem estado, FavoritesPage, e depois mostrar esse widget em vez do Placeholder.

Aqui estão algumas dicas:

  • Quando quiser uma Column rolável, use o widget ListView.
  • Não se esqueça de acessar a instância MyAppState de qualquer widget usando context.watch<MyAppState>().
  • Caso também queira testar um novo widget, o ListTile tem propriedades como title (geralmente para texto), leading (para ícones ou avatares) e onTap (para interações). No entanto, você pode conseguir efeitos semelhantes com os widgets que já conhece.
  • O Dart permite usar uma repetição for dentro de literais de coleção. Por exemplo, se o widget de messages contém uma lista de strings, você pode ter um código como este:

f0444bba08f205aa.png

Por outro lado, caso você tenha mais familiaridade com a programação funcional, o Dart também permite criar códigos como messages.map((m) => Text(m)).toList(). E, é claro, você sempre pode criar uma lista de widgets e adicioná-la obrigatoriamente dentro do método build.

A vantagem de adicionar você mesmo a página Favorites é que você aprende mais tomando suas próprias decisões. A desvantagem é que você pode ter problemas que ainda não consegue resolver sem ajuda. Não se esqueça: falhar é normal e é um dos elementos mais importantes do aprendizado. Ninguém espera que você acerte de primeira no desenvolvimento do Flutter, e você nem deveria.

252f7c4a212c94d2.png

O que mostramos abaixo é apenas uma maneira de implementar a página de favoritos. A forma como ela é implementada vai inspirar você (assim espero) a brincar com o código, para melhorar a interface e dar a ela seu toque pessoal.

Esta é a nova classe FavoritesPage:

lib/main.dart (link em inglês)

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

Confira o que o widget faz:

  • Ele detecta o estado atual do app.
  • Se a lista de favoritos estiver vazia, a mensagem centralizada: No favorites yet (nenhum favorito) vai aparecer.
  • Caso contrário, aparecerá uma lista (rolável).
  • A lista começa com um resumo. Por exemplo, You have 5 favorites (você tem 5 favoritos).
  • O código é iterado em todos os favoritos e cria um widget ListTile para cada um.

Tudo o que resta agora é substituir o widget Placeholder por um FavoritesPage. E voilá!

1d26af443561f39c.gif

Confira o código final deste app no repositório do codelab (link em inglês) no GitHub.

9. Próximas etapas

Parabéns!

Que legal! Você pegou um scaffolding não funcional com uma Column e dois widgets Text e o transformou em um pequeno app responsivo e bonito.

d6e3d5f736411f13.png

O que aprendemos

  • Noções básicas de como o Flutter funciona.
  • Como criar layouts no Flutter.
  • Como conectar as interações do usuário, como pressionamentos de botão, ao comportamento do app.
  • Como manter o código do Flutter organizado.
  • Como tornar seu app responsivo.
  • Como criar uma aparência consistente no app.

E agora?

  • Faça mais com o app que você criou neste laboratório.
  • Confira o código desta versão avançada (link em inglês) do mesmo app, para saber como adicionar listas animadas, gradientes, cross-fades e muito mais.

d4afd1f43ab976f7.gif