Como criar um app da Cupertino com o Flutter

Este é o codelab Cupertino com o Flutter.

Nele, você criará um app da Cupertino para iOS usando o Flutter. O SDK do Flutter tem duas bibliotecas de widgets com estilo, além da biblioteca de widgets básica (link em inglês):

  • Os widgets do Material Design (link em inglês) implementam a linguagem do Material Design para iOS, Android, Web e computadores.
  • Os widgets da Cupertino (link em inglês) implementam a linguagem de design atual do iOS com base nas Diretrizes de Interface Humana da Apple.

Por que criar um app Cupertino? A linguagem do Material Design foi criada para qualquer plataforma, não apenas para Android. Quando um app é criado com o Material Design no Flutter, ele tem a aparência do Material Design em todos os dispositivos, inclusive iOS. Se você quiser que seu app tenha a aparência padrão de um app iOS, precisará usar a biblioteca Cupertino.

Tecnicamente, é possível executar um app da Cupertino no Android ou no iOS. Mas, devido a questões de licenciamento, a Cupertino não exibirá as fontes corretas no Android. Por isso, use um dispositivo iOS ao criar um app Cupertino.

Você implementará um app de compras com o estilo da Cupertino contendo três guias: uma para a lista de produtos, outra para a pesquisa de produtos e a terceira para o carrinho de compras.

f104a94356854c24.png 6f345bfa17663f9a.png

daf61aa9d823646a.png

O que você aprenderá neste codelab

  • Como criar um app do Flutter com o comportamento e a aparência do iOS.
  • Como criar várias guias e navegar entre elas.
  • Como usar o pacote provider para gerenciar o estado das telas.

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 laboratório: o SDK do Flutter e um editor (links em inglês). Use seu editor preferido, como o Android Studio ou o IntelliJ com os plug-ins do Flutter e do Dart instalados, ou o Visual Studio Code com as extensões do Dart Code e do Flutter (link em inglês).

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

  • Um dispositivo iOS físico conectado ao computador (link em inglês).
  • O iOS Simulator (link em inglês).

Você também precisará de:

  • Um computador Mac configurado com o Xcode.

Crie o app inicial usando um CupertinoPageScaffold.

b2f84ff91b0e1396.png Crie um projeto do Flutter chamado cupertino_store e migre-o para um sistema de segurança contra nulidade da seguinte maneira:

$ flutter create cupertino_store
$ cd cupertino_store
$ dart migrate --apply-changes

b2f84ff91b0e1396.png Substitua o conteúdo do lib/main.dart. Exclua todo o código do lib/main.dart, o que criará um app para contagem de pressionamentos de botão com o tema do Material Design. Substitua-o pelo código a seguir, que inicializa um app da Cupertino.

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

import 'package:flutter/cupertino.dart';

import 'app.dart';

void main() {
  return runApp(CupertinoStoreApp());
}

cf1e10b838bf60ee.png Observações

  • Importe o pacote Cupertino. Isso disponibiliza todos os widgets e constantes da Cupertino para seu app.

b2f84ff91b0e1396.png Crie o lib/styles.dart. Adicione um arquivo ao diretório lib chamado styles.dart. A classe Styles define o estilo do texto e das cores para personalizar o app. Um exemplo do arquivo é apresentado a seguir, mas você pode buscar o conteúdo completo no GitHub: lib/styles.dart (link em inglês).

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

// THIS IS A SAMPLE FILE. Get the full content at the link above.
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';

abstract class Styles {
  static const TextStyle productRowItemName = TextStyle(
    color: Color.fromRGBO(0, 0, 0, 0.8),
    fontSize: 18,
    fontStyle: FontStyle.normal,
    fontWeight: FontWeight.normal,
  );

  static const TextStyle productRowTotal = TextStyle(
    color: Color.fromRGBO(0, 0, 0, 0.8),
    fontSize: 18,
    fontStyle: FontStyle.normal,
    fontWeight: FontWeight.bold,
  );

 // ...
// THIS IS A SAMPLE FILE. Get the full content at the link above.

cf1e10b838bf60ee.png Observações

  • É possível centralizar as definições de estilo de uma maneira semelhante a como os desenvolvedores da Web centralizam a marcação de estilo em arquivos CSS, agrupando todas as definições em um único arquivo. Essa é a maneira mais fácil de reutilizar e redefinir estilos em todo o app.

b2f84ff91b0e1396.png Crie o lib/app.dart e adicione a classe CupertinoStoreApp. Adicione a seguinte classe CupertinoStoreApp ao lib/app.dart.

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

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';

class CupertinoStoreApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // This app is designed only to work vertically, so we limit
    // orientations to portrait up and down.
    SystemChrome.setPreferredOrientations(
        [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

    return CupertinoApp(
      theme: const CupertinoThemeData(brightness: Brightness.light),
      home: CupertinoStoreHomePage(),
    );
  }
}

cf1e10b838bf60ee.png Observações

  • Importe a biblioteca de serviços (link em inglês). Com isso, os serviços da plataforma ficarão disponíveis para o app, como a área de transferência e a definição da orientação do dispositivo.
  • Instancie o CupertinoApp, que oferece temas, navegação, direção do texto e outros padrões necessários para criar um app que atenda às expectativas de um usuário do iOS.
  • Instancie a CupertinoStoreHomePage como a página inicial.
  • O app foi projetado para funcionar somente na vertical. Portanto, a orientação do dispositivo é limitada ao modo retrato.

b2f84ff91b0e1396.png Adicione a classe CupertinoStoreHomePage. Adicione a seguinte classe CupertinoStoreHomePage ao lib/app.dart para criar o layout da página inicial:

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

class CupertinoStoreHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Cupertino Store'),
      ),
      child: SizedBox(),
    );
  }
}

cf1e10b838bf60ee.png Observações

  • O pacote Cupertino oferece dois tipos de scaffolds de página. O CupertinoPageScaffold é compatível com páginas únicas e aceita uma barra de navegação e uma cor no plano de fundo no estilo da Cupertino, além de conter a árvore de widgets da página. Você aprenderá sobre o segundo tipo de scaffold na próxima etapa.
  • A página tem um título, e a árvore de widgets contém um contêiner vazio.

b2f84ff91b0e1396.png Atualize o arquivo pubspec.yaml. Na parte superior do projeto, edite o arquivo pubspec.yaml. Adicione as bibliotecas necessárias e uma lista de recursos de imagem. Um exemplo do arquivo é apresentado a seguir, e o conteúdo completo está disponível no GitHub: pubspec.yaml (link em inglês).

pubspec.yaml (link em inglês)

# THIS IS A SAMPLE OF THE FILE. Get the full file at the link above.
name: cupertino_store
description: Creating a Store in Cupertino widgets
publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=2.12.0 <3.0.0"

dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  intl: ^0.17.0
  provider: ^5.0.0
  shrine_images: ^2.0.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  pedantic: ^1.11.0

flutter:
  assets:
    - packages/shrine_images/0-0.jpg
# THIS IS A SAMPLE OF THE FILE. Get the full file at the link above.

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

  • Isso extrai vários pacotes, incluindo shine_images, que contêm produtos para preencher a loja.
  • O pacote provider oferece uma forma simples de gerenciar o estado das telas.
  • O pacote intl oferece recursos de internacionalização e localização.
  • O pacote cupertino_icons contém recursos de ícone para os widgets da Cupertino.

b2f84ff91b0e1396.png Execute o app (link em inglês). Você verá a seguinte tela em branco com a barra de navegação da Cupertino e um título:

5705e4da178665a5.png

Problemas?

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

O app final terá três guias:

  • Lista de produtos
  • Pesquisa de produtos
  • Carrinho de compras

Nesta etapa, você atualizará a página inicial para incluir as três guias usando um CupertinoTabScaffold. Você também adicionará uma fonte de dados que fornece a lista de itens à venda, com fotos e preços.

Na etapa anterior, você criou uma classe CupertinoStoreHomePage usando um CupertinoPageScaffold. Use esse scaffold para páginas sem guias. O app final terá três guias, então é necessário trocar o CupertinoPageScaffold por um CupertinoTabScaffold.

A guia da Cupertino tem um scaffold separado porque, no iOS, a guia inferior normalmente é persistente acima das rotas aninhadas, e não dentro das páginas.

b2f84ff91b0e1396.png Atualize o lib/app.dart. Substitua a classe CupertinoStoreHomePage pela seguinte, que configura um scaffold de três guias:

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

class CupertinoStoreHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.home),
            label: 'Products',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.search),
            label: 'Search',
          ),
          BottomNavigationBarItem(
            icon: Icon(CupertinoIcons.shopping_cart),
            label: 'Cart',
          ),
        ],
      ),
      tabBuilder: (context, index) {
        late final CupertinoTabView returnValue;
        switch (index) {
          case 0:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ProductListTab(),
              );
            });
            break;
          case 1:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: SearchTab(),
              );
            });
            break;
          case 2:
            returnValue = CupertinoTabView(builder: (context) {
              return CupertinoPageScaffold(
                child: ShoppingCartTab(),
              );
            });
            break;
        }
        return returnValue;
      },
    );
  }
}

cf1e10b838bf60ee.png Observações

  • Para não gerar erros no momento de execução, a CupertinoTabBar requer pelo menos dois itens.
  • O tabBuilder: é responsável por garantir que a guia especificada seja criada. Nesse caso, ele chama um construtor de classe para configurar cada guia, unindo as três em CupertinoTabView e CupertinoPageScaffold.

b2f84ff91b0e1396.png Adicione classes de stub para o conteúdo das novas guias. Crie um arquivo lib/product_list_tab.dart para a primeira guia que seja compilado de forma clara, mas exiba apenas uma tela em branco. Use o seguinte conteúdo:

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

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';

class ProductListTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return const CustomScrollView(
          slivers: <Widget>[
            CupertinoSliverNavigationBar(
              largeTitle: Text('Cupertino Store'),
            ),
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Observações

  • A guia da lista de produtos é um widget sem estado.
  • O Consumer, do pacote provider, ajuda no gerenciamento do estado. Veja mais informações sobre o modelo adiante.
  • Existem duas variantes da barra de navegação no iOS. A pequena, comum e estática, usada desde o iOS 1, e a grande, com título rolável, introduzida no iOS 11. Nesta página, o segundo tipo é implementado em uma CustomScrollView com um widget CupertinoSliverNavigationBar.

b2f84ff91b0e1396.png Adicione um stub à página de pesquisa. Crie um arquivo lib/search_tab.dart que seja compilado de forma clara, mas que exiba apenas uma tela em branco. Use o seguinte conteúdo:

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

import 'package:flutter/cupertino.dart';

class SearchTab extends StatefulWidget {
  @override
  _SearchTabState createState() {
    return _SearchTabState();
  }
}

class _SearchTabState extends State<SearchTab> {
  @override
  Widget build(BuildContext context) {
    return const CustomScrollView(
      slivers: <Widget>[
        CupertinoSliverNavigationBar(
          largeTitle: Text('Search'),
        ),
      ],
    );
  }
}

cf1e10b838bf60ee.png Observações

  • A guia de pesquisa é um widget com estado, porque a lista de resultados muda à medida que o usuário faz pesquisas.

b2f84ff91b0e1396.png Adicione um stub à página do carrinho de compras. Crie um arquivo lib/shopping_cart_tab.dart que seja compilado de forma clara, mas que exiba apenas uma tela em branco. Use o seguinte conteúdo:

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

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';

class ShoppingCartTab extends StatefulWidget {
  @override
  _ShoppingCartTabState createState() {
    return _ShoppingCartTabState();
  }
}

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return const CustomScrollView(
          slivers: <Widget>[
            CupertinoSliverNavigationBar(
              largeTitle: Text('Shopping Cart'),
            ),
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Observações

  • A guia do carrinho de compras é um widget com estado, porque mantém a lista de compras e as informações do cliente.
  • Essa página também usa uma CustomScrollView.

b2f84ff91b0e1396.png Atualize o lib/app.dart. Atualize as instruções de importação no lib/app.dart para extrair os widgets da nova guia:

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

import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'product_list_tab.dart';   // NEW
import 'search_tab.dart';         // NEW
import 'shopping_cart_tab.dart';  // NEW

Na segunda parte desta etapa, na próxima página, você adicionará um código para gerenciar e compartilhar o estado entre as guias.

O app tem alguns dados comuns que precisam ser compartilhados entre várias telas. Portanto, você precisa de uma forma simples de enviar os dados para cada um dos objetos que precisam deles. O pacote provider oferece uma maneira fácil de fazer isso. No provider, defina o modelo de dados e use o ChangeNotifierProvider para introduzir seu modelo de dados na árvore.

b2f84ff91b0e1396.png Crie as classes de modelo de dados. Crie um diretório model no lib. Adicione um arquivo lib/model/product.dart que defina os dados do produto provenientes da fonte de dados:

lib/model/product.dart (link em inglês)

enum Category {
  all,
  accessories,
  clothing,
  home,
}

class Product {
  const Product({
    required this.category,
    required this.id,
    required this.isFeatured,
    required this.name,
    required this.price,
  });

  final Category category;
  final int id;
  final bool isFeatured;
  final String name;
  final int price;

  String get assetName => '$id-0.jpg';
  String get assetPackage => 'shrine_images';

  @override
  String toString() => '$name (id=$id)';
}

cf1e10b838bf60ee.png Observações

  • Cada instância da classe Product descreve um produto que está à venda.

A classe ProductsRepository contém a lista completa de produtos à venda, além do preço, texto do título e categoria. Nosso app não fará nada com a propriedade isFeatured. A classe também inclui um método loadProducts() que retorna todos os produtos ou os produtos de determinada categoria.

b2f84ff91b0e1396.png Crie o repositório de produtos. Crie um arquivo lib/model/products_repository.dart. Esse arquivo contém todos os produtos à venda. Cada produto pertence a uma categoria. Um exemplo do arquivo é apresentado a seguir, mas o conteúdo completo está disponível no GitHub: products_repository.dart (link em inglês).

lib/model/products_repository.dart (link em inglês)

// THIS IS A SAMPLE FILE. Get the full content at the link above.

import 'product.dart';

class ProductsRepository {
 static const _allProducts = <Product>[
   Product(
     category: Category.accessories,
     id: 0,
     isFeatured: true,
     name: 'Vagabond sack',
     price: 120,
   ),
   Product(
     category: Category.home,
     id: 9,
     isFeatured: true,
     name: 'Gilt desk trio',
     price: 58,
   ),
   Product(
     category: Category.clothing,
     id: 33,
     isFeatured: true,
     name: 'Cerise scallop tee',
     price: 42,
   ),
   // THIS IS A SAMPLE FILE. Get the full content at the link above.
 ];

 static List<Product> loadProducts(Category category) {
   if (category == Category.all) {
     return _allProducts;
   } else {
     return _allProducts.where((p) => p.category == category).toList();
   }
 }
}

cf1e10b838bf60ee.png Observations

  • Nesse caso, estamos criando um banco de dados de produtos fictício para facilitar o desenvolvimento, mas essas informações precisarão ser fornecidas ao app por uma API. Uma forma fácil de fazer isso, abordando a realidade parcialmente desconexa dos smartphones, é com o Cloud Firestore.

Agora está tudo pronto para definir o modelo. Crie um arquivo lib/model/app_state_model.dart. Na classe AppStateModel, forneça métodos para acessar dados do modelo. Por exemplo, adicione um método para acessar o total do carrinho de compras, outro para a lista de produtos selecionados para compra, outro para o custo do frete e assim por diante.

b2f84ff91b0e1396.png Crie a classe de modelo. A lista de assinaturas de método fornecida por essa classe é apresentada a seguir. O conteúdo completo está disponível no GitHub: lib/model/app_state_model.dart (link em inglês).

lib/model/app_state_model.dart (link em inglês)

// THIS IS A SAMPLE FILE ONLY. Get the full content at the link above.

import 'package:flutter/foundation.dart' as foundation;

import 'product.dart';
import 'products_repository.dart';

double _salesTaxRate = 0.06;
double _shippingCostPerItem = 7;

class AppStateModel extends foundation.ChangeNotifier {
 List<Product> _availableProducts = [];
 Category _selectedCategory = Category.all;
 final _productsInCart = <int, int>{};

 Map<int, int> get productsInCart
 int get totalCartQuantity
 Category get selectedCategory
 double get subtotalCost
 double get shippingCost

 double get tax
 double get totalCost
 List<Product> getProducts()
 List<Product> search(String searchTerms)
 void addProductToCart(int productId)
 void removeItemFromCart(int productId)
 Product getProductById(int id)
 void clearCart()
 void loadProducts()
 void setCategory(Category newCategory)
// THIS IS A SAMPLE FILE ONLY. Get the full content at the link above.

cf1e10b838bf60ee.png Observations

  • O AppStateModel apresenta uma forma de centralizar o estado do aplicativo e disponibilizá-lo para todo o app. Nas próximas etapas, usaremos esse estado para direcionar a funcionalidade de pesquisa e do carrinho de compras.

b2f84ff91b0e1396.png Atualize o lib/main.dart. No método main(), inicialize o modelo. Adicione as linhas marcadas com NEW.

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

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';             // NEW

import 'app.dart';
import 'model/app_state_model.dart';                 // NEW

void main() {
 return runApp(
   ChangeNotifierProvider<AppStateModel>(            // NEW
     create: (_) => AppStateModel()..loadProducts(), // NEW
     child: CupertinoStoreApp(),                     // NEW
   ),
 );
}

cf1e10b838bf60ee.png Observations

  • Conectamos o AppStateModel na parte superior da árvore de widgets para que ele fique disponível em todo o app.
  • Usamos o ChangeNotifierProvider do pacote provider (links em inglês), que monitora o AppStateModel, para enviar notificações sobre mudanças.

b2f84ff91b0e1396.png Execute o app (link em inglês). A tela em branco a seguir será exibida, com a barra de navegação, um título e uma gaveta da Cupertino com três ícones rotulados, que representam as três guias. É possível alternar entre as guias, mas as três páginas estão em branco no momento.

35520995039d98a6.png

Problemas?

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

Nesta etapa, você exibirá os produtos à venda na guia da lista de produtos.

b2f84ff91b0e1396.png Adicione o lib/product_row_item.dart para exibir os produtos. Crie o lib/product_row_item.dart file com o seguinte conteúdo:

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

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';
import 'model/product.dart';
import 'styles.dart';

class ProductRowItem extends StatelessWidget {
  const ProductRowItem({
    required this.product,
    required this.lastItem,
  });

  final Product product;
  final bool lastItem;

  @override
  Widget build(BuildContext context) {
    final row = SafeArea(
      top: false,
      bottom: false,
      minimum: const EdgeInsets.only(
        left: 16,
        top: 8,
        bottom: 8,
        right: 8,
      ),
      child: Row(
        children: <Widget>[
          ClipRRect(
            borderRadius: BorderRadius.circular(4),
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
              fit: BoxFit.cover,
              width: 76,
              height: 76,
            ),
          ),
          Expanded(
            child: Padding(
              padding: const EdgeInsets.symmetric(horizontal: 12),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.start,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[
                  Text(
                    product.name,
                    style: Styles.productRowItemName,
                  ),
                  const Padding(padding: EdgeInsets.only(top: 8)),
                  Text(
                    '\$${product.price}',
                    style: Styles.productRowItemPrice,
                  )
                ],
              ),
            ),
          ),
          CupertinoButton(
            padding: EdgeInsets.zero,
            onPressed: () {
              final model = Provider.of<AppStateModel>(context, listen: false);
              model.addProductToCart(product.id);
            },
            child: const Icon(
              CupertinoIcons.plus_circled,
              semanticLabel: 'Add',
            ),
          ),
        ],
      ),
    );

    if (lastItem) {
      return row;
    }

    return Column(
      children: <Widget>[
        row,
        Padding(
          padding: const EdgeInsets.only(
            left: 100,
            right: 16,
          ),
          child: Container(
            height: 1,
            color: Styles.productRowDivider,
          ),
        ),
      ],
    );
  }
}

cf1e10b838bf60ee.png Observations

  • A CupertinoSliverNavigationBar (link em inglês) define a forma como conseguimos os títulos de expansão no estilo do iOS 11 na barra de navegação. Isso é importante para que um usuário do iOS se sinta familiarizado com o layout do app.
  • Esse arquivo ficará bastante complexo à medida que emularmos a aparência altamente refinada dos aplicativos iOS. O Flutter oferece a vantagem de poder fazer essas mudanças em um editor e visualizá-las praticamente em tempo real, graças à recarga dinâmica com estado.

b2f84ff91b0e1396.png No lib/product_list_tab.dart, importe o arquivo product_row_item.dart.

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

import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';

import 'model/app_state_model.dart';
import 'product_row_item.dart';      // NEW

b2f84ff91b0e1396.png Extraia a lista de produtos e o número de produtos do método build() da ProductListTab. Adicione as novas linhas indicadas abaixo:

class ProductListTab extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return CupertinoPageScaffold(
     child: Consumer<AppStateModel>(
       builder: (context, child, model) {
         final products = model.getProducts();  // NEW
         return CustomScrollView(
           semanticChildCount: products.length, // NEW
           slivers: <Widget>[
             CupertinoSliverNavigationBar(
               largeTitle: const Text('Cupertino Store'),
             ),
           ],
         );
       },
     ),
   );
 }
}

b2f84ff91b0e1396.png Também no método build(), adicione um novo sliver à lista de widgets desse tipo para abrigar a lista de produtos. Adicione as novas linhas indicadas abaixo:

class ProductListTab extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        final products = model.getProducts();
        return CustomScrollView(
          semanticChildCount: products.length,
          slivers: <Widget>[
            const CupertinoSliverNavigationBar(
              largeTitle: Text('Cupertino Store'),
            ),
            SliverSafeArea(   // BEGINNING OF NEW CONTENT
              top: false,
              minimum: const EdgeInsets.only(top: 8),
              sliver: SliverList(
                delegate: SliverChildBuilderDelegate(
                  (context, index) {
                    if (index < products.length) {
                      return ProductRowItem(
                        product: products[index],
                        lastItem: index == products.length - 1,
                      );
                    }

                    return null;
                  },
                ),
              ),
            )     // END OF NEW CONTENT
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Observations

  • O entalhe é contabilizado pelo primeiro sliver (a CupertinoSliverNavigationBar).
  • O novo sliver e o primeiro são irmãos, e não pai e filho. Por isso, o primeiro sliver não consegue informar que já consumiu o entalhe. Assim, o segundo sliver define a propriedade top da SliverSafeArea como false para que ela ignore o entalhe.
  • As propriedades left e right da SliverSafeArea continuam definidas como true por padrão caso o smartphone seja girado. Elas ainda são contabilizadas pelo bottom para que o sliver possa rolar para além da barra inferior da página inicial, a fim de evitar obstruções ao rolar a tela até o fim.
  • Essa lógica não é especificamente obrigatória nesse caso, porque o app é restrito ao modo retrato. Mas, ao incluí-la, o código poderá ser reutilizado em apps que processam a exibição na horizontal.

b2f84ff91b0e1396.png Execute o app (link em inglês). Na guia de produtos, você verá uma lista de produtos com imagens, preços e um botão com sinal de adição para incluir o produto no carrinho de compras. O botão será implementado mais tarde, na etapa em que você criará o carrinho de compras.

f104a94356854c24.png

Problemas?

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

Nesta etapa, você criará a guia de pesquisa e adicionará o recurso de pesquisar produtos.

b2f84ff91b0e1396.png Atualize as importações no lib/search_tab.dart.

Adicione importações para as classes que serão usadas pela guia de pesquisa:

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

import 'package:flutter/cupertino.dart'
import 'package:provider/provider.dart'
import 'model/app_state_model.dart'
import 'product_row_item.dart'
import 'search_bar.dart'
import 'styles.dart'

b2f84ff91b0e1396.png Atualize o método build() no _SearchTabState.

Inicialize o modelo e substitua a CustomScrollView por componentes individuais para pesquisa e informações dos produtos.

class _SearchTabState extends State<SearchTab> {
// ...

  @override
  Widget build(BuildContext context) {
    final model = Provider.of<AppStateModel>(context);
    final results = model.search(_terms);

    return DecoratedBox(
      decoration: const BoxDecoration(
        color: Styles.scaffoldBackground,
      ),
      child: SafeArea(
        child: Column(
          children: [
            _buildSearchBox(),
            Expanded(
              child: ListView.builder(
                itemBuilder: (context, index) => ProductRowItem(
                  product: results[index],
                  lastItem: index == results.length - 1,
                ),
                itemCount: results.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

cf1e10b838bf60ee.png Observations

  • Estamos recriando uma experiência de pesquisa no estilo do iOS, mas ainda existem muitas opções para personalizar a experiência do usuário.

b2f84ff91b0e1396.png Adicione variáveis, funções e métodos compatíveis à classe _SearchTabState.

Eles incluem initState(), dispose(), _onTextChanged() e _buildSearchBox(), conforme mostrado abaixo:

class _SearchTabState extends State<SearchTab> {
  late final TextEditingController _controller;
  late final FocusNode _focusNode;
  String _terms = '';

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController()..addListener(_onTextChanged);
    _focusNode = FocusNode();
  }

  @override
  void dispose() {
    _focusNode.dispose();
    _controller.dispose();
    super.dispose();
  }

  void _onTextChanged() {
    setState(() {
      _terms = _controller.text;
    });
  }

  Widget _buildSearchBox() {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: SearchBar(
        controller: _controller,
        focusNode: _focusNode,
      ),
    );
  }    // TO HERE

 @override
 Widget build(BuildContext context) {

cf1e10b838bf60ee.png Observations

  • O _SearchTabState é o local em que o estado específico da pesquisa é mantido. Nessa implementação, vamos armazenar os termos de pesquisa e conectá-los ao AppStateModel para realizar a pesquisa. Em casos em que um back-end de API é implementado, ele é o lugar ideal para acessar a rede para fazer a pesquisa.

b2f84ff91b0e1396.png Adicione uma classe SearchBar.

Crie um novo arquivo, lib/search_bar.dart. A classe SearchBar processa a pesquisa na lista de produtos. Insira o seguinte conteúdo no arquivo:

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

import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';
import 'styles.dart';

class SearchBar extends StatelessWidget {
  const SearchBar({
    required this.controller,
    required this.focusNode,
  });

  final TextEditingController controller;
  final FocusNode focusNode;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: Styles.searchBackground,
        borderRadius: BorderRadius.circular(10),
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(
          horizontal: 4,
          vertical: 8,
        ),
        child: Row(
          children: [
            const Icon(
              CupertinoIcons.search,
              color: Styles.searchIconColor,
            ),
            Expanded(
              child: CupertinoTextField(
                controller: controller,
                focusNode: focusNode,
                style: Styles.searchText,
                cursorColor: Styles.searchCursorColor,
                decoration: null,
              ),
            ),
            GestureDetector(
              onTap: controller.clear,
              child: const Icon(
                CupertinoIcons.clear_thick_circled,
                color: Styles.searchIconColor,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

cf1e10b838bf60ee.png Observations

  • As interfaces de pesquisa no iOS são interessantes, porque há uma ampla variação de implementações. Com o Flutter, é possível ajustar o layout e a cor da implementação de forma rápida e fácil.

b2f84ff91b0e1396.png Execute o app (link em inglês). Selecione a guia search e insira "shirt" no campo de texto. Você verá uma lista com cinco produtos que contêm a palavra "shirt" no nome.

6f345bfa17663f9a.png

Problemas?

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

Nas próximas três etapas, você criará a guia do carrinho de compras. Nesta primeira etapa, você adicionará campos para coletar informações do cliente.

b2f84ff91b0e1396.png Atualize o arquivo lib/shopping_cart_tab.dart.

Adicione métodos particulares para criar os campos de nome, e-mail e local. Em seguida, adicione um método _buildSliverChildBuildDelegate() que crie partes da interface do usuário.

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

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  String? Name;    // ADD FROM HERE
  String? email;
  String? location;
  String? pin;
  DateTime dateTime = DateTime.now();

  Widget _buildNameField() {
    return CupertinoTextField(
      prefix: const Icon(
        CupertinoIcons.person_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      textCapitalization: TextCapitalization.words,
      autocorrect: false,
      decoration: const BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Name',
      onChanged: (newName) {
        setState(() {
          name = newName;
        });
      },
    );
  }

  Widget _buildEmailField() {
    return const CupertinoTextField(
      prefix: Icon(
        CupertinoIcons.mail_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      keyboardType: TextInputType.emailAddress,
      autocorrect: false,
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Email',
    );
  }

  Widget _buildLocationField() {
    return const CupertinoTextField(
      prefix: Icon(
        CupertinoIcons.location_solid,
        color: CupertinoColors.lightBackgroundGray,
        size: 28,
      ),
      padding: EdgeInsets.symmetric(horizontal: 6, vertical: 12),
      clearButtonMode: OverlayVisibilityMode.editing,
      textCapitalization: TextCapitalization.words,
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(
            width: 0,
            color: CupertinoColors.inactiveGray,
          ),
        ),
      ),
      placeholder: 'Location',
    );
  }

  SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
      AppStateModel model) {
    return SliverChildBuilderDelegate(
      (context, index) {
        switch (index) {
          case 0:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildNameField(),
            );
          case 1:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildEmailField(),
            );
          case 2:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildLocationField(),
            );
          default:
          // Do nothing. For now.
        }
        return null;
      },
    );
  }    // TO HERE

cf1e10b838bf60ee.png Observations

  • Um dos principais diferenciais do Flutter, comparado a ambientes de desenvolvimento de interface do usuário mais tradicionais, é que ele oferece todos os recursos de uma linguagem de programação para introduzir abstrações. É possível adicionar funções para agrupar funcionalidades ou transformá-las em um widget independente, se você quiser reutilizá-las com facilidade. Como programador, é você que escolhe a forma como a funcionalidade será definida.

b2f84ff91b0e1396.png Atualize o método build() na classe _ShoppingCartTabState.

Adicione uma SliverSafeArea que chame o método _buildSliverChildBuilderDelegate:

  @override
  Widget build(BuildContext context) {
    return Consumer<AppStateModel>(
      builder: (context, model, child) {
        return CustomScrollView(
          slivers: <Widget>[
            const CupertinoSliverNavigationBar(
              largeTitle: Text('Shopping Cart'),
            ),
            SliverSafeArea(
              top: false,
              minimum: const EdgeInsets.only(top: 4),
              sliver: SliverList(
                delegate: _buildSliverChildBuilderDelegate(model),
              ),
            )
          ],
        );
      },
    );
  }
}

cf1e10b838bf60ee.png Observations

  • Com toda a interface do usuário definida nas funções do builder, o método de compilação pode ser bem pequeno.

b2f84ff91b0e1396.png Execute o app (link em inglês). Selecione a guia shopping cart (carrinho de compras). Você verá três campos de texto para coletar informações do cliente:

bcb97c1aff65d3d7.png

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, você adicionará um CupertinoDatePicker ao carrinho de compras para que o usuário possa selecionar a data de entrega que preferir.

b2f84ff91b0e1396.png Adicione importações e um const ao lib/shopping_cart_tab.dart.

Adicione as novas linhas, conforme mostrado:

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

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';            // NEW
import 'package:provider/provider.dart';
import 'model/app_state_model.dart';
import 'styles.dart';                       // NEW

const double _kDateTimePickerHeight = 216;  // NEW

b2f84ff91b0e1396.png Adicione uma função _buildDateAndTimePicker() ao widget _ShoppingCartTab.

Adicione a função da seguinte maneira:

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  // ...

  Widget _buildDateAndTimePicker(BuildContext context) {
    // NEW FROM HERE
    return Column(
      children: <Widget>[
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: const <Widget>[
                Icon(
                  CupertinoIcons.clock,
                  color: CupertinoColors.lightBackgroundGray,
                  size: 28,
                ),
                SizedBox(width: 6),
                Text(
                  'Delivery time',
                  style: Styles.deliveryTimeLabel,
                ),
              ],
            ),
            Text(
              DateFormat.yMMMd().add_jm().format(dateTime),
              style: Styles.deliveryTime,
            ),
          ],
        ),
        Container(
          height: _kDateTimePickerHeight,
          child: CupertinoDatePicker(
            mode: CupertinoDatePickerMode.dateAndTime,
            initialDateTime: dateTime,
            onDateTimeChanged: (newDateTime) {
              setState(() {
                dateTime = newDateTime;
              });
            },
          ),
        ),
      ],
    );
  }    // TO HERE

SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
   AppStateModel model) {
  // ...

cf1e10b838bf60ee.png Observations

  • Adicionar um CupertinoDatePicker é rápido e oferece aos usuários do iOS uma forma intuitiva de inserir datas e horários.

b2f84ff91b0e1396.png Adicione uma chamada à função _buildSliverChildBuilderDelegate para criar a IU de data e hora. Adicione o novo código, conforme mostrado a seguir:

  SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
      AppStateModel model) {
    return SliverChildBuilderDelegate(
      (context, index) {
        switch (index) {
          case 0:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildNameField(),
            );
          case 1:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildEmailField(),
            );
          case 2:
            return Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              child: _buildLocationField(),
            );
          case 3:                // ADD FROM HERE
            return Padding(
              padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
              child: _buildDateAndTimePicker(context),
            );                   // TO HERE
          default:
          // Do nothing. For now.
        }
        return null;
      },
    );
  }

b2f84ff91b0e1396.png Execute o app (link em inglês). Selecione a guia shopping cart. Você verá um seletor de data no estilo do iOS abaixo dos campos de texto para coletar informações do cliente:

ecd9ef206f1e86c7.png

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, você adicionará itens selecionados ao carrinho de compras para finalizar o app.

b2f84ff91b0e1396.png Importe o pacote de produtos no shopping_cart_tab.dart.

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

import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'model/app_state_model.dart';
import 'model/product.dart';              // NEW
import 'styles.dart';

b2f84ff91b0e1396.png Adicione um formato de moeda à classe _ShoppingCartTabState.

Adicione a linha marcada como NEW:

class _ShoppingCartTabState extends State<ShoppingCartTab> {
  String? name;
  String? email;
  String? location;
  String? pin;
  DateTime dateTime = DateTime.now();
  final _currencyFormat = NumberFormat.currency(symbol: '\$'); // NEW

b2f84ff91b0e1396.png Adicione um índice de produtos à função _buildSliverChildBuilderDelegate.

Adicione a linha marcada como NEW:

SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
   AppStateModel model) {
 return SliverChildBuilderDelegate(
   (context, index) {
     final productIndex = index - 4;    // NEW
     switch (index) {
  // ...

b2f84ff91b0e1396.png Na mesma função, exiba os itens que serão comprados.

Adicione o código à seção default: da instrução "switch" da seguinte forma:

switch (index) {
 case 0:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildNameField(),
   );
 case 1:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildEmailField(),
   );
 case 2:
   return Padding(
     padding: const EdgeInsets.symmetric(horizontal: 16),
     child: _buildLocationField(),
   );
 case 3:
   return Padding(
     padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
     child: _buildDateAndTimePicker(context),
   );
 default:                      // NEW FROM HERE
   if (model.productsInCart.length > productIndex) {
     return ShoppingCartItem(
       index: index,
       product: model.getProductById(
           model.productsInCart.keys.toList()[productIndex]),
       quantity: model.productsInCart.values.toList()[productIndex],
       lastItem: productIndex == model.productsInCart.length - 1,
       formatter: _currencyFormat,
     );
   } else if (model.productsInCart.keys.length == productIndex &&
       model.productsInCart.isNotEmpty) {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 20),
       child: Row(
         mainAxisAlignment: MainAxisAlignment.end,
         children: <Widget>[
           Column(
             crossAxisAlignment: CrossAxisAlignment.end,
             children: <Widget>[
               Text(
                 'Shipping '
                 '${_currencyFormat.format(model.shippingCost)}',
                  style: Styles.productRowItemPrice,
               ),
               const SizedBox(height: 6),
               Text(
                 'Tax ${_currencyFormat.format(model.tax)}',
                 style: Styles.productRowItemPrice,
                ),
                const SizedBox(height: 6),
                Text(
                  'Total ${_currencyFormat.format(model.totalCost)}',
                  style: Styles.productRowTotal,
                ),
              ],
            )
          ],
        ),
      );
    }
}                       // TO HERE

b2f84ff91b0e1396.png Na parte inferior do arquivo, adicione uma nova classe ShoppingCartItem:

class ShoppingCartItem extends StatelessWidget {
  const ShoppingCartItem({
    required this.index,
    required this.product,
    required this.lastItem,
    required this.quantity,
    required this.formatter,
  });

  final Product product;
  final int index;
  final bool lastItem;
  final int quantity;
  final NumberFormat formatter;

  @override
  Widget build(BuildContext context) {
    final row = SafeArea(
      top: false,
      bottom: false,
      child: Padding(
        padding: const EdgeInsets.only(
          left: 16,
          top: 8,
          bottom: 8,
          right: 8,
        ),
        child: Row(
          children: <Widget>[
            ClipRRect(
              borderRadius: BorderRadius.circular(4),
              child: Image.asset(
                product.assetName,
                package: product.assetPackage,
                fit: BoxFit.cover,
                width: 40,
                height: 40,
              ),
            ),
            Expanded(
              child: Padding(
                padding: const EdgeInsets.symmetric(horizontal: 12),
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.start,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: <Widget>[
                        Text(
                          product.name,
                          style: Styles.productRowItemName,
                        ),
                        Text(
                          '${formatter.format(quantity * product.price)}',
                          style: Styles.productRowItemName,
                        ),
                      ],
                    ),
                    const SizedBox(
                      height: 4,
                    ),
                    Text(
                      '${quantity > 1 ? '$quantity x ' : ''}'
                      '${formatter.format(product.price)}',
                      style: Styles.productRowItemPrice,
                    )
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );

    return row;
  }
}

b2f84ff91b0e1396.png Execute o app (link em inglês). Na guia de produtos, selecione alguns itens para comprar usando o botão de adição à direita de cada item. Selecione a guia carrinho de compras. Você verá os itens listados no carrinho de compras abaixo do seletor de data:

28201e6fa0dc3102.png

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!

Você concluiu o codelab e criou um app do Flutter com o layout e o comportamento da Cupertino. Também usou o pacote provider para gerenciar o estado do app em várias telas. Para saber mais, consulte a documentação sobre gerenciamento de estados (link em inglês).

Outras etapas seguintes

Neste codelab, você criou o front-end de uma experiência de compras. O próximo passo para torná-la real é criar um back-end que processe as contas de usuários, os produtos, os carrinhos de compras, entre outros. Existem várias formas de alcançar essa meta:

Saiba mais

Veja mais informações nestes links: