MDC (Material Components) 帮助开发者实现 Material Design。

MDC 由谷歌的工程师和交互团队联合呈现,其具备数种精美的 UI 组件,适用于 Android、iOS、Web 和 Flutter。

查看详细:material.io/develop

在 Codelab MDC-101 中,你已经使用两种 Material 组件构建了一个登录页面,也就是文本框和带有 ink 波纹效果的按钮。

本次你将做出什么

在本期 Codelab 中,你将为一个 Shrine 这个售卖衣物和家庭用品的电商应用构建主界面。它包含:

我们最终会构建一个卖衣服和家居的电子商务应用 - SHRINE,总共通过四个 Codelab 搞定它,本 Codelab 是其中的第二个,相关的 Codelab 如下:

在 MDC Flutter 教程 4 结束时,你将有机会做出一个这样的应用:

本期 Codelab 中的 MDC 组件

你可能需要准备

在 iOS 上运行 Flutter 应用:

在 Android 上运行 Flutter 应用:

如何定位你的 Flutter 开发水平?

初学者 中级水平 专业

你需要安装两部分来完成本次实验,Flutter 的 SDK编辑器(editor),这个 codelab 里,我们以 Android Studio 作为编辑器(editor),但你可以用个人更顺手的编辑器。

你可以通过如下任何设备完成本 codelab:

从 MDC 101 继续?

如果你已经完成了 MDC Flutter 教程 1:Material 组件基础(MDC 101),你可以跳过这一节,直接从下一节继续......

从本节开始?

下载 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
git checkout 102-starter_and_101-complete

设置你的工程

如下设置工程的方法是使用 Android Studio 做演示的。

创建新的工程

1. 在你的命令行工具里,切换到这个文件夹 material-components-flutter-codelabs

2. 执行命令 flutter create mdc_100_series

打开你的工程

1. 打开 Android Studio

2. 当你看到欢迎界面时候,选择打开一个已存在的工程(Open an existing Android Studio project.)

3. 打开这个文件夹 material-components-flutter-codelabs/mdc_100_series

如果此时提示一些错误请忽略,等第一次成功编译之后应该会好一些。

4. 在项目面板左侧,删除测试文件 ../test/widget_test.dart

5. 如果提示"升级平台和插件"或要配置 FlutterRunConfigurationType,请重启 Android Studio。

运行起步应用

下面我们以在 Android 模拟器或真机上为例做如下的步骤示范,当然如果你装有 Xcode,你也可以在 iOS 模拟器或真机上进行尝试。

1. 选择设备或模拟器

如果 Android 模拟器还没有运行起来,请选择 Tools -> Android -> AVD Manager 先进行 创建并启动模拟器 的操作。如果 AVD 里已经有创建好的模拟器,你可以直接在 IntelliJ 的设备选择列表里启动模拟器,如下一步所示。

(对于 iOS 模拟器,如果你还没有启动,请在你的电脑上选择 Flutter Device Selection -> Open iOS Simulator 来启动。)

2. 运行你的 Flutter 应用:

  • 在你的编辑器上方, Flutter 设备下拉菜单里,选择一款设备(比如选择 iPhone SE 或者某个 Android SDK 编译版本的设备)。
  • 点击 Play 按钮 ()。

一切就绪后,在模拟器或真机中你应该可以看到 Shrine 的登录页面,这是我们在 MDC-101 Codelab 中所完成的工作。

现在登录页看起来还不错。让我们为应用再充实一下。

退出登录界面以后,就会显示出主界面,上面写有一行 "You did it!" 。这很不错!但目前对用户来说,他们还做不了什么,也不知道当前在哪个页面上。为解决这些问题,是时候要来添加导航了。

Material Design 为导航提供了相关的规范,使其有足够高的可用度。其中最常见的组件即应用顶部栏。

让我们来添加应用顶部栏,以提供导航和其他操作的快捷入口。

添加一个 AppBar widget

home.dart 中,为 Scaffold 添加 AppBar :

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

AppBar 添加到 Scaffold 的 appBar 中,我们很轻松得就得到了一个完美的布局,它把 AppBar 保持在顶部,而 body 在下面。

保存项目。当 Shrine 更新后,点击 Next 以观察一下主界面。

看起来 AppBar 已经有了,但还需要一个标题。

添加 Text widget

home.dart 中,为 AppBar 添加一个标题:

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

    title: Text('SHRINE'),
        // TODO: Add trailing buttons (102)

保存项目。

很多应用顶部栏在标题旁边都会有按钮。让我们在应用中添加一个菜单图标吧。

在头部添加一个 IconButton

依旧是在 home.dart 中,为 AppBar 的 leading 字段设置一个 IconButton 。(需要将其放在 title 字段前面,因为它们遵循从头至尾 leading-to-trailing 的先后顺序):

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

保存项目。

菜单图标(也被称为 "汉堡图标")就出现在了那里,正如你所期待的那样。

你也可以在标题尾部添加按钮。在 Flutter 中它们被称为 "actions" 。

添加 actions

我们还有空间放下两个 IconButtons 。

把它们添加到 AppBar 实例中,title 之后:

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

保存项目。你的主界面现在看起来应该是这样:

现在应用有了头部的按钮,标题,以及右边的两个 actions 。应用顶部栏还显示出了 elevation ,它以一种浅阴影的形式来表示它和其他内容在不同的层级上。

现在应用有一定的结构了,让我们以卡片的形式来组织内容吧。

添加一个 GridView

让我们从在顶部栏下方添加一个卡片开始吧。单凭 Card widget 不足以展示出我们想要的效果,我们要把它放进一个 GridView widget 中。

在 Scaffold 的 body 里,将 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:指定每横行展示多少条目。这里我们想要两列。

padding: 给 GridView 的四周都增加了空间。当然你目前还看不到 GridView 尾部或底部新增加的空间,因为还没有足够多的子 view 被添加进来。

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 除法关系颠倒了一下。

现在我们有了一个卡片,但它里面是空的。让我们来给它里面加入 widget 吧。

将内容布局

卡片里需要有一张图片、一个标题和一段文字。

更新一下 GridView 的子项:

// TODO: Build a grid of cards (102)
children: <Widget>[
  Card(
    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 widget ,使子项可以垂直排列。

crossAxisAlignment 字段设为了 CrossAxisAlignment.start ,意思是"将文本向头部对齐"。

图片形状由 AspectRatio 决定,而不是提供的图片本身的形状。

Padding 则使得文本从边缘向中间移动一点。

两个 Text widgets 上下放置,用 SizedBox 来表示它们之间有 8 points 的距离。我们在 Padding 之中创建了一个 Column 来放置它们。

保存项目:

在预览里,你会看到这张卡片被靠边放置,它带有圆角、阴影(表现出卡片的 elevation )。这种外形在 Material 里叫 "container" 。(不要与 Container widget 混淆。)

卡片通常放在一起用于展示集合。让我们来把它们以集合的形式布局到一个网格里。

每当多个卡片在同一界面呈现时,它们都会被以一个或多个集合的形式组织起来。同一集合中的卡片处于相同的水平面,即它们静止时 evelation 相同(除非卡片被选中或者拖动,我们在这里还不会这么做)。

同一集合中的多个卡片

目前与卡片相关的多有代码都写在了 GridView 的 children 里。这样会有很多嵌套的代码,难以阅读。让我们将它们抽象到一个函数里,使它可以生成任意数量的卡片,最后以卡片列表的形式返回。

build() 之后添加一个私有函数(注意以单下划线开头的函数即是私有的):

// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) => Card(
      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 children 里之前所有的代码:

// 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
),

保存项目:

卡片出现了,但它们还没有展示任何东西。现在该加上一些产品数据上去了。

添加产品数据

应用里包含产品图片、名称、价格。让我们把这些内容加到每个卡片里吧。

接下来,在 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(
      // 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.title,
                    maxLines: 1,
                  ),
                  SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.body2,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).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
),

你也许注意到了,我们没有在卡片之间添加垂直向间距。这是因为它们默认就在顶部和底部分别有 4 points 的 padding 。

保存项目:

产品数据展示出来了,但是图片周围有多余的空间。图片是由 BoxFit 进行绘制的,(在这个案例中)默认是 .scaleDown 模式。我们把它改为 .fitWidth 以让图片填充,去掉多出的空白。

更改一下图片的 fit 字段:

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

我们的产品就这样完美地呈现在应用之中了!

我们的应用已经拥有了基本的流程,它把用户从登录界面带到主界面,在这里用户可以浏览产品。我们用了数行代码中,便添加了应用顶部栏(带有标题和三个按钮)和卡片(展示应用的内容)。我们的主界面现在简洁易用,结构简单,并具备一些可以进行交互的内容。

接下来

我们现在已经用过 MDC-Flutter 库中的四个核心组件了!有应用顶部栏,卡片,文本框和按钮。访问 Flutter Widgets Catalog 来探索更多的组件。

虽然功能已经有了,但我们的应用还没有表现出任何品牌或是意图。在 MDC-103: Material Design Theming with Color, Shape, Elevation and Type 中,我们将定制这些组件的样式,以表现出一个现代的、充满活力的品牌形象。

我能够用合理的时间和精力完成这个代码库

非常赞同 赞同 一般 不赞同 非常不赞同

我希望将来继续使用 Material Components

非常赞同 赞同 一般 不赞同 非常不赞同