MDC-102 no Flutter: estrutura e layout do Material Design (Flutter)

logo_components_color_2x_web_96dp.png

Os Componentes do Material Design (MDC, na sigla em inglês) ajudam os desenvolvedores a implementar o Material Design. Criados por uma equipe de engenheiros e designers de UX do Google, os MDC oferecem dezenas de componentes de IU bonitos e funcionais disponíveis para Android, iOS, Web e Flutter.material.io/develop

No codelab MDC-101, você usou dois componentes do Material Design para criar uma página de login: campos de texto e botões de ondulações com tinta. Agora, vamos nos aprofundar nessa base adicionando navegação, estrutura e dados.

O que você criará

Neste codelab, você criará uma tela inicial para o app Shrine, um app de comércio eletrônico que vende roupas e artigos domésticos. Ele conterá:

  • Uma barra de apps superior.
  • Uma lista de grade cheia de produtos.

Android

iOS

Componentes do MCD neste codelab

  • Barra superior do app.
  • Grades.
  • Cards.

Como você classificaria seu nível de experiência em desenvolvimento com o Flutter?

Iniciante Intermediário Proficiente

Antes de começar

Para começar a desenvolver apps para dispositivos móveis com o Flutter:

  1. Faça o download e instale o SDK do Flutter.
  2. Atualize seu PATH usando o SDK do Flutter.
  3. Instale o Android Studio com os plug-ins do Flutter e do Dart ou o editor que preferir.
  4. Instale um emulador Android, um simulador de iOS (exige um Mac com Xcode) ou use um dispositivo físico.

Para ver mais informações sobre a instalação do Flutter, consulte Primeiros passos: instalação (link em inglês). Para configurar um editor, consulte Primeiros passos: configurar um editor (link em inglês). Você pode usar as opções padrão para instalar um emulador Android, como um smartphone Pixel 3 com a imagem do sistema mais recente. Recomendamos ativar a aceleração de VM, embora não seja obrigatório. Após concluir as quatro etapas acima, você poderá retornar ao codelab. Para concluir este codelab, você só precisa instalar o Flutter em uma plataforma (Android ou iOS).

Verificar se o SDK do Flutter está no estado correto

Antes de seguir este codelab, verifique se o SDK está no estado correto. Se o SDK do Flutter já tiver sido instalado anteriormente, use flutter upgrade para garantir que o SDK esteja no estado mais recente.

 flutter upgrade

Executar flutter upgrade executará automaticamente flutter doctor. Se esta for uma nova instalação do Flutter e nenhuma atualização for necessária, execute flutter doctor manualmente. Você receberá uma notificação caso seja necessário instalar alguma dependência para concluir a configuração. Ignore as marcas de seleção que não são relevantes para você (por exemplo, Xcode se você não pretende desenvolver para iOS).

 flutter doctor

Perguntas frequentes

Está continuando do MDC-101?

Se você concluiu o MDC-101, o código para este codelab já está pronto. Pular para a etapa: Adicionar uma barra de apps superior.

Está começando do zero?

Fazer o download do app inicial do codelab

Fazer o download do app inicial

O app inicial está localizado no diretório material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series.

... ou clone-o do GitHub

Para clonar este codelab do GitHub, execute os seguintes comandos:

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs/mdc_100_series
git checkout 102-starter_and_101-complete

Configurar o projeto

As instruções a seguir presumem que você esteja usando o Android Studio (IntelliJ).

Abrir o projeto

1. Abra o Android Studio.

2. Se a tela de boas-vindas for exibida, clique em Open an existing Android Studio project.

3. Navegue até o diretório material-components-flutter-codelabs/mdc_100_series e clique em "Open". O projeto será aberto. Você pode ignorar qualquer erro exibido no Dart Analysis antes de criar o projeto pela primeira vez.

4. Se solicitado:

  • Instale todas as atualizações de plataforma e de plug-ins ou do FlutterRunConfigurationType.
  • Se o SDK do Dart ou do Flutter não estiver configurado, defina o caminho do SDK do Flutter para o plug-in do Flutter (link em inglês).
  • Configure os frameworks do Android.
  • Clique em "Get dependencies" ou em "Run ‘flutter packages get'".

Em seguida, reinicie o Android Studio.

Executar o app inicial

As instruções a seguir presumem que você está testando em um dispositivo ou emulador Android, mas também é possível testar em um simulador ou dispositivo iOS com o Xcode instalado.

1. Selecione o dispositivo ou emulador. Caso o emulador Android ainda não esteja em execução, selecione Tools -> Android -> AVD Manager para criar um dispositivo virtual e iniciar o emulador. Se você já tiver um AVD, é possível iniciar o emulador diretamente do seletor de dispositivos no Android Studio, como mostrado na próxima etapa. Para o iOS Simulator, se ele ainda não estiver em execução, inicie o simulador na máquina de desenvolvimento selecionando Flutter Device Selection -> Open iOS Simulator.

2. Inicie o app do Flutter:

  • Localize o menu suspenso "Flutter Device Selection" na parte superior da tela do editor e selecione o dispositivo (por exemplo, iPhone SE ou o SDK do Android criado para <versão>).
  • Pressione o ícone Play ().

Pronto! Você verá a página de login do Shrine do codelab MDC-101 no simulador ou emulador.

Android

iOS

Agora que a tela de login está pronta, vamos preencher o app com alguns produtos.

Agora, se você clicar no botão "Next", poderá ver a tela inicial com a mensagem "You did it!". Isso é ótimo. Mas agora o usuário não pode realizar nenhuma outra ação e também não sabe em que parte do app está. Vamos adicionar a navegação para resolver isso.

O Material Design oferece padrões de navegação que garantem uma boa usabilidade. Um dos componentes mais visíveis é a barra de apps superior.

Para que os usuários possam navegar e ter acesso rápido a outras ações, vamos adicionar uma barra de apps superior.

Adicionar um widget AppBar

Em home.dart, adicione o widget AppBar ao Scaffold:

  // TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
  ),

Adicionar o AppBar ao campo appBar: do Scaffold oferece um layout perfeito, exibindo o AppBar na parte superior da página com o corpo abaixo.

Salve o projeto. Quando o app Shrine for atualizado, clique em Next para ver a tela inicial.

Android

iOS

O AppBar está ótimo, mas precisa de um título.

Adicionar um widget Text

Em home.dart, adicione um título ao AppBar:

// TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
    title: Text('SHRINE'),
    // TODO: Add trailing buttons (102)

Salve o projeto.

Android

iOS

Muitas barras de apps têm um botão ao lado do título. Vamos adicionar um ícone de menu ao app.

Adicionar um IconButton inicial

Ainda no home.dart, defina um IconButton para o campo leading: do AppBar. Coloque-o antes do campo title: para copiar a ordem do início ao fim:

    // TODO: Add buttons and title (102)
    leading: IconButton(
      icon: Icon(
        Icons.menu,
        semanticLabel: 'menu',
      ),
      onPressed: () {
        print('Menu button');
      },
    ),

Salve o projeto.

Android

iOS

O ícone de menu (também conhecido como "hamburguer") será exibido exatamente onde você esperava.

Também é possível adicionar botões ao lado final do título. No Flutter, eles são conhecidos como "ações".

Adicionar ações

Há espaço para adicionar mais dois IconButtons.

Adicione-os à instância do AppBar após o título:

// TODO: Add trailing buttons (102)
actions: <Widget>[
  IconButton(
    icon: Icon(
      Icons.search,
      semanticLabel: 'search',
    ),
    onPressed: () {
      print('Search button');
    },
  ),
  IconButton(
    icon: Icon(
      Icons.tune,
      semanticLabel: 'filter',
    ),
    onPressed: () {
      print('Filter button');
    },
  ),
],

Salve o projeto. A tela inicial ficará assim:

Android

iOS

Agora, o app tem um botão inicial, um título e duas ações no lado direito. A barra do app também tem uma elevação representada por uma sombra sutil que mostra que a barra está em uma camada diferente da do conteúdo.

Agora que nosso app tem uma estrutura básica, vamos organizar o conteúdo colocando-o em cards.

Adicionar um GridView

Para começar, vamos adicionar um card abaixo da barra de apps superior. Por enquanto, o widget Card sozinho não tem informações suficientes para ser exibido. Por isso, queremos encapsulá-lo em um widget GridView.

Substitua o centro do corpo do Scaffold por um GridView:

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  // TODO: Build a grid of cards (102)
  children: <Widget>[Card()],
),

Vamos descompactar esse código. O GridView invocará o construtor count(), já que o número de itens exibidos é contável, ou seja, não é infinito. No entanto, algumas informações são necessárias para definir o layout.

O crossAxisCount: especifica quantos itens serão exibidos. Queremos exibir duas colunas.

O campo padding: fornece espaço para os quatro lados do GridView. Não é possível ver o preenchimento dos lados finais ou inferiores porque ainda não há filhos do GridView ao lado deles.

O campo childAspectRatio: identifica o tamanho dos itens com base na proporção (largura dividida pela altura).

Por padrão, o GridView cria blocos com tamanhos iguais.

Ao fazer isso, o GridView calcula a largura de cada filho desta forma: ([width of the entire grid] - [left padding] - [right padding]) / number of columns. Usando os valores que temos: ([width of the entire grid] - 16 - 16) / 2.

A altura é calculada usando a largura e aplicando a proporção: ([width of the entire grid] - 16 - 16) / 2 * 9 / 8. Invertemos os números 8 e 9 porque partimos da largura para calcularmos a altura, e não o contrário.

Agora, temos um card, mas ele está vazio. Vamos adicionar widgets filhos ao nosso card.

Criar o layout com o conteúdo

Os cards precisam ter regiões para uma imagem, um título e um texto secundário.

Atualize os filhos do GridView:

// TODO: Build a grid of cards (102)
children: <Widget>[
  Card(
    clipBehavior: Clip.antiAlias,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 18.0 / 11.0,
          child: Image.asset('assets/diamond.png'),
        ),
        Padding(
          padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

Esse código adiciona um widget Column usado para exibir os widgets filhos verticalmente em uma coluna.

O crossAxisAlignment: field especifica CrossAxisAlignment.start. Isso significa "alinhar o texto com a borda inicial".

O widget AspectRatio define o formato da imagem, independentemente do tipo dela.

O Padding afasta levemente o texto da lateral.

Os dois widgets Text são dispostos verticalmente com 8 pontos de espaço vazio entre eles (SizedBox). Usaremos o widget Column para criar outra coluna que armazenará os widgets Text no Padding.

Salve o projeto:

Android

iOS

Nesta prévia, é possível ver que o card se enquadra na borda, com cantos arredondados e uma sombra (que representa a elevação dele). No Material Design, a forma como um todo é conhecida como um "contêiner". Não confunda essa forma com a classe de widget Container.

Os cards geralmente são exibidos com outros em uma coleção. Vamos exibi-los como uma coleção em uma grade.

Sempre que vários cards forem exibidos em uma tela, eles serão agrupados em uma ou mais coleções. Os cards de uma coleção são coplanares, ou seja, compartilham a mesma elevação de repouso (a menos que os cards sejam retirados ou arrastados, mas não faremos isso neste codelab).

Multiplicar o card em uma coleção

Agora, nosso card é construído in-line no campo children: do GridView. Ele tem um código aninhado grande que pode ser difícil de ler. Vamos extraí-lo para uma função que pode gerar quantos cards vazios quisermos e retornar uma lista de cards.

Crie uma nova função particular acima da função build() (as funções que começam com um sublinhado são APIs particulares):

// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) => Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18.0 / 11.0,
            child: Image.asset('assets/diamond.png'),
          ),
          Padding(
            padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text('Title'),
                SizedBox(height: 8.0),
                Text('Secondary Text'),
              ],
            ),
          ),
        ],
      ),
    ),
  );

  return cards;
}

Atribua os cards gerados ao campo children do GridView. Lembre-se de substituir todo o conteúdo do GridView por esse novo código:

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(10) // Replace
),

Salve o projeto.

Android

iOS

Os cards já existem, mas ainda não mostram nada. Vamos adicionar alguns dados de produtos.

Adicionar dados de produtos

O app tem alguns produtos com imagens, nomes e preços. Vamos adicioná-lo aos widgets que já criamos no card.

Em seguida, em home.dart, importe um novo pacote e alguns arquivos que fornecemos para um modelo de dados:

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import 'model/products_repository.dart';
import 'model/product.dart';

Por fim, mude o método _buildGridCards() para buscar as informações do produto e use esses dados nos cards:

// TODO: Make a collection of cards (102)

// Replace this entire method
List<Card> _buildGridCards(BuildContext context) {
  List<Product> products = ProductsRepository.loadProducts(Category.all);

  if (products == null || products.isEmpty) {
    return const <Card>[];
  }

  final ThemeData theme = Theme.of(context);
  final NumberFormat formatter = NumberFormat.simpleCurrency(
      locale: Localizations.localeOf(context).toString());

  return products.map((product) {
    return Card(
      clipBehavior: Clip.antiAlias,
      // TODO: Adjust card heights (103)
      child: Column(
        // TODO: Center items on the card (103)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18 / 11,
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
             // TODO: Adjust the box size (102)
            ),
          ),
          Expanded(
            child: Padding(
              padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
               // TODO: Align labels to the bottom and center (103)
               crossAxisAlignment: CrossAxisAlignment.start,
                // TODO: Change innermost Column (103)
                children: <Widget>[
                 // TODO: Handle overflowing labels (103)
                 Text(
                    product.name,
                    style: theme.textTheme.headline6,
                    maxLines: 1,
                  ),
                  SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.subtitle2,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

OBSERVAÇÃO: o app ainda não poderá ser compilado nem executado. Temos que fazer mais uma mudança.

Além disso, altere a função build() para transmitir o BuildContext para o método _buildGridCards() antes de tentar compilar o código:

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(context) // Changed code
),

Android

iOS

Talvez você tenha percebido que não adicionamos nenhum espaço vertical entre os cards. Isso acontece porque eles têm, por padrão, quatro pontos de preenchimento nas partes superiores e inferiores.

Salve o projeto:

Os dados do produto são exibidos, mas as imagens têm espaço extra ao redor delas. Por padrão, as imagens são exibidas pelo widget BoxFit de .scaleDown (nesse caso). Vamos alterar isso para usar .fitWidth para aumentar o zoom e remover o espaço em branco extra das imagens.

Adicione um campo fit: à imagem com um valor de BoxFit.fitWidth:

  // TODO: Adjust the box size (102)
  fit: BoxFit.fitWidth,

Android

iOS

Agora, nossos produtos estão sendo exibido perfeitamente no app!

Nosso app tem um fluxo básico que leva o usuário da tela de login a uma tela inicial, onde os produtos são exibidos. Em apenas algumas linhas de código, adicionamos uma barra de apps superior (com um título e três botões) e cards (para exibir o conteúdo do nosso app). Nossa tela inicial agora é simples e funcional, com uma estrutura básica e um conteúdo acionável.

Próximas etapas

Usamos quatro componentes principais da biblioteca MDC-Flutter, a barra de apps, o card, o campo de texto e o botão. Para ver ainda mais componentes, acesse o catálogo de widgets do Flutter (em inglês).

Embora nosso app funcione perfeitamente, ele ainda não representa uma marca ou ponto de vista específicos. No codelab MDC-103: temas do Material Design usando cor, forma, elevação e tipo, vamos personalizar o estilo desses componentes para representar uma marca vibrante e moderna.

Próximo codelab

Este codelab exigiu esforço e tempo normais para ser concluído

Concordo totalmente Concordo Não concordo nem discordo Discordo Discordo totalmente

Quero continuar usando componentes do Material Design no futuro

Concordo totalmente Concordo Não concordo nem discordo Discordo Discordo totalmente