欢迎来到 Flutter Cupertino codelab!

在这个 codelab 中,你讲会使用 Flutter 创建一个 Cupertino(iOS 风格)的 app。当然,Flutter SDK 默认提供了两种风格的 widget 库(除了基本的 widget library 之外)。

那为什么要实现这个 Cupertino app 呢?Material 设计风格是为全平台设计的,不仅仅只是 Android 。当你使用 Flutter 编写一个 Material 风格的 app 时,它运行在任何平台上都是有着 Material 的设计展示,即使是在 iOS 下。但是如果你想要让你的 app 更像标准的 iOS 风格的话,那你就需要用到 Cupertino 库了。

技术上来说,你可以在 iOS 和 Android 上正常运行一个 Cupertino 搭建的 app,但是(因为一些授权的原因),Cupertino 在 Android 上并不能展示它应有的字体。正因为此,当你编写一个 Cupertino app 的时候,你应该让其运行在 iOS 的设备上。

你将会实现一个拥有三个 tab 用来购物的 Cupertino 风格的 app,其中三个 tab 分别用来展示产品列表,产品搜索和购物车。

你讲从这个 codelab 中学到什么

你需要安装两个软件来完成:Flutter SDK文本编辑器。你可以选择你喜欢的编辑器,比如 Android Studio 或者是集成了 Flutter 和 Dart 插件的 IntelliJ,又或者是安装了 Dart Code 和 Flutter 扩展 的 Visual Studio Code。

你可以在以下设备中运行这个 codelab 的代码:

你同样需要:

使用 CupertinoPageScaffold 来创建初始的 app 结构。

根据这篇文档 Getting Started with your first Flutter app 来创建一个简单的模板化 Flutter 应用。将项目命名为 cupertino_store(而不是 myapp)。 这个初始的 app 将作为完成整个 codelab 的第一步。

更改 lib/main.dart 下的代码。
Delete all of the code from lib/main.dart, which creates a Material-themed button counting app. Replace with the following code, which initializes a Cupertino app.

先将 lib/main.dart 下的所有代码删掉,它们只是用来创建一个 Material 主题风格的计数按钮。然后用下列初始化 Cupertino app 的代码替换,

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

import 'app.dart';

void main() {
  // This app is designed only to work vertically, so we limit
  // orientations to portrait up and down.
  SystemChrome.setPreferredOrientations(
      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

  return runApp(CupertinoStoreApp());
}

注意事项

创建 lib/styles.dart.

lib 文件夹中创建 styles.dart 文件。里面的 Styles 类定义了文本和一些颜色,用来定义 app 的样式。以下的只是文本中的一点内容,你可以在 Github 中的 lib/styles.dart 中查看全部内容。

// 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.

注意事项

创建 lib/app.dart 文件,然后增加 CupertinoStoreApp 类。紧接着在 lib/app.dart 文件中增加以下的 CupertinoStoreApp 类。

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

class CupertinoStoreApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      home: CupertinoStoreHomePage(),
    );
  }
}

注意事项

增加 CupertinoStoreHomePage 类,并在 lib/app.dart 中增加以下 CupertinoStoreHomePage 类来为首页创建基本的排版。

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

注意事项

更新 pubspec.yaml 文件。

编辑 pubspec.yaml 文件,增加一些需要用到的依赖和图片资源,以下是文本部分内容,完成内容可以在 Github 的 pubspec.yaml 中查看。

# 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

environment:
 sdk: ">=2.2.0 <3.0.0"

dependencies:
 flutter:
   sdk: flutter

 cupertino_icons: ^0.1.2
 intl: ^0.15.7
 provider: ^2.0.0+1
 shrine_images: ^1.0.0

dev_dependencies:
 pedantic: ^1.4.0

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

注意事项

运行 app,你将会看到以下图片这样只有一个 Cupertino Store 的导航栏的空白界面。

遇到问题了?

如果你的 app 并没有正确运行,你需要查看是否有拼写错误。如果需要的话,你可以查看以下文件来仔细检查。

最终的 app 会有 3 个标签栏:

在这一部分,你将会使用 CupertinoTabScaffold 来将首页更改为一个有着三个 tab 的页面。另外,你还会增加一些数据来展示待售商品的架构、图片。

在上一步中,你使用了 CupertinoPageScaffold 创建了一个 CupertinoPageScaffold 类。这样创建出来的界面是没有标签栏的。但是我们不想要这样的效果,所以我们不再使用 CupertinoPageScaffold,而该用 CupertinoTabScaffold 进行首页的创建。

Cupertino tab 在 iOS 平台下有一个单独的创建方式,因为在 iOS 平台上底部的 tab 通常是路由层级的嵌套关系,而不是页面层级的嵌套。

更新 lib/app.dart 文件。

CupertinoStoreHomePage 类替换为如下代码,这样将会配置一个 3-tabs 的页面结构。

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

注意事项

为新的 tab 的内容添加 stub 类

为第一个 tab 创建 lib/product_list_tab.dart 文件,它当前只展示一个空白屏幕。代码内容如下:

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 CustomScrollView(
          slivers: const <Widget>[
            CupertinoSliverNavigationBar(
              largeTitle: Text('Cupertino Store'),
            ),
          ],
        );
      },
    );
  }
}

注意事项

新增搜索页面

创建一个 lib/search_tab.dart 文件,它当前只展示一个空白屏幕。代码内容如下:

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 CustomScrollView(
      slivers: const <Widget>[
        CupertinoSliverNavigationBar(
          largeTitle: Text('Search'),
        ),
      ],
    );
  }
}

注意事项

新增购物车页面

创建一个 lib/shopping_cart_tab.dart文件,它当前只展示一个空白屏幕。代码内容如下:

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 CustomScrollView(
          slivers: const <Widget>[
            CupertinoSliverNavigationBar(
              largeTitle: Text('Shopping Cart'),
            ),
          ],
        );
      },
    );
  }
}

注意事项

更新 lib/app.dart

lib/app.dart 中更改 import 语句,来引入之前定义的三个 tab:

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

在此步骤的第二部分中,继续在下一页上,你将添加用于跨页面管理和共享 state 的代码。

这个 app 中有一些需要在多个页面中共享的基础数据,所以你需要能够让需要用到这些数据的地方能够访问到它们所需的数据 scoped_model package 提供一个简单的方法来实现这一目的。在 scoped_model 中,你定义了一个数据模型用来将父 widget 的数据传递到它所有的子 widget 中。将数据模型封装在 ScopedModel widget 中会让这个数据模型在所有的子 widget 中可用,子 widget 将会通过 ScopedModelDescendant widget 来找到相应的 ScopedModel.

创建数据模型类

lib 文件夹下新建 model 文件夹。然后新增 lib/model/product.dart 文件用来定义来自数据源中的产品数据。

import 'package:flutter/foundation.dart';

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,
  })  : assert(category != null),
        assert(id != null),
        assert(isFeatured != null),
        assert(name != null),
        assert(price != null);

  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)';
}

注意事项

ProductsRepository 类包含了待售产品的完整列表,以及它们的价格,标题文本和类别。 我们的 app 不会对 isFeatured 属性执行任何操作。该类还包括 loadProducts() 方法,该方法返回所有产品或给定类别中的所有产品。

创建商品库。

创建 lib/model/products_repository.dart 文件,该文件包含了所有的待售商品,每个商品都隶属于某个类别。以下是文件的部分内容,你可以在 Github 中的products_repository.dart 查看全部内容:

// 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();
   }
 }
}

注意事项

你现在可以进行 model 的定义了。 创建 lib/model/app_state_model.dart 文件。在AppStateModel 类中,提供了从 model 访问数据的方法。 例如,添加一个用于访问购物车总数的方法,另一个用于购买所选产品的列表,另一个用于运输成本,等等。

创建 model 类。

以下是此类提供的方法签名列表,你可以在 GitHub 上查看到完整内容:lib/model/app_state_model.dart

// 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.

注意事项

更新 lib/main.dart

main() 方法中,初始化 model,即添加以下以 NEW 结尾的那几行代码。


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

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

void main() {
  // This app is designed only to work vertically, so we limit
  // orientations to portrait up and down.
  SystemChrome.setPreferredOrientations(
      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);

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

注意事项

运行 app,你将会看到一个包含 Cupertino 导航栏,标题和带有 3 个标记图标的 tab 页面。 你可以在 tab 之间自由切换,当然,当前所有三个页面都是空白的。

Problems?

遇到问题了?

如果你的 app 并没有正确运行,你需要查看是否有拼写错误。如果需要的话,你可以查看以下文件来仔细检查。

在这一步骤中,将会实现在商品列表的 tab 展示待售商品。

增加文件 lib/product_row_item.dart 用于商品的展示,创建 lib/product_row_item.dart 文件,内容如下:

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({
    this.index,
    this.product,
    this.lastItem,
  });

  final Product product;
  final int index;
  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,
            child: const Icon(
              CupertinoIcons.plus_circled,
              semanticLabel: 'Add',
            ),
            onPressed: () {
              final model = Provider.of<AppStateModel>(context);
              model.addProductToCart(product.id);
            },
          ),
        ],
      ),
    );

    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,
          ),
        ),
      ],
    );
  }
}

注意事项

lib/product_list_tab.dart 文件中,引入 product_row_item.dart 文件。

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

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

ProductRowTab 的 build() 方法里,获取商品列表以及商品的数量。增加以下代码:

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

同样在 build()方法中, 将新的内容添加到 sliver widget 列表用来保存产品列表。添加以下代码:

class ProductListTab extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return CupertinoPageScaffold(
     child: Consumer<AppStateModel>(
       builder: (context, model, child) {
         final products = model.getProducts();
         return CustomScrollView(
           semanticChildCount: products.length,
           slivers: <Widget>[
             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(
                         index: index,
                         product: products[index],
                         lastItem: index == products.length - 1,
                       );
                     }
                     return null;
                   },
                 ),
               ),
             )                  // END OF NEW CONTENT
           ],
         );
       },
     ),
   );
 }
}

注意事项

运行 app, 在产品列表的 tab 中,你应该会看到包含图片,价格和带加号按钮的产品列表,这个按钮可将产品添加到购物车。该按钮的功能将在稍后创建购物车的时候实现。

遇到问题了?

如果你的 app 并没有正确运行,你需要查看是否有拼写错误。如果需要的话,你可以查看以下文件来仔细检查。

在这一步中,你将会创建一个搜索栏,并增加在 app 中搜索商品的功能。

更新 lib/search_tab.dart 中的 import 语句,代码如下:

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'

更新 _SearchTabState 中的 build() 方法,初始化 model 并将 CustomScrollView 替换为用于搜索和展示的控件。

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(
                      index: index,
                      product: results[index],
                      lastItem: index == results.length - 1,
                    ),
                itemCount: results.length,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

注意事项

_SearchTabState 类中增加一些变量、函数和方法,保护 initState()dispose()_onTextChanged(),和 _buildSearchBox() 方法,如下所示:

class _SearchTabState extends State<SearchTab> {
  TextEditingController _controller;
  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) {

注意事项

增加一个 SearchBar 类。

创建一个新的文件, lib/search_bar.dart,其中 SearchBar 类会处理在商品列表中搜索商品的事件。代码如下:

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,
              ),
            ),
            GestureDetector(
              onTap: controller.clear,
              child: const Icon(
                CupertinoIcons.clear_thick_circled,
                color: Styles.searchIconColor,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

注意事项

运行 app, 选择 search 栏,然后在搜索框中输入 "shirt" 一次,你应该会看到 5 条商品内容,它们都会在商品名中包含 "shirt" 一词。

遇到问题了?

如果你的 app 并没有正确运行,你需要查看是否有拼写错误。如果需要的话,你可以查看以下文件来仔细检查。

在接下来的三步中,你将会创建一个购物车的 tab,第一步先增加一个用来表示客户信息的字段。

更新 lib/shopping_cart_tab.dart 文件,增加一些私有方法用来创建 name, email, 和 location 字段。然后新增 _buildSliverChildBuildDelegate() 方法来创建用户信息的部分界面。

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

注意事项

_SearchTabState 中更新 build()方法。新增 SliverSafeArea 用来调用 _buildSliverChildBuildingDelegate 方法:

  @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),
              ),
            )
          ],
        );
      },
    );
  }
}

注意事项

运行 app,选择 shopping cart 这个 tab,你应该会看到三个文本框用来收集用户的基本信息。

遇到问题了?

如果你的 app 并没有正确运行,你需要查看是否有拼写错误。如果需要的话,你可以查看以下文件来仔细检查。

在这一步中,在购物车一栏中新增 CupertinoDatePicker 用于让用户选择合适的配送时间。

lib/shopping_cart_tab.dart 新增 import 语句和一个 const 常量,代码如下:

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

const double _kDateTimePickerHeight = 216;  // NEW

在 _ShoppingCartTab widget 中新增一个 _buildDateAndTimePicker()函数,代码如下:

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) {
  // ...

注意事项

_buildSliverChildBuilderDelegate 方法中调用创建 UI 的方法,代码如下:

  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;
      },
    );
  }

运行 app,选择购物车这一标签栏,你应该会看到在文本框下的 iOS 风格的日期选择框:

遇到问题了?

如果你的 app 并没有正确运行,你需要查看是否有拼写错误。如果需要的话,你可以查看以下文件来仔细检查。

在这一步中,实现向购物车里增加要购买的商品。

shopping_cart_tab.dart 文件中引入 product package:

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

_ShoppingCartTabState 类中添加货币的单位,代码如下:

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

_buildSliverChildBuilderDelegate 函数中添加商品的序号,代码如下:

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

在同样的方法中,展示要购买的商品,在 default: 这部分代码中增加一些 switch 语句,代码如下:

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

运行 app,在商品列表的 tab 下选择某几项商品,并使用添加到购物车按钮将他们添加到购物车中。然后切换到购物车 tab 下,你应该会看到你刚刚选择的商品展示在这一栏中,并处于日期选择框下。

遇到问题了?

如果你的 app 并没有正确运行,你需要查看是否有拼写错误。如果需要的话,你可以查看以下文件来仔细检查。

恭喜!

你已经完成了 codelab 并构建了一个具有 Cupertino 风格的 Flutter app!你还使用了 provider package 实现跨页面管理应用程序状态。如果需要了解更多关于 state 管理方面的内容你可以查看我们的 state management documentation 这篇文章。

其余的一些步骤:

这个 codelab 构建了一个购物的前端项目,下一步应该是创建一个处理用户帐户,产品,购物车等的后端服务。有多种方法可以实现这一目标:

更多

你可以在以下链接中查看到更多的信息:

特别感谢