MDC-102 Flutter:Material 结构和布局

1. 简介

logo_components_color_2x_web_96dp.png

Material Components (MDC) 有助于开发者实现 Material Design。MDC 由 Google 的工程师和用户体验设计人员倾力打造,提供数十种精美实用的界面组件,可用于 Android、iOS、网页和 Flutter。如需了解详情,请访问 material.io/develop

在 Codelab MDC-101 中,您已经使用以下两种 Material 组件构建了一个登录页面:文本字段和带有水墨涟漪效果的按钮。现在,我们在此基础上通过添加导航、结构和数据进行扩展。

您将构建的内容

在此 Codelab 中,您将为名为 Shrine 的应用构建一个主屏幕。Shrine 是一款销售服装和家居用品的电子商务应用。其中包含:

  • 顶部应用栏
  • 商品网格列表

Android

iOS

包含顶部应用栏和满是商品的网格的电子商务应用

包含顶部应用栏和满是商品的网格的电子商务应用

此 Codelab 中的 Material Flutter 组件和子系统

  • 顶部应用栏
  • 网格
  • 卡片

您如何评价自己在 Flutter 开发方面的经验水平?

新手水平 中等水平 熟练水平

2. 设置您的 Flutter 开发环境

您需要使用两款软件才能完成此 Codelab:Flutter SDK一款编辑器

您可使用以下任一设备学习此 Codelab:

  • 一台连接到计算机并设置为开发者模式的实体 AndroidiOS 设备。
  • iOS 模拟器(需要安装 Xcode 工具)。
  • Android 模拟器(需要在 Android Studio 中设置)。
  • 浏览器(需要使用 Chrome,以便进行调试)。
  • 对于 WindowsLinuxmacOS 桌面应用,您必须在打算部署到的平台上进行开发。因此,如果您要开发 Windows 桌面应用,则必须在 Windows 上进行开发,才能使用相应的构建链。如需详细了解针对各种操作系统的具体要求,请访问 docs.flutter.dev/desktop

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

打开项目并运行应用

  1. 在您选择的编辑器中打开项目。
  2. 按照所选编辑器的《使用入门:试驾》中的说明“运行应用”。

大功告成!您应该会在设备上看到 MDC-101 Codelab 中的 Shrine 登录页面。

Android

iOS

包含用户名和密码字段、“取消”和“下一步”按钮的登录页面

包含用户名和密码字段、“取消”和“下一步”按钮的登录页面

现在,登录屏幕看起来很好,让我们在应用中增加一些商品。

4. 添加顶部应用栏

现在,如果您点击“Next”按钮,就可以看到显示“You did it!”的主屏幕。太棒了!但现在用户没有任何可执行的操作,或者对在应用中的位置知情。为提供帮助,是时候添加导航了。

Material Design 可提供确保高度易用性的导航模式。顶部应用栏是最明显的组件之一。

为了提供导航功能并让用户快速执行其他操作,我们来添加一个顶部应用栏。

添加 AppBar widget

home.dart 中,向 Scaffold 中添加 AppBar,并移除突出显示的 const

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

AppBar 添加到 Scaffold 的 appBar: 字段中,即可免费获得完美的布局,将 AppBar 保持在页面顶部和正文下方。

保存您的项目。当 Shrine 应用更新时,点击 Next 查看主屏幕。

Android

iOS

屏幕显示“你做到了!”

屏幕显示“你做到了!”

AppBar 看起来很棒,但还需要一个标题。

添加 Text 微件

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

// TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
    title: const Text('SHRINE'),
    // TODO: Add trailing buttons (102)

保存您的项目。

Android

iOS

以 Shrine 为标题的应用栏

以 Shrine 为标题的应用栏

许多应用栏的标题旁边都有一个按钮。让我们在应用中添加一个菜单图标。

添加位于首部的 IconButton

同样,仍是在 home.dart 中,为 AppBar 的 leading: 字段设置一个 IconButton。(将其放在 title: 字段前,以模拟从首到尾的顺序):

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

保存您的项目。

Android

iOS

一个应用栏,标题为 Shrine,标题为汉堡图标

一个应用栏,标题为 Shrine,标题为汉堡图标

菜单图标(也称为“汉堡图标”)会显示在您预期的位置。

您也可以在标题尾部添加按钮。在 Flutter 中,它们被称为“操作”。

添加操作

剩余空间还可以添加两个 IconButton。

将它们添加到标题后的 AppBar 实例中:

// TODO: Add trailing buttons (102)
actions: <Widget>[
  IconButton(
    icon: const Icon(
      Icons.search,
      semanticLabel: 'search',
    ),
    onPressed: () {
      print('Search button');
    },
  ),
  IconButton(
    icon: const Icon(
      Icons.tune,
      semanticLabel: 'filter',
    ),
    onPressed: () {
      print('Filter button');
    },
  ),
],

保存您的项目。主屏幕看起来应该像下面这样:

Android

iOS

一个应用栏,标题为 Shrine,标题为汉堡图标,结尾带有搜索图标和自定义图标

一个应用栏,标题为 Shrine,标题为汉堡图标,结尾带有搜索图标和自定义图标

现在,应用的右侧有一个首部按钮、一个标题和两个操作。应用栏还使用细微的阴影来显示高度,表示其与内容位于不同的层级。

5. 在网格中添加卡

应用现在已初步成型,让我们接着放置一些卡片来组织内容。

添加 GridView

首先,在顶部应用栏下方添加一张卡片。单独使用卡片微件的信息不足,无法使其出现在正确的位置,因此我们需要将其封装在 GridView 微件中。

将 Scaffold 正文的中心替换为 GridView:

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const 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 的 4 条边设置内边距。当然,您看不到尾部或底边的内边距,因为这两边旁边还没有 GridView 子项。

childAspectRatio: 字段会根据宽高比(宽度与高度的比)确定项目的大小。

默认情况下,GridView 将创建大小相同的图块。

我们有一个空卡片,让我们为卡片添加一些子级微件。

布局内容

卡片应包含一张图片、一个标题和一个辅助文本。

更新 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: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              const SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

此代码会添加一个列微件,用于垂直布局子微件。

crossAxisAlignment: field 指定 CrossAxisAlignment.start,这意味着“将文本与前缘对齐”。

无论提供何种类型的图片,AspectRatio 微件都会决定图片所采用的形状。

Padding 使得文本与边框保持一定距离。

两个 Text 微件垂直堆叠,它们之间保持 8 个单位的间隔 (SizedBox)。我们将使用另一个 Column 来把它们放到 Padding 中。

保存您的项目。

Android

iOS

包含图片、标题和辅助文本的单项内容

包含图片、标题和辅助文本的单项内容

在此预览中,您可以看到卡片从边缘插入,带有圆角和阴影(这表示卡片的高度)。整个形状在 Material 中被称为“容器”。(不要与名为 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) {
      return 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: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: const <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: const 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/product.dart';
import 'model/products_repository.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.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: const 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.titleLarge,
                    maxLines: 1,
                  ),
                  const SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.titleSmall,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

注意:应用现在还无法编译和运行。我们还需要进行一项更改。

此外,在尝试编译之前,更改 build() 函数以将 BuildContext 传递到 _buildGridCards()

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: const EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(context) // Changed code
),

热重启应用。

Android

iOS

包含图片、商品名和价格的商品网格

包含图片、商品名和价格的商品网格

您可能已经注意到,我们未在卡片之间添加任何垂直的间隔。这是因为默认情况下顶部和底部外边距为 4 个点。

保存您的项目。

商品数据显示出来了,但图像四周有额外的空间。图像默认依据 .scaleDownBoxFit 绘制(在这个情况下)。让我们将其更改为 .fitWidth,以便将图像略微放大一点,删除多余的空白。

向图像添加 fit: 字段,值为 BoxFit.fitWidth

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

Android

iOS

包含剪裁图片、商品名和价格的商品网格

包含剪裁图片、商品名和价格的商品网格

现在,我们的商品完美地展现在应用中了!

7. 恭喜!

我们的应用已经有了基本的流程,可将用户从登录页面转到主屏幕,然后用户可在主屏幕中查看商品。通过几行代码,我们添加了一个顶部应用栏(包含标题和三个按钮)以及卡片(用于展示应用的内容)。我们的主屏幕简洁实用,具有基本的结构和可操作的内容。

后续步骤

在顶部应用栏、卡片、文本字段和按钮中,我们现在使用 Material Flutter 库中的四个核心组件!您可以访问 Material 组件 widget 目录探索更多内容。

虽然我们的应用完全可以正常运行,但它尚未表达任何特殊的品牌或观点。在 MDC-103:通过颜色、形状、高度和类型设置 Material Design 主题中,我们将自定义这些组件的样式,来诠释一个充满活力的、现代的品牌。

我能够用合理的时间和精力完成此 Codelab

非常同意 同意 一般 不同意 非常不同意

我希望日后继续使用 Material Components

非常同意 同意 一般 不同意 非常不同意