Como criar IUs incríveis com o Flutter

O Flutter é um kit de ferramentas de IU do Google para criar apps incríveis e nativos para dispositivos móveis, Web e computadores com uma única base de código. Neste codelab, você criará um aplicativo de chat simples para Android, iOS e, opcionalmente, a Web.

Este codelab oferece uma apresentação mais aprofundada sobre o Flutter do que os codelabs Criar seu primeiro app do Flutter, parte 1 e parte 2. Caso queira ver uma introdução mais básica ao Flutter, consulte esses codelabs.

O que você aprenderá

  • Como criar um app do Flutter que tenha um layout natural no Android e no iOS.
  • Como usar o ambiente de desenvolvimento integrado do Android Studio com vários atalhos compatíveis com o plug-in do Flutter para o Android Studio e o IntelliJ.
  • Como depurar seu app do Flutter.
  • Como executar o app do Flutter em um emulador, um simulador e um dispositivo.

O que você quer aprender neste codelab?

Ainda não conheço bem o assunto e quero ter uma boa visão geral. Conheço um pouco sobre esse assunto, mas quero me atualizar. Estou procurando exemplos de código para usar no meu projeto. Estou procurando uma explicação de algo específico.

Você precisa de dois softwares para concluir este codelab: o SDK do Flutter (faça o download) e um editor (configure). Este codelab pressupõe que você usa o Android Studio, mas é possível usar o editor que preferir.

É possível completar este codelab usando qualquer um dos seguintes dispositivos:

  • Um dispositivo físico (Android ou iOS) conectado ao computador e configurado para o modo de desenvolvedor
  • O Android Emulator
  • O iOS Simulator
  • O navegador Chrome
  • Um computador Windows, macOS ou Linux, se você ativar a compatibilidade com um computador do Flutter (link em inglês)

Se você estiver executando o Android, será necessário definir algumas configurações no Android Studio. Caso esteja executando o iOS, é necessário ter também o Xcode instalado em um computador Mac. Para ver mais informações, consulte Configurar um editor (link em inglês).

Crie um app de modelo simples do Flutter. Você modificará esse app inicial para criar o final.

b2f84ff91b0e1396.png Inicie o Android Studio.

  1. Se não houver projetos abertos, selecione Start a new Flutter app na página inicial. Caso contrário, selecione File > New > New Flutter Project.
  2. Selecione Flutter Application como o tipo de projeto e clique em Next.
  3. Verifique se o caminho do SDK do Flutter está especificando o local do SDK. Selecione Install SDK se o campo de texto estiver em branco.
  4. Insira friendly_chat como o nome do projeto e clique em Next.
  5. Use o nome de pacote padrão sugerido pelo Android Studio e clique em Next.
  6. Clique em Finish.
  7. Aguarde o Android Studio instalar o SDK e criar o projeto.

b2f84ff91b0e1396.png Se preferir, você pode criar um app do Flutter na linha de comando.

$ flutter create friendly_chat
$ cd friendly_chat
$ dart migrate --apply-changes
$ flutter run

Problemas?

Consulte a página Test drive (em inglês) para ver mais informações sobre como criar um app de modelo simples ou use o código nos links (em inglês) a seguir para colocar tudo de volta nos eixos.

Nesta seção, você começará a modificar o app de exemplo padrão para transformá-lo em um app de chat. O objetivo é usar o Flutter para criar o FriendlyChat, um app de chat simples e extensível com os seguintes recursos:

  • O app exibe mensagens de texto em tempo real.
  • Os usuários podem inserir uma string de mensagem de texto e enviá-la ao pressionar a tecla Enter ou o botão Enviar.
  • A IU é executada em dispositivos Android e iOS e também na Web.

Teste o app concluído no DartPad (link em inglês).

Criar o scaffold principal do app

O primeiro elemento adicionado é uma barra de app simples, que exibe o título estático do app. Nas próximas seções deste codelab, você adicionará mais elementos de IU responsivos e com estado ao app.

O arquivo main.dart está localizado no diretório lib do projeto do Flutter e contém a função main(), que inicia a execução do app.

b2f84ff91b0e1396.png Substitua todo o código no main.dart pelo seguinte:

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'FriendlyChat',
      home: Scaffold(
        appBar: AppBar(
          title: Text('FriendlyChat'),
        ),
      ),
    ),
  );
}

cf1e10b838bf60ee.png Observações

  • Qualquer programa em Dart, seja um app de linha de comando, um app do AngularDart ou um app do Flutter, começa com uma função main().
  • As definições das funções main() e runApp() (links em inglês) são as mesmas que as do app gerado automaticamente.
  • A função runApp() usa como argumento um Widget, que o framework do Flutter expande e exibe na tela no momento da execução.
  • Esse app de chat usa elementos do Material Design na IU. Portanto, um objeto MaterialApp (link em inglês) é criado e transmitido para a função runApp(). O widget MaterialApp passa a ser a raiz da árvore de widgets do app.
  • O argumento home especifica a tela padrão que será exibida aos usuários no app. Nesse caso, ela consiste em um widget Scaffold que tem um AppBar simples como widget filho (links em inglês). Isso é comum em um app do Material Design.

b2f84ff91b0e1396.png Execute o app clicando no ícone Run 6869d41b089cc745.png no editor. Isso pode demorar um pouco quando o app é executado pela primeira vez. Ele ficará mais rápido nas etapas posteriores.

febbb7a3b70462b7.png

Você verá algo parecido com o exemplo abaixo:

Pixel 3XL

iPhone 11

Criar a tela de chat

Para criar a base para componentes interativos, divida o app simples em duas subclasses de widget diferentes: um widget FriendlyChatApp no nível raiz, que nunca muda, e um widget filho ChatScreen, que é recriado quando as mensagens são enviadas e o estado interno muda. Por enquanto, as duas classes podem estender o StatelessWidget. Mais tarde, ChatScreen será modificado para se tornar um widget com estado. Dessa forma, será possível mudar o estado dele conforme necessário.

b2f84ff91b0e1396.png Crie o widget FriendlyChatApp:

  1. Dentro de main(), posicione o cursor sobre o M do MaterialApp.
  2. Clique com o botão direito do mouse e selecione Refactor > Extract > Extract Flutter widget.

a133a9648f86738.png

  1. Insira FriendlyChatApp na caixa de diálogo ExtractWidget e clique no botão Refactor. O código MaterialApp será colocado em um novo widget sem estado, chamado FriendlyChatApp, e o main() será atualizado para chamar essa classe quando chamar a função runApp().
  2. Selecione o bloco de texto depois de home:. Comece com Scaffold( e termine com o parêntese de fechamento do Scaffold: ). Não inclua a vírgula final.
  3. Comece a digitar ChatScreen, e selecione ChatScreen() no pop-up. Escolha a entrada ChatScreen, marcada com um sinal de igual dentro do círculo amarelo. Isso selecionará uma classe com parênteses vazios, em vez de uma constante.

b2f84ff91b0e1396.pngCrie um widget sem estado, ChatScreen:

  1. Na classe FriendlyChatApp, ao redor da linha 27, comece a digitar stless. O editor perguntará se você quer criar um widget Stateless. Pressione Enter para aceitar. O código boilerplate será exibido e o cursor será posicionado para que você insira o nome do widget sem estado.
  2. Digite ChatScreen.

b2f84ff91b0e1396.pngAtualize o widget ChatScreen:

  1. No widget ChatScreen, selecione Container e comece a digitar Scaffold. Selecione Scaffold no pop-up.
  2. O cursor ficará posicionado entre os parênteses. Pressione Enter para começar uma nova linha.
  3. Comece a digitar appBar, e selecione appBar: no pop-up.
  4. Depois de appBar:, comece a digitar AppBar, e selecione a classe AppBar no pop-up.
  5. Dentro dos parênteses, comece a digitar title, e selecione title: no pop-up.
  6. Depois de title:, comece a digitar Text, e selecione a classe Text.
  7. O código boilerplate de Text contém a palavra data. Exclua a primeira vírgula depois de data. Selecione data, e substitua-a por 'FriendlyChat'. O Dart é compatível com aspas simples ou duplas, mas prioriza o uso de aspas simples, a menos que o texto já contenha esse tipo de aspas.

Observe o canto superior direito do painel de código. Caso você veja uma marca de seleção verde, isso significa que o código foi aprovado na análise. Parabéns!

cf1e10b838bf60ee.png Observações

Esta etapa apresenta vários conceitos importantes do framework do Flutter:

  • A parte da interface do usuário representada por um widget é descrita no método build() dele. O framework chama os métodos build() para FriendlyChatApp e ChatScreen ao inserir esses widgets na hierarquia de widgets e quando há mudanças nas dependências.
  • A @override (link em inglês) é uma anotação em Dart que indica que o método marcado substitui o método de uma superclasse.
  • Alguns widgets, como Scaffold e AppBar, são específicos para apps do Material Design (links em inglês). Outros, como Text (link em inglês), são genéricos e podem ser usados em qualquer app. Os widgets das diferentes bibliotecas do framework do Flutter são compatíveis e podem funcionar juntos em um app.
  • A simplificação do método main() ativa a recarga dinâmica, porque não executa o main() novamente.

b2f84ff91b0e1396.pngClique no botão de recarga dinâmica 48583acd5d1a5e12.png para ver as mudanças quase instantaneamente. Depois de dividir a IU em classes separadas e modificar o widget raiz, não haverá mudanças visíveis na IU.

Problemas?

Se o app não estiver sendo executado corretamente, veja se há erros de digitação. Se necessário, use o código do link a seguir (em inglês) para colocar tudo de volta nos eixos.

Nesta seção, você aprenderá a criar um controle que permita que o usuário insira e envie mensagens de chat.

64fd9c97437a7461.png

Em um dispositivo, clique no campo de texto para que o teclado de software seja exibido. O usuário pode enviar mensagens de chat digitando uma string não vazia e pressionando a tecla Enter do teclado virtual. Como alternativa, os usuários podem enviar mensagens digitadas pressionando o botão gráfico Enviar, ao lado do campo de entrada.

Por enquanto, a IU para escrever mensagens fica na parte superior da tela do chat. Mas, depois que você adicionar a IU para exibição de mensagens na próxima etapa, ela será movida para a parte inferior da tela do chat.

Adicionar um campo de entrada de texto interativo

O framework do Flutter inclui um widget do Material Design chamado TextField. Trata-se de um StatefulWidget, um widget com estado mutável, com propriedades para personalizar o comportamento do campo de entrada. O State inclui informações que podem ser lidas de forma síncrona quando o widget é criado e que podem mudar durante o ciclo de vida dele. Para adicionar o primeiro widget com estado ao app FriendlyChat, é necessário fazer algumas modificações.

b2f84ff91b0e1396.png Mude a ChatScreen para que se torne uma classe com estado:

  1. Selecione a ChatScreen na linha class ChatScreen extends StatelessWidget.
  2. Pressione Option+Return (macOS) ou Alt+Enter (Linux e Windows) para exibir um menu.
  3. Selecione Convert to StatefulWidget nele. A classe será atualizada automaticamente com o código boilerplate de um widget com estado, que inclui uma nova classe _ChatScreenState para gerenciar o estado.

Para gerenciar as interações com o campo de texto, use um objeto TextEditingController (link em inglês) para ler o conteúdo do campo de entrada e limpá-lo depois que a mensagem de chat for enviada.

b2f84ff91b0e1396.png Adicione um TextEditingController ao _ChatScreenState.:

Adicione o seguinte código como a primeira linha na classe _ChatScreenState:

final _textController = TextEditingController();

Agora que seu app consegue gerenciar o estado, você pode criar a classe _ChatScreenState com um campo de entrada e um botão Enviar.

b2f84ff91b0e1396.png Adicione uma função _buildTextComposer a _ChatScreenState:

  Widget _buildTextComposer() {
    return  Container(
        margin: EdgeInsets.symmetric(horizontal: 8.0),
      child: TextField(
        controller: _textController,
        onSubmitted: _handleSubmitted,
        decoration: InputDecoration.collapsed(
            hintText: 'Send a message'),
      ),
    );
  }

cf1e10b838bf60ee.png Observações (links em inglês)

  • No Flutter, os dados com estado de um widget são encapsulados por um objeto State. Depois disso, o objeto State é associado a um widget que estende a classe StatefulWidget.
  • O código acima define um método particular chamado _buildTextComposer() que retorna um widget Container com um widget TextField configurado.
  • O widget Container adiciona uma margem horizontal entre a borda da tela e cada lado do campo de entrada.
  • As unidades transmitidas para EdgeInsets.symmetric são pixels lógicos traduzidos em um número específico de pixels físicos, dependendo da proporção em pixels do dispositivo. Talvez você já conheça o termo equivalente do Android, pixels de densidade independente, ou do iOS, pontos.
  • A propriedade onSubmitted fornece um método de callback particular, _handleSubmitted(). Inicialmente, esse método só limpa o campo, mas depois ele será estendido para enviar a mensagem de chat.
  • O TextField com o TextEditingController oferece controle sobre o campo de texto. Esse controlador limpará o campo e lerá o valor dele.

b2f84ff91b0e1396.png Adicione a função _handleSubmitted a _ChatScreenState para limpar o controlador de texto:

  void _handleSubmitted(String text) {
    _textController.clear();
  }

Adicionar um widget de criação de texto

b2f84ff91b0e1396.pngAtualize o método build() para o _ChatScreenState.:

Depois da linha appBar: AppBar(...), adicione uma propriedade body::

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('FriendlyChat')),
      body: _buildTextComposer(),    // NEW
    );
  }

cf1e10b838bf60ee.png Observações

  • O método _buildTextComposer retorna um widget que encapsula o campo de entrada de texto.
  • Ao adicionar o _buildTextComposer à propriedade body, o app exibe o controle do usuário sobre a entrada de texto.

b2f84ff91b0e1396.png Faça a recarga dinâmica do app. Você verá uma tela parecida com esta:

Pixel 3XL

iPhone 11

Adicionar um botão "Enviar" responsivo

Agora, você adicionará o botão Enviar à direita do campo de texto. Para isso, é necessário adicionar mais estrutura ao layout.

b2f84ff91b0e1396.png Na função _buildTextComposer, envolva o TextField em uma Row:

  1. Selecione o TextField no _buildTextComposer.
  2. Pressione Option+Return (macOS) ou Alt+Enter (Linux e Windows) para exibir um menu e selecione Wrap with widget. Um novo widget que envolve o TextField será adicionado. O nome do marcador será selecionado e o ambiente de desenvolvimento integrado aguardará até que você insira um novo nome para ele.
  3. Comece a digitar Row, e selecione Row na lista exibida. Um pop-up com a definição do construtor da Row será exibido. A propriedade child terá uma borda vermelha e o analisador informará que falta incluir a propriedade children necessária.
  4. Passe o cursor sobre child para exibir um pop-up. Ele perguntará se você quer mudar a propriedade para children. Selecione essa opção.
  5. A propriedade children utiliza uma lista, e não um único widget. No momento, há apenas um item na lista, mas você adicionará outro em breve. Converta o widget em uma lista de um item inserindo um colchete ([) após o texto children:. O editor também inclui o parêntese direito, de fechamento. Exclua o colchete de fechamento. Várias linhas para baixo, antes do parêntese direito que fecha a linha, insira o colchete direito seguido de uma vírgula (],). Agora o analisador exibirá uma marca de seleção verde.
  6. O código está correto, mas não está bem formatado. Clique com o botão direito do mouse no painel de código e selecione Reformat Code with dartfmt.

b2f84ff91b0e1396.png Envolva o TextField em um Flexible:

  1. Selecione Row.
  2. Pressione Option+Return (macOS) ou Alt+Enter (Linux e Windows) para exibir um menu e selecione Wrap with widget. Um novo widget que envolve o TextField será adicionado. O nome do marcador será selecionado e o ambiente de desenvolvimento integrado aguardará até que você insira um novo nome para ele.
  3. Comece a digitar Flexible, e selecione Flexible na lista exibida. Um pop-up com a definição do construtor da Row será exibido.
Widget _buildTextComposer() {
  return  Container(
    margin: EdgeInsets.symmetric(horizontal: 8.0),
    child:  Row(                             // NEW
      children: [                            // NEW
         Flexible(                           // NEW
          child:  TextField(
            controller: _textController,
            onSubmitted: _handleSubmitted,
            decoration:  InputDecoration.collapsed(
                hintText: 'Send a message'),
          ),
        ),                                    // NEW
      ],                                      // NEW
    ),                                        // NEW
  );
}

cf1e10b838bf60ee.png Observações

  • O uso de uma Row permite posicionar o botão Enviar ao lado do campo de entrada.
  • Ao envolver o TextField em um widget Flexible, a Row (links em inglês) é instruída a dimensionar automaticamente o campo de texto, usando o espaço restante que não é ocupado pelo botão.
  • Adicionar uma vírgula após o colchete direito informa ao formatador como o código precisa ser formatado.

Em seguida, você adicionará o botão Enviar. Como esse é um app do Material Design, use o ícone correspondente 2de111ba4b057a1e.png:

b2f84ff91b0e1396.png Adicione o botão Enviar à Row.

O botão Enviar passa a ser o segundo item na lista da Row.

  1. Posicione o cursor depois do colchete de fechamento e da vírgula do widget Flexible. Pressione Enter para iniciar uma nova linha.
  2. Comece a digitar Container, e selecione Container no pop-up. O cursor ficará posicionado dentro dos parênteses do contêiner. Pressione Enter para começar uma nova linha.
  3. Adicione as seguintes linhas de código ao contêiner:
margin: EdgeInsets.symmetric(horizontal: 4.0),
child: IconButton(
    icon: const Icon(Icons.send),
    onPressed: () => _handleSubmitted(_textController.text)),

cf1e10b838bf60ee.png Observações (links em inglês)

  • O IconButton exibe o botão Enviar.
  • A propriedade icon especifica a constante Icons.send da biblioteca do Material Design para criar uma nova instância Icon.
  • Ao colocar o IconButton dentro de um widget Container você consegue personalizar o espaçamento das margens do botão, para que ele fique melhor visualmente ao lado do campo de entrada.
  • A propriedade onPressed usa uma função anônima para invocar o método _handleSubmitted() e transmite o conteúdo da mensagem usando o _textController.
  • No Dart, às vezes a sintaxe de seta (=> expression) é usada na declaração de funções. Essa é uma abreviação da { return expression; }, usada apenas para funções de uma linha. Para ter uma visão geral da compatibilidade com funções do Dart, incluindo funções anônimas e aninhadas, consulte o Tour pela linguagem Dart (link em inglês).

b2f84ff91b0e1396.png Faça uma recarga dinâmica do app para ver o botão Enviar:

Pixel 3XL

iPhone 11

A cor do botão é preta, que é o tema padrão do Material Design. Para atribuir uma cor de destaque aos ícones do app, transmita o argumento de cor para o IconButton ou aplique um tema diferente.

b2f84ff91b0e1396.png Em _buildTextComposer(), envolva o Container em um IconTheme:

  1. Selecione o Container na parte superior da função _buildTextComposer().
  2. Pressione Option+Return (macOS) ou Alt+Enter (Linux e Windows) para exibir um menu e selecione Wrap with widget. Um novo widget que envolve o Container será adicionado. O nome do marcador será selecionado e o ambiente de desenvolvimento integrado aguardará até que você insira um novo nome para ele.
  3. Comece a digitar IconTheme, e selecione IconTheme na lista. A propriedade child será cercada por uma caixa vermelha e o analisador informará que a propriedade data é necessária.
  4. Adicione a propriedade data.
return IconTheme(
  data: IconThemeData(color: Theme.of(context).accentColor), // NEW
  child: Container(

cf1e10b838bf60ee.png Observações (links em inglês)

  • Os ícones herdam a cor, a opacidade e o tamanho de um widget IconTheme, que usa um objeto IconThemeData para definir essas características.
  • A propriedade data do IconTheme especifica o objeto ThemeData do tema atual. Isso atribui a cor de destaque do tema atual ao botão e a qualquer outro ícone dessa parte da árvore de widgets.
  • Um objeto BuildContext é um handle para a localização do widget na árvore de widgets do app. Cada widget tem o próprio BuildContext, que se torna o pai do widget retornado pela função StatelessWidget.build ou State.build. Isso significa que o _buildTextComposer() pode acessar o objeto BuildContext no objeto State encapsulado. Não é necessário transmitir o contexto para o método de forma explícita.

b2f84ff91b0e1396.png Atualize o app. O botão Enviar ficará azul:

Pixel 3XL

iPhone 11

Problemas?

Se o app não estiver sendo executado corretamente, veja se há erros de digitação. Se necessário, use o código do link a seguir (em inglês) para colocar tudo de volta nos eixos.

e57d18c5bb8f2ac7.pngVocê encontroualgo especial (link em inglês)!

Há duas maneiras de depurar o app. É possível usar o ambiente de desenvolvimento integrado diretamente para definir pontos de interrupção ou usar o Dart DevTools (link em inglês), que não pode ser confundido com o Chrome DevTools. Este codelab demonstra como definir pontos de interrupção usando o Android Studio e o IntelliJ. Caso você esteja usando outro editor, como o VS Code, use o DevTools para a depuração. Para uma introdução mais básica ao Dart DevTools, consulte a Etapa 2.5 em Criar seu primeiro app do Flutter na Web (link em inglês).

Os ambientes de desenvolvimento integrado do Android Studio e do IntelliJ permitem depurar apps do Flutter executados em um emulador, um simulador ou um dispositivo. Com esses editores, você pode:

  • selecionar um dispositivo ou simulador para depurar o app;
  • visualizar as mensagens do console;
  • definir pontos de interrupção no código;
  • examinar as variáveis e avaliar as expressões no momento da execução.

Os editores do Android Studio e do IntelliJ exibem o registro do sistema enquanto o app está em execução. Além disso, eles oferecem uma IU do Debugger para trabalhar com pontos de interrupção e controlar o fluxo de execução.

6ea611ca007eb43c.png

Trabalhar com pontos de interrupção

b2f84ff91b0e1396.png Depure seu app do Flutter usando pontos de interrupção:

  1. Abra o arquivo de origem em que você quer definir um ponto de interrupção.
  2. Localize a linha em que você quer configurar um ponto de interrupção, clique nela e selecione Run > Toggle Line Breakpoint. Se preferir, clique no gutter, à direita do número da linha, para ativar um ponto de interrupção.
  3. Se você não estiver no modo de depuração, pause o app.
  4. Reinicie-o usando Run > Debug ou clicando no botão Run debug na IU.

O editor inicia a IU do Debugger e pausa a execução do app quando ele alcança o ponto de interrupção. Você pode usar os controles na IU do Debugger para identificar a causa do erro.

Pratique o uso do Debugger definindo pontos de interrupção nos métodos build() do app FriendlyChat. Em seguida, execute e depure o app. Você pode inspecionar os frames de pilha para ver o histórico de chamadas de método do app.

Com o scaffolding básico e a tela do app prontos, agora você pode definir a área em que as mensagens de chat serão exibidas.

de23b9bb7bf84592.png

Implementar uma lista de mensagens de chat

Nesta seção, você criará um widget que exibe mensagens de chat usando uma composição, ou seja, criando e combinando vários widgets menores. Você começará com um widget que representa uma única mensagem de chat. Em seguida, esse widget será aninhado em uma lista mãe rolável. Por fim, você aninhará a lista rolável no scaffold básico do app.

b2f84ff91b0e1396.png Adicione o widget ChatMessage sem estado:

  1. Posicione o cursor depois da classe FriendlyChatApp e comece a digitar stless. A ordem das classes não é importante, mas essa ordem facilita a comparação do seu código com a solução.
  2. Digite ChatMessage como o nome da classe.

b2f84ff91b0e1396.png Adicione uma Row ao método build() da ChatMessage:

  1. Posicione o cursor dentro dos parênteses em return Container() e pressione Enter para iniciar uma nova linha.
  2. Adicione uma propriedade margin:
margin: EdgeInsets.symmetric(vertical: 10.0),
  1. O filho do Container' será uma Row. A lista da Row contém dois widgets: um avatar e uma coluna de texto.
return Container(
  child: Row(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Container(
        margin: const EdgeInsets.only(right: 16.0),
        child: CircleAvatar(child: Text(_name[0])),
      ),
      Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(_name, style: Theme.of(context).textTheme.headline4),
          Container(
            margin: EdgeInsets.only(top: 5.0),
            child: Text(text),
          ),
        ],
      ),
    ],
  ),
);
  1. Adicione uma variável text e um construtor à parte superior da ChatMessage:
class ChatMessage extends StatelessWidget {
  ChatMessage({required this.text}); // NEW
  final String text;                 // NEW

Nesse ponto, o único problema que o analisador indicará é que o _name está indefinido. Isso será corrigido em seguida.

b2f84ff91b0e1396.png Defina a variável _name.

Defina a variável _name conforme mostrado, substituindo Your Name pelo seu nome. Use essa variável para rotular cada mensagem de chat com o nome do remetente. Para simplificar, neste codelab o valor será codificado, mas a maioria dos apps recupera o nome do remetente usando uma autenticação. Depois da função main(), adicione a seguinte linha:

String _name = 'Your Name';

cf1e10b838bf60ee.png Observações (links em inglês)

  • O método build() da ChatMessage retorna uma Row que exibe um avatar gráfico simples, representando o usuário que enviou a mensagem de chat, um widget de Column com o nome do remetente e o texto da mensagem.
  • Para personalizar o CircleAvatar, rotule-o com a primeira inicial do usuário transmitindo o primeiro caractere do valor da variável _name para um widget Text filho.
  • O parâmetro crossAxisAlignment especifica o CrossAxisAlignment.start no construtor da Row para definir a posição do avatar e das mensagens em relação aos widgets pais. Para o avatar, o pai é um widget Row com eixo principal na horizontal. Por isso, o CrossAxisAlignment.start atribui a ele a posição mais alta no eixo vertical. Para as mensagens, o pai é um widget Column com eixo principal na vertical. Assim, o CrossAxisAlignment.start alinha o texto na posição mais à esquerda no eixo horizontal.
  • Ao lado do avatar, dois widgets Text ficam alinhados verticalmente para exibir o nome do remetente na parte superior e o texto da mensagem abaixo.
  • O Theme.of(context) fornece o objeto padrão ThemeData do Flutter para o app. Nas próximas etapas, o tema padrão será modificado para definir o estilo do app de formas diferentes para o Android e o iOS.
  • A propriedade textTheme do ThemeData fornece acesso aos estilos lógicos do Material Design para textos como headline4. Assim, você não precisa codificar os tamanhos das fontes e outros atributos de texto. Nesse exemplo, o estilo do nome do remetente é definido para que fique maior que o texto da mensagem.

b2f84ff91b0e1396.png Faça uma recarga dinâmica do app.

Digite mensagens no campo de texto. Pressione o botão Enviar para limpar a mensagem. Digite uma mensagem longa no campo de texto para ver o que acontece quando ela ultrapassa o limite do campo. Mais adiante, na etapa 9, você envolverá a coluna em um widget Expanded para unir com o widget Text.

Implementar uma lista de mensagens de chat na IU

O próximo refinamento é buscar a lista de mensagens de chat e exibi-las na IU. Essa lista precisa ser rolável, para que os usuários consigam ver o histórico de mensagens. Ela também precisa apresentar as mensagens em ordem cronológica, com a mensagem mais recente na parte inferior da lista visível.

b2f84ff91b0e1396.png Adicione uma lista _messages ao _ChatScreenState.

Na definição do _ChatScreenState, adicione um membro da List (link em inglês) chamado _messages para representar cada mensagem de chat:

class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];      // NEW
  final _textController = TextEditingController();

b2f84ff91b0e1396.png Modifique o método _handleSubmitted() no _ChatScreenState.

Quando o usuário enviar uma mensagem de chat no campo de texto, o app a adicionará à lista de mensagens. Modifique o método _handleSubmitted() para implementar esse comportamento:

void _handleSubmitted(String text) {
  _textController.clear();
  ChatMessage message = ChatMessage(    //NEW
    text: text,                         //NEW
  );                                    //NEW
  setState(() {                         //NEW
    _messages.insert(0, message);       //NEW
  });                                   //NEW
 }

b2f84ff91b0e1396.png Coloque o foco novamente no campo de texto após o envio do conteúdo.

  1. Adicione um FocusNode ao _ChatScreenState:
class _ChatScreenState extends State<ChatScreen> {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();    // NEW
  1. Adicione a propriedade focusNode ao TextField em _buildTextComposer():
child: TextField(
  controller: _textController,
  onSubmitted: _handleSubmitted,
  decoration: InputDecoration.collapsed(hintText: 'Send a message'),
  focusNode: _focusNode,  // NEW
),
  1. Em _handleSubmitted(), após a chamada a setState(), solicite o foco no TextField:
    setState(() {
      _messages.insert(0, message);
    });
    _focusNode.requestFocus();  // NEW

cf1e10b838bf60ee.png Observações

  • Cada item da lista é uma instância ChatMessage.
  • A lista é inicializada para ficar vazia.
  • Ao chamar setState() (link em inglês) para modificar _messages, o framework é informado de que essa parte da árvore de widgets mudou e de que a IU precisa ser recriada. Realize somente operações síncronas no setState(). Caso contrário, o framework poderá recriar os widgets antes que a operação seja concluída.
  • Em geral, é possível chamar o setState() com um fechamento vazio depois que alguns dados particulares mudam fora dessa chamada de método. No entanto, é recomendável atualizar os dados no fechamento do setState para que você não se esqueça de chamá-lo depois.

b2f84ff91b0e1396.png Faça uma recarga dinâmica do app.

Digite o texto no campo e pressione Return. O foco retorna para o campo de texto.

Incluir a lista de mensagens

Está tudo pronto para exibir a lista de mensagens de chat. Use os widgets ChatMessage da lista _messages e coloque-os em um widget ListView para exibir uma lista rolável.

b2f84ff91b0e1396.png No método build() do _ChatScreenState, adicione uma ListView dentro de uma Column:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text ('FriendlyChat')),
    body: Column(                                            // MODIFIED
      children: [                                            // NEW
        Flexible(                                            // NEW
          child: ListView.builder(                           // NEW
            padding: EdgeInsets.all(8.0),                    // NEW
            reverse: true,                                   // NEW
            itemBuilder: (_, int index) => _messages[index], // NEW
            itemCount: _messages.length,                     // NEW
          ),                                                 // NEW
        ),                                                   // NEW
        Divider(height: 1.0),                                // NEW
        Container(                                           // NEW
          decoration: BoxDecoration(
            color: Theme.of(context).cardColor),             // NEW
          child: _buildTextComposer(),                       // MODIFIED
        ),                                                   // NEW
      ],                                                     // NEW
    ),                                                       // NEW
  );
}

cf1e10b838bf60ee.png Observações (links em inglês)

  • O método de fábrica ListView.builder cria uma lista sob demanda, fornecendo uma função que será chamada uma vez por item na lista. A função retorna um novo widget a cada chamada. O builder também detecta automaticamente mutações do parâmetro children e inicia uma recriação.
  • Os parâmetros transmitidos ao construtor ListView.builder personalizam o conteúdo e a aparência da lista:
  • O padding cria espaços em branco ao redor do texto da mensagem.
  • O itemCount especifica o número de mensagens na lista.
  • O itemBuilder fornece a função que cria cada widget no [index]. Como você não precisa do contexto de compilação atual, pode ignorar o primeiro argumento do IndexedWidgetBuilder. A convenção para indicar que o argumento não será usado é nomeá-lo apenas com um sublinhado (_).
  • A propriedade body do widget Scaffold agora contém a lista de mensagens recebidas, o campo de entrada e o botão Enviar. O layout usa os seguintes widgets:
  • Column: coloca os filhos diretos na vertical. O widget Column aceita uma lista de widgets filhos (igual a uma Row), que se torna uma lista rolável e uma linha para um campo de entrada.
  • Flexible, como pai da ListView: instrui o framework a permitir que a lista de mensagens recebidas se expanda para preencher a altura da Column, enquanto o TextField permanece com um tamanho fixo.
  • Divider: mostra uma linha horizontal entre a IU para exibir mensagens e o campo de entrada de texto para escrevê-las.
  • Container, como pai do compositor de texto: define as imagens de plano de fundo, o preenchimento, as margens e outros detalhes comuns de layout.
  • decoration: cria um novo objeto BoxDecoration que define a cor do plano de fundo. Nesse caso, você está usando a cardColor definida pelo objeto ThemeData do tema padrão. Isso faz com que a IU para redação de mensagens tenha um plano de fundo diferente da lista de mensagens.

b2f84ff91b0e1396.png Faça uma recarga dinâmica do app. Você verá uma tela parecida com esta:

Pixel 3XL

iPhone 11

b2f84ff91b0e1396.png Tente enviar algumas mensagens de chat usando as IUs para escrever e exibir mensagens que você acabou de criar.

Pixel 3XL

iPhone 11

Problemas?

Se o app não estiver sendo executado corretamente, veja se há erros de digitação. Se necessário, use o código do link a seguir (em inglês) para colocar tudo de volta nos eixos.

É possível adicionar uma animação aos seus widgets para tornar a experiência do usuário do app mais fluida e intuitiva. Nesta seção, você aprenderá a adicionar um efeito de animação básico à lista de mensagens de chat.

Quando o usuário envia uma nova mensagem, em vez de simplesmente exibi-la na lista, é possível animá-la para que suba gradualmente, saindo da parte inferior da tela.

As animações no Flutter são encapsuladas como objetos Animation que contêm um valor digitado e um status, como forward, reverse, complete e dismissed. Você pode anexar um objeto de animação a um widget ou detectar mudanças no objeto de animação. Com base nas mudanças nas propriedades do objeto de animação, o framework pode modificar a forma como o widget é exibido e recriar a árvore de widgets.

Especificar um controlador de animação

Use a classe AnimationController para especificar como a animação será executada. O AnimationController permite definir características importantes da animação, como a duração e a direção da reprodução, para frente ou para trás.

b2f84ff91b0e1396.png Atualize a definição da classe _ChatScreenState para incluir um TickerProviderStateMixin:

class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {   // MODIFIED
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  ...

b2f84ff91b0e1396.png Na definição da classe ChatMessage, adicione uma variável para armazenar o controlador de animação:

class ChatMessage extends StatelessWidget {
  ChatMessage({required this.text, required this.animationController}); // MODIFIED
  final String text;
  final AnimationController animationController;      // NEW
  ...

b2f84ff91b0e1396.png Adicione um controlador de animação ao método _handleSubmitted():

void _handleSubmitted(String text) {
  _textController.clear();
  var message = ChatMessage(
    text: text,
    animationController: AnimationController(      // NEW
      duration: const Duration(milliseconds: 700), // NEW
      vsync: this,                                 // NEW
    ),                                             // NEW
  );                                               // NEW
  setState(() {
    _messages.insert(0, message);
  });
  _focusNode.requestFocus();
  message.animationController.forward();           // NEW
}

cf1e10b838bf60ee.png Observações (links em inglês)

  • O AnimationController especifica que a animação será executada por 700 milissegundos. Essa duração mais longa desacelera o efeito da animação para que você possa ver a transição ocorrer de forma mais gradual. Na prática, o ideal é definir uma duração mais curta ao executar o app.
  • O controlador de animação é anexado a uma nova instância da ChatMessage e especifica que a animação será reproduzida sempre que uma mensagem for adicionada à lista de chat.
  • Ao criar um AnimationController, é necessário transmitir um argumento vsync para ele. O vsync é a origem dos sinais de funcionamento, o Ticker, que impulsiona a animação. Nesse exemplo, o _ChatScreenState é usado como o vsync. Portanto, ele adiciona um mixin TickerProviderStateMixin à definição da classe _ChatScreenState.
  • Em Dart, um mixin permite que um corpo de classe seja reutilizado em várias hierarquias de classe. Para ver mais informações, consulte Como adicionar recursos a uma classe: mixins, uma seção do Tour pela linguagem Dart (links em inglês).

Adicionar um widget SizeTransition

O acréscimo de um widget SizeTransition à animação tem o efeito de animar um ClipRect, que expõe cada vez mais o texto enquanto ele desliza para a tela.

b2f84ff91b0e1396.png Adicione um widget SizeTransition ao método build() para a ChatMessage:

  1. No método build() da ChatMessage, selecione a primeira instância do Container.
  2. Pressione Option+Return (macOS) ou Alt+Enter (Linux e Windows) para exibir um menu e selecione Wrap with widget.
  3. Digite SizeTransition. Uma caixa vermelha será exibida ao redor da propriedade child:. Isso indica que uma propriedade obrigatória está faltando na classe do widget. Passe o cursor sobre a SizeTransition, e uma dica informará que o sizeFactor é obrigatório e se oferecerá para criá-lo. Escolha essa opção e a propriedade aparecerá com um valor null.
  4. Substitua o null por uma instância da CurvedAnimation. Isso adicionará o código boilerplate a duas propriedades: parent (obrigatória) e curve.
  5. Para a propriedade parent, substitua o null pelo animationController.
  6. Para a propriedade curve, substitua o null pelo Curves.easeOut, uma das constantes da classe Curves (link em inglês).
  7. Adicione uma linha depois do sizeFactor (no mesmo nível) e insira uma propriedade axisAlignment para a SizeTransition, com o valor 0.0.
@override
Widget build(BuildContext context) {
  return SizeTransition(             // NEW
    sizeFactor:                      // NEW
        CurvedAnimation(parent: animationController, curve: Curves.easeOut),  // NEW
    axisAlignment: 0.0,              // NEW
    child: Container(                // MODIFIED
    ...

cf1e10b838bf60ee.png Observações

  • O objeto CurvedAnimation, em conjunto com a classe SizeTransition (links em inglês), produz um efeito de animação ease-out. Esse efeito faz com que a mensagem deslize rapidamente no início da animação e desacelere até parar completamente.
  • O widget SizeTransition se comporta como um ClipRect animado, que expõe mais o texto à medida que desliza para a tela.

Descartar a animação

É recomendável descartar os controladores de animação para liberar os recursos quando não forem mais necessários.

b2f84ff91b0e1396.png Adicione o método dispose() ao _ChatScreenState.

Adicione o seguinte método à parte inferior do _ChatScreenState:

@override
void dispose() {
  for (var message in _messages){
    message.animationController.dispose();
  }
  super.dispose();
}

b2f84ff91b0e1396.png O código está correto, mas não está bem formatado. Clique com o botão direito do mouse no painel de código e selecione Reformat Code with dartfmt.

b2f84ff91b0e1396.png Faça uma recarga dinâmica do app, ou uma inicialização a quente caso o app em execução contenha mensagens de chat, e digite algumas mensagens para observar o efeito da animação.

Caso queira saber mais sobre as animações, teste as seguintes ideias (links em inglês):

  • Acelere ou acelere a animação modificando o valor da duration especificado no método _handleSubmitted().
  • Especifique curvas de animação diferentes usando as constantes definidas na classe Curves.
  • Crie um efeito de animação de esmaecimento, envolvendo o Container em um widget FadeTransition, em vez de SizeTransition.

Problemas?

Se o app não estiver sendo executado corretamente, veja se há erros de digitação. Se necessário, use o código do link a seguir (em inglês) para colocar tudo de volta nos eixos.

Nesta etapa opcional, você incluirá alguns detalhes sofisticados ao app, como deixar o botão Enviar ativado somente quando houver texto a ser enviado, agrupar mensagens mais longas e adicionar personalizações nativas para Android e iOS.

Tornar o botão "enviar" contextual

No momento, o botão Enviar aparece sempre ativado, mesmo quando não há texto no campo de entrada. Você pode fazer a aparência do botão mudar com base em se o campo tem ou não texto a ser enviado.

b2f84ff91b0e1396.png Defina a _isComposing, uma variável particular que é verdadeira sempre que o usuário digita uma string no campo de entrada:

class _ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  final List<ChatMessage> _messages = [];
  final _textController = TextEditingController();
  final FocusNode _focusNode = FocusNode();
  bool _isComposing = false;            // NEW

b2f84ff91b0e1396.png Adicione um método de callback onChanged() ao _ChatScreenState.

No método _buildTextComposer(), adicione a propriedade onChanged ao TextField e atualize a propriedade onSubmitted:

Flexible(
  child: TextField(
    controller: _textController,
    onChanged: (String text) {            // NEW
      setState(() {                       // NEW
        _isComposing = text.isNotEmpty;   // NEW
      });                                 // NEW
    },                                    // NEW
    onSubmitted: _isComposing ? _handleSubmitted : null, // MODIFIED
    decoration:
        InputDecoration.collapsed(hintText: 'Send a message'),
    focusNode: _focusNode,
  ),
),

b2f84ff91b0e1396.png Atualize o método de callback onPressed() no _ChatScreenState.

Ainda no método _buildTextComposer(), atualize a propriedade onPressed do IconButton:

Container(
  margin: EdgeInsets.symmetric(horizontal: 4.0),
  child: IconButton(
      icon: const Icon(Icons.send),
      onPressed: _isComposing                            // MODIFIED
          ? () => _handleSubmitted(_textController.text) // MODIFIED
          : null,                                        // MODIFIED
      )
      ...
)

b2f84ff91b0e1396.png Modifique o _handleSubmitted para definir _isComposing como falso quando o campo de texto estiver em branco:

void _handleSubmitted(String text) {
  _textController.clear();
  setState(() {                             // NEW
    _isComposing = false;                   // NEW
  });                                       // NEW

  ChatMessage message = ChatMessage(
  ...

cf1e10b838bf60ee.png Observações (links em inglês)

  • O callback onChanged notifica o TextField de que o usuário editou o texto. O TextField chama esse método sempre que o valor do campo muda.
  • O callback onChanged chama setState() para mudar o valor do _isComposing para verdadeiro quando o campo contém texto.
  • Quando o _isComposing é falso, a propriedade onPressed é definida como null.
  • A propriedade onSubmitted também foi modificada para não adicionar uma string vazia à lista de mensagens.
  • A variável _isComposing agora controla o comportamento e a aparência do botão Enviar.
  • Se o usuário digitar uma string no campo de texto, o _isComposing será true, e a cor do botão será definida como Theme.of(context).accentColor. Quando o usuário pressiona o botão Enviar, o framework invoca _handleSubmitted().
  • Se o usuário não digitar nada no campo de texto, o _isComposing será false, e a propriedade onPressed do widget será definida comonull, desativando o botão Enviar. O framework muda a cor do botão para Theme.of(context).disabledColor automaticamente.

b2f84ff91b0e1396.png Faça uma recarga dinâmica do app para testá-lo.

Agrupar linhas longas

Quando um usuário envia uma mensagem de chat que excede a largura da IU para exibição de mensagens, as linhas precisam ser agrupadas para que toda a mensagem seja exibida. No momento, as linhas que ultrapassam o limite da tela ficam truncadas e um erro de limite visual é exibido. Uma forma simples de garantir que o texto será agrupado corretamente é colocá-lo em um widget Expanded.

b2f84ff91b0e1396.png Envolva o widget Column em um widget Expanded:

  1. No método build() da ChatMessage, selecione o widget Column dentro da Row do Container.
  2. Pressione Option+Return (macOS) ou Alt+Enter (Linux e Windows) para exibir um menu.
  3. Comece a digitar Expanded, e selecione Expanded na lista de objetos possíveis.

O exemplo de código a seguir mostra como a classe ChatMessage fica depois dessa mudança:

...
Container(
  margin: const EdgeInsets.only(right: 16.0),
  child: CircleAvatar(child: Text(_name[0])),
),
Expanded(            // NEW
  child: Column(     // MODIFIED
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text(_name, style: Theme.of(context).textTheme.headline4),
      Container(
        margin: EdgeInsets.only(top: 5.0),
        child: Text(text),
      ),
    ],
  ),
),                    // NEW
...

cf1e10b838bf60ee.png Observações

O widget Expanded permite que o widget filho, como Column, imponha restrições de layout a um widget filho, nesse caso, a largura do Column. Aqui ele restringe a largura do widget Text (link em inglês), que normalmente é determinada pelo conteúdo.

Personalizar para Android e iOS

Para que a IU do app tenha uma aparência natural, adicione um tema e uma lógica simples ao método build() (link em inglês) da classe FriendlyChatApp. Nessa etapa, você definirá um tema de plataforma que aplica um conjunto diferente de cores primárias e de destaque. Você também pode personalizar o botão Enviar para usar um IconButton do Material Design no Android e um CupertinoButton no iOS (links em inglês).

b2f84ff91b0e1396.png Adicione o seguinte código ao main.dart, depois do método main():

final ThemeData kIOSTheme = ThemeData(
  primarySwatch: Colors.orange,
  primaryColor: Colors.grey[100],
  primaryColorBrightness: Brightness.light,
);

final ThemeData kDefaultTheme = ThemeData(
  primarySwatch: Colors.purple,
  accentColor: Colors.orangeAccent[400],
);

cf1e10b838bf60ee.png Observações

  • O objeto kDefaultTheme ThemeData especifica as cores para Android: roxo com tons de laranja.
  • O objeto kIOSTheme ThemeData (link em inglês) especifica cores para iOS: cinza claro com tons de laranjas.

b2f84ff91b0e1396.png Modifique a classe FriendlyChatApp para variar o tema usando a propriedade theme do widget MaterialApp do app:

  1. Importe o pacote "foundation" na parte superior do arquivo:
import 'package:flutter/foundation.dart';  // NEW
import 'package:flutter/material.dart';
  1. Modifique a classe FriendlyChatApp para escolher um tema adequado:
class FriendlyChatApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'FriendlyChat',
      theme: defaultTargetPlatform == TargetPlatform.iOS // NEW
        ? kIOSTheme                                      // NEW
        : kDefaultTheme,                                 // NEW
      home: ChatScreen(),
    );
  }
}

b2f84ff91b0e1396.png Modifique o tema do widget AppBar, o banner na parte superior da IU do app.

  1. No método build() do _ChatScreenState, localize a seguinte linha de código:
      appBar: AppBar(title: Text('FriendlyChat')),
  1. Coloque o cursor entre os dois parênteses à direita ())), digite uma vírgula e pressione Enter para iniciar uma nova linha.
  2. Adicione as duas linhas a seguir:
elevation:
   Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
  1. Clique com o botão direito do mouse no painel de código e selecione Reformat Code with dartfmt.

cf1e10b838bf60ee.png Observações

  • A propriedade de nível superior defaultTargetPlatform e os operadores condicionais (links em inglês) são usados para selecionar o tema.
  • A propriedade elevation define as coordenadas Z da AppBar. No Android, o valor das coordenadas Z de 4.0 define uma sombra e, no iOS, o valor 0.0 não define uma sombra. .

b2f84ff91b0e1396.png Personalize o ícone "Enviar" para Android e para iOS.

  1. Adicione a seguinte importação à parte superior do main.dart:
import 'package:flutter/cupertino.dart';   // NEW
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
  1. No método _buildTextComposer() do _ChatScreenState, modifique a linha que atribui um IconButton como filho do Container. Mude a atribuição para que ela passe a ser condicional na plataforma. Para iOS, use um CupertinoButton. Para outros sistemas, continue com um IconButton:
Container(
   margin: EdgeInsets.symmetric(horizontal: 4.0),
   child: Theme.of(context).platform == TargetPlatform.iOS ? // MODIFIED
   CupertinoButton(                                          // NEW
     child: Text('Send'),                                    // NEW
     onPressed: _isComposing                                 // NEW
         ? () =>  _handleSubmitted(_textController.text)     // NEW
         : null,) :                                          // NEW
   IconButton(                                               // MODIFIED
       icon: const Icon(Icons.send),
       onPressed: _isComposing ?
           () =>  _handleSubmitted(_textController.text) : null,
       )
   ),

b2f84ff91b0e1396.png Envolva a Column de nível superior em um widget Container e atribua a ela uma borda cinza clara na borda superior.

Essa borda ajuda a diferenciar visualmente a barra de apps do corpo do app no iOS. Para ocultar a borda no Android, aplique a mesma lógica usada para a barra de apps no exemplo de código anterior:

  1. No método build() do _ChatScreenState, selecione a Column que aparece depois de body:.
  2. Pressione Option+Return (macOS) ou Alt+Enter (Linux e Windows) para abrir um menu e selecione Wrap with Container.
  3. Depois do fim dessa Column, e antes do fim do Container, adicione o código mostrado que adiciona o botão adequado de forma condicional, dependendo da plataforma.
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('FriendlyChat'),
      elevation:
          Theme.of(context).platform == TargetPlatform.iOS ? 0.0 : 4.0, // NEW
    ),
    body: Container(
        child: Column(
          children: [
            Flexible(
              child: ListView.builder(
                padding: EdgeInsets.all(8.0),
                reverse: true,
                itemBuilder: (_, int index) => _messages[index],
                itemCount: _messages.length,
              ),
            ),
            Divider(height: 1.0),
            Container(
              decoration: BoxDecoration(color: Theme.of(context).cardColor),
              child: _buildTextComposer(),
            ),
          ],
        ),
        decoration: Theme.of(context).platform == TargetPlatform.iOS // NEW
            ? BoxDecoration(                                 // NEW
                border: Border(                              // NEW
                  top: BorderSide(color: Colors.grey[200]!), // NEW
                ),                                           // NEW
              )                                              // NEW
            : null),                                         // MODIFIED
  );
}

b2f84ff91b0e1396.png Faça uma recarga dinâmica do app. Você verá diferentes cores, sombras e botões de ícone para Android e iOS.

Pixel 3XL

iPhone 11

Problemas?

Se o app não estiver sendo executado corretamente, veja se há erros de digitação. Se necessário, use o código do link a seguir (em inglês) para colocar tudo de volta nos eixos.

Parabéns!

Agora você já conhece os conceitos básicos para a criação de apps multiplataformas com o framework do Flutter.

O que abordamos

  • Como criar um app do Flutter desde o início.
  • Como usar alguns dos atalhos oferecidos pelo Android Studio e pelo IntelliJ.
  • Como executar, atualizar e depurar seu app do Flutter em um emulador, um simulador e um dispositivo.
  • Como personalizar sua interface do usuário com widgets e animações.
  • Como personalizar sua interface do usuário para Android e iOS.

Qual é a próxima etapa?

Veja um dos outros codelabs do Flutter (link em inglês).

Continue aprendendo sobre o Flutter (links em inglês):

Para ver mais informações sobre atalhos de teclado (links em inglês):

Você pode fazer o download do exemplo de código para ter como referência ou iniciar o codelab em uma seção específica. Para receber uma cópia do exemplo de código do codelab, execute o seguinte comando no terminal:

 git clone https://github.com/flutter/codelabs

O exemplo de código deste codelab está na pasta friendly_chat. Cada pasta enumerada está alinhada com o código final das etapas enumeradas deste codelab. Também é possível soltar o código do arquivo lib/main.dart de qualquer uma dessas etapas em uma instância do DartPad e executá-lo.