欢迎来到 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 中学到什么
provider
package 来实现多个界面之间的 state 管理你需要安装两个软件来完成: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(),
);
}
}
注意事项
CupertinoApp
,这提供主题、导航、文本排列和其他一些 iOS 用户期望的功能。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(),
);
}
}
注意事项
,CupertinoPageScaf
fold 可以为一个页面提供 Cupertino 风格的导航栏,背景色,并管理整个页面的 widget 嵌套关系。另外一个风格的页面结构你将会在下一步中学到。 更新 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(),
);
});
}
},
);
}
}
注意事项
CupertinoTabBar
至少需要两个项目,否则将会在运行时报错。tabBuilder:
用来确保某个特定的 tab 被创建。在这一前提下,它会调用类的构造函数来设置每一个相应的 tab,将所有的 tab 封装在 CupertinoTabView
和CupertinoPageScaffold 中。
为新的 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'),
),
],
);
},
);
}
}
注意事项
provider
package 中的 Consumer
用来协助 state 管理,稍后会再次介绍它。CustomScrollView
中的 CupertinoSliverNavigationBar
widget 来实现的。新增搜索页面
创建一个 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'),
),
],
);
},
);
}
}
注意事项
CustomScrollView.
更新 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.
注意事项
AppStateModel
显示了一种集中处理应用程序状态的方法,并让该状态在整个应用程序中可用。在后面的步骤中,我们将使用此状态来驱动实现我们的搜索和购物车功能。 更新 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
),
);
}
注意事项
AppStateModel
来让它在整个 app 中都可以访问。ChangeNotifierProvider
,它会监听 AppStateModel 变化所发出的通知。运行 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,
),
),
],
);
}
}
注意事项
CupertinoSliverNavigationBar
用来实现 iOS 11 风格的导航栏,这对于 iOS 的用户来说是很地道的体验。 在 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 的 bui
ld() 方法里,获取商品列表以及商品的数量。增加以下代码:
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
],
);
},
),
);
}
}
注意事项
CupertinoSliverNavigationBar
)。SliverSafeArea 的
top 属性设置成了 false 好让它忽略了间距的存在。
当手机
发生旋转时,SliverSafeArea 的 left 和 right 属性仍然默认为 true,同时,它仍然决定着 bottom 属性,以便当发生内容滑动到最后时,最后的内容刚好保持在最底部。
运行 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) {
注意事项
_SearchTabState
代表的是当前的搜索状态。在此实现中,我们存储搜索项的内容,并且我们 hook 了 AppStateModel
以实现搜索功能。在我们实现 API 后端的情况下,这里是一个为搜索进行网络访问的好地方。 增加一个 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
在 _ShoppingCar
tTab 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) {
// ...
注意事项
使用 CupertinoDatePicker
是最简单的方法,同时还提供给 iOS 用户更原生的选择时间的体验。
在 _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 构建了一个购物的前端项目,下一步应该是创建一个处理用户帐户,产品,购物车等的后端服务。有多种方法可以实现这一目标:
更多
你可以在以下链接中查看到更多的信息: