1. 简介
Material Components (MDC) 可帮助开发者实施 Material Design。MDC 由 Google 的工程师和用户体验设计师团队创建,拥有几十个精美而强大的界面组件,可用于 Android、iOS、Web 和 Flutter。material.io/develop |
在 Codelab MDC-101 中,您使用了两个 Material Components 来构建登录页面:文本字段和带墨纹的按钮。下面我们将在此基础上进行扩展,添加导航、结构和数据。
您将构建的应用
在本 Codelab 中,您将为一个名为 Shrine 的应用(一个销售服饰和家在用品的电子商务应用) 构建主屏幕。其中将包含:
- 顶部应用栏
- 产品网格列表
Android | iOS |
本 Codelab 中的 MDC 组件
- 顶部应用栏
- 网格
- 卡片
您如何评价您在 Flutter 开发方面的经验?
2. 设置您的 Flutter 环境
准备工作
要开始使用 Flutter 开发移动应用,您需要执行以下操作:
- 下载并安装 Flutter SDK。
- 更新 PATH 以包含 Flutter SDK。
- 安装 Android Studio 并添加 Flutter 和 Dart 插件,或者选择您喜欢的编辑器。
- 安装 Android 模拟器、iOS 模拟器(需要装有 Xcode 的 Mac),或者使用实体设备。
如需了解有关 Flutter 安装的详细信息,请参阅 使用入门:安装。要设置编辑器,请参阅 使用入门:设置编辑器。安装 Android 模拟器时,您可以使用默认选项(如 Pixel 3 手机)并安装最新系统映像。建议启用 VM 加速,但不做强制要求。在完成上述 4 个步骤后,您可以返回到本 Codelab。要完成本 Codelab,您只需要安装适用于其中一个平台(Android 或 iOS)的 Flutter。
确保 Flutter SDK 处于正确的状态
在继续本 Codelab 之前,请确保您的 SDK 处于正确的状态。如果之前安装了 Flutter SDK,请使用 flutter upgrade
确保 SDK 处于最新状态。
flutter upgrade
运行 flutter upgrade
会自动运行 flutter doctor。如果这是全新的 Flutter 安装,则无需 升级,手动运行 flutter doctor
即可。它将报告是否存在为完成设置您需安装的任何依赖项。您可以忽略与您无关的对勾标记(例如,如果您不打算针对 iOS 开发,则可以忽略 Xcode)。
flutter doctor
常见问题解答
3. 下载 Codelab 起始应用
接续 MDC-101?
如您完成了 MDC-101,那么您的代码应该已经可以用于此 Codelab。请跳到以下步骤:添加顶部应用栏。
从头开始?
下载起始 Codelab 应用
起始应用位于 material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series
目录中。
......或者从 GitHub 克隆
要从 GitHub 克隆此 Codelab,请运行以下命令:
git clone https://github.com/material-components/material-components-flutter-codelabs.git cd material-components-flutter-codelabs/mdc_100_series git checkout 102-starter_and_101-complete
设置项目
以下说明假定您使用的是 Android Studio (IntelliJ)。
打开项目
1.打开 Android Studio。 |
2.如果看到欢迎屏幕,请点击 Open an existing Android Studio project(打开现有的 Android Studio 项目)。 |
3.导航至 |
4.如果出现提示:
然后重新启动 Android Studio。 |
运行起始应用
以下说明假定您是在 Android 模拟器或设备上进行的测试,但如果您安装了 Xcode,也可以在 iOS 模拟器或设备上测试。
1.选择设备或模拟器。如果 Android 模拟器尚未运行,请选择 Tools(工具)-> Android -> AVD Manager |
2.启动 Flutter 应用:
|
成功!您应该会在模拟器中看到 MDC-101 Codelab 的 Shrine 登录页面。
Android | iOS |
现在登录屏幕看起来不错,让我们在应用中填入一些产品。
4. 添加顶部应用栏
现在,如果您点击"Next"(下一步)按钮,将能看到显示"You did it!"(您已完成!)的主屏幕。很好!但现在用户没有操作可执行,也不知道自己处在应用中的哪一个位置。为解决此问题,接下来我们应该添加导航。
Material Design 可提供确保高度实用性的导航模式。顶部应用栏是最明显的组件之一。
为提供导航,并让用户快速访问其他操作,我们需要添加一个顶部应用栏。
添加 AppBar 微件
在 home.dart
中,将 AppBar 添加到 Scaffold:
// TODO: Add app bar (102)
appBar: AppBar(
// TODO: Add buttons and title (102)
),
将 AppBar 添加到 Scaffold 的 appBar:
字段,可让我们免费获得完美的布局, 使 AppBar 保持在页面顶部,正文在其下方。
保存项目。当 Shrine 应用更新后,点击 **Next(下一步)**查看主屏幕。
Android | iOS |
AppBar 看起来很好,但还需要一个标题。
添加文本微件
在 home.dart
中,为 AppBar 添加标题:
// TODO: Add app bar (102)
appBar: AppBar(
// TODO: Add buttons and title (102)
title: Text('SHRINE'),
// TODO: Add trailing buttons (102)
保存项目。
Android | iOS |
很多应用栏的标题旁边会有一个按钮。让我们在应用中添加菜单图标。
添加头部 IconButton
还是在 home.dart
中,为 AppBar 的 leading:
字段设置 IconButton。(将其放在 title:
字段前面,以模仿从头到尾的顺序):
// TODO: Add buttons and title (102)
leading: IconButton(
icon: Icon(
Icons.menu,
semanticLabel: 'menu',
),
onPressed: () {
print('Menu button');
},
),
保存项目。
Android | iOS |
菜单图标(也称为"汉堡图标")就显示在您所预期的位置。
您也可以将按钮添加到标题的尾端。在 Flutter 中,这些称为"操作"。
添加操作
还可以再添加两个 IconButton。
将它们添加到标题后面的 AppBar 实例中:将它们添加到 AppBar 实例中的标题后方:
// TODO: Add trailing buttons (102)
actions: <Widget>[
IconButton(
icon: Icon(
Icons.search,
semanticLabel: 'search',
),
onPressed: () {
print('Search button');
},
),
IconButton(
icon: Icon(
Icons.tune,
semanticLabel: 'filter',
),
onPressed: () {
print('Filter button');
},
),
],
保存项目。主屏幕显示的内容应如下所示:
Android | iOS |
现在应用的右侧有一个头部按钮、一个标题和两个操作。应用栏还运用了细微的阴影以显示高度,以表示其与内容在不同的层上。
5. 在网格中添加卡片
现有我们的应用已经有一些结构,让我们通过将内容放到卡片中来组织内容。
添加 GridView
我们先在顶部应用栏的下方添加一张卡片。单独的 Card 微件没有足够的信息,无法将自己布置在我们可以看到的地方,因此我们要将其封装到 GridView 微件中。
将 Scaffold 正文中的 Center 替换为 GridView:
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
// TODO: Build a grid of cards (102)
children: <Widget>[Card()],
),
让我们来分析这段代码。GridView 将调用 count()
构造函数,因为它显示的项目数是可数的,不是无限的。但需要一些信息才可定义其布局。
crossAxisCount:
用于指定涵盖多少个项目。我们需要 2 列。
padding:
字段用于在 GridView 的所有四边都提供空间。当然,您在尾部或底边看不到填充,因为它们旁边还没有 GridView 子项。
childAspectRatio:
字段会根据宽高比(宽度与高度的比)识别项目的大小。
默认情况下,GridView 会让所有卡片的大小相同。
将上述因素一起考虑后,GridView 会计算每个子项的宽度,具体如下:([width of the entire grid] - [left padding] - [right padding]) / number of columns
。代入我们现有的值:([width of the entire grid] - 16 - 16) / 2
。
通过宽高比,我们可以从宽度计算出高度:([width of the entire grid] - 16 - 16) / 2 * 9 / 8
。在这里我们调换了 8 和 9 的位置,因为我们从宽度开始,然后计算高度,而不是相反。
我们现在有一张卡片,但还是空的。让我们在卡片中添加一些子微件。
布置内容
卡片应该具有用来放置图像、标题和次要文本的区域。
更新 GridView 的子项:
// TODO: Build a grid of cards (102)
children: <Widget>[
Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 18.0 / 11.0,
child: Image.asset('assets/diamond.png'),
),
Padding(
padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Title'),
SizedBox(height: 8.0),
Text('Secondary Text'),
],
),
),
],
),
)
],
此代码会添加用于垂直布置子微件的 Column 微件。
crossAxisAlignment: field
用于指定 CrossAxisAlignment.start
,表示"靠前缘对齐文本"。
AspectRatio 微件用于决定图像将采用的形状,而不管提供的是哪类图像。
Padding 用于从侧边一点填入文本。
两个 Text 微件垂直堆叠,间隔 8 点空间 (SizedBox)。我们创建另一个 Column 将它们包含在 Padding 内。
保存项目:
Android | iOS |
在此预览中,您可以看到卡片从边缘插入,具有圆角和阴影(用于表示卡片的高度)。 整个形状在 Material 中称为"container"(容器)。(不要与称为 Container 的实际微件类混淆。)
卡片通常与其他卡片一起显示在一个集合中。让我们将这些卡片作为一个集合布置在网格中。
6. 建立卡片集
只要屏幕中存在多张卡片,它们就会组成一个或多个集合。一个集合中的卡片是共面的,也就是说,卡片彼此共享相同的高度(除非卡片被选取或拖曳,但在这里我们不会执行该操作)。
将卡片增加到集合
现在,我们在 GridView 的 children:
字段中内联构建了卡片。这里有很多难以读懂的嵌套代码。我们将其提取到一个函数中,该函数可以根据我们的需要生成很多空白卡片,并且返回卡片列表。
在 build()
函数上方建立新的私有函数(请记住,以下划线开头的函数是私有 API):
// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
List<Card> cards = List.generate(
count,
(int index) => Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 18.0 / 11.0,
child: Image.asset('assets/diamond.png'),
),
Padding(
padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Title'),
SizedBox(height: 8.0),
Text('Secondary Text'),
],
),
),
],
),
),
);
return cards;
}
将生成的卡片分配给 GridView 的 children
字段。记得将 GridView 中包含的所有内容替换为以下的新代码:
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
children: _buildGridCards(10) // Replace
),
保存项目:
Android | iOS |
卡片已经有了,但其中尚未显示任何内容。现在添加一些产品数据。
添加产品数据
应用有一些包含图像、名称和价格的产品。我们将这些信息添加到卡片中已有的微件
然后,在 home.dart
中导入新的软件包和我们为数据模型提供的一些文件:
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'model/products_repository.dart';
import 'model/product.dart';
最后,更改 _buildGridCards()
以提取产品信息,并在卡片中使用该数据:
// TODO: Make a collection of cards (102)
// Replace this entire method
List<Card> _buildGridCards(BuildContext context) {
List<Product> products = ProductsRepository.loadProducts(Category.all);
if (products == null || products.isEmpty) {
return const <Card>[];
}
final ThemeData theme = Theme.of(context);
final NumberFormat formatter = NumberFormat.simpleCurrency(
locale: Localizations.localeOf(context).toString());
return products.map((product) {
return Card(
clipBehavior: Clip.antiAlias,
// TODO: Adjust card heights (103)
child: Column(
// TODO: Center items on the card (103)
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AspectRatio(
aspectRatio: 18 / 11,
child: Image.asset(
product.assetName,
package: product.assetPackage,
// TODO: Adjust the box size (102)
),
),
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
child: Column(
// TODO: Align labels to the bottom and center (103)
crossAxisAlignment: CrossAxisAlignment.start,
// TODO: Change innermost Column (103)
children: <Widget>[
// TODO: Handle overflowing labels (103)
Text(
product.name,
style: theme.textTheme.headline6,
maxLines: 1,
),
SizedBox(height: 8.0),
Text(
formatter.format(product.price),
style: theme.textTheme.subtitle2,
),
],
),
),
),
],
),
);
}).toList();
}
请注意:现在还不能编译和运行。我们还需要进行一个更改。
另请更改 build()
函数以将 BuildContext 传递到 _buildGridCards()
,然后尝试编译:
// TODO: Add a grid view (102)
body: GridView.count(
crossAxisCount: 2,
padding: EdgeInsets.all(16.0),
childAspectRatio: 8.0 / 9.0,
children: _buildGridCards(context) // Changed code
),
Android | iOS |
您可能会发现,我们没有在卡片之间添加任何垂直空间。这是因为,在默认情况下,其顶部和底部会有 4 点的填充。
保存项目:
产品数据已经显示,但图像周围有多出的空间。默认情况下(在本例中), 图像会由 .scaleDown
的 BoxFit 绘制。我们将其更改为 .fitWidth
,以便将图像放大一些,然后删除多余的空白。
将 fit:
字段添加到图像,并将值设为 BoxFit.fitWidth
:
// TODO: Adjust the box size (102)
fit: BoxFit.fitWidth,
Android | iOS |
我们的产品此时会在应用中完美地显示!
7. 总结
我们的应用的基本流程是,将用户从登录屏幕带至主屏幕,然后用户可以在主屏幕浏览产品。 只需要几行代码,我们就添加了顶部应用栏(包含标题和三个按钮)和卡片(用于显示我们应用的内容)。我们的主屏幕现在是一个简单的功能性屏幕,具有基本结构和可操作的内容。
后续步骤
我们现在使用了 MDC-Flutter 库的四个核心组件:顶部应用栏、卡片、文本字段和按钮!您可以访问 Flutter 微件目录探索更多组件。
虽然我们的应用功能齐全,但尚未表现任何特定的品牌或观点。在 MDC-103:Material Design 主题中的颜色、形状、高度和类型中,我们将自定义这些组件的样式,以表现充满活力的现代品牌。