MDC-102 Flutter:Material 结构和布局 (Flutter)

logo_components_color_2x_web_96dp.png

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

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

构建内容

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

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

Android

iOS

此 Codelab 中用到的 MDC 组件

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

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

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

前期准备

如需开始使用 Flutter 开发移动应用,您需要执行以下操作:

  1. 下载并安装 Flutter SDK。
  2. 使用 Flutter SDK 更新您的 PATH。
  3. 安装带有 Flutter 和 Dart 插件的 Android Studio 或您喜欢使用的编辑器。
  4. 安装 Android 模拟器、iOS 模拟器(需要装有 Xcode 的 Mac)或使用实体设备。

如需详细了解 Flutter 安装说明,请参阅开始使用:安装。如需设置编辑器,请参阅开始使用:设置编辑器。安装 Android 模拟器时,您可以根据需要选择默认选项,例如带有最新系统映像的 Pixel 3 手机。建议启用虚拟机加速功能,但这并非强制要求。完成上述 4 个步骤后,您可以返回此 Codelab。您只需要为一个平台(Android 或 iOS)安装 Flutter 即可完成此 Codelab。

确保 Flutter SDK 的版本符合要求

在继续学习此 Codelab 之前,请确保 SDK 版本符合要求。如果 Flutter SDK 是之前安装的,请使用 flutter upgrade 确保 SDK 是最新版本。

 flutter upgrade

运行 flutter upgrade 将自动运行 flutter doctor.。如果此次是全新的 Flutter 安装,则无需升级,手动运行 flutter doctor 即可。它将报告您是否需要安装任何依赖项才能完成此安装操作。您可以忽略与您的情况无关的勾选项(例如,如果您不打算开发 iOS 应用,就可以忽略 Xcode)。

 flutter doctor

常见问题解答

从 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

3. 导航到 material-components-flutter-codelabs/mdc_100_series 目录并点击“Open”。此时项目应会打开。在构建一次项目之前,您可以忽略您在 Dart Analysis 中看到的任何错误。

4. 如果出现提示,请执行以下操作:

  • 安装所有的平台和插件更新或 FlutterRunConfigurationType。
  • 如果未配置 Dart 或 Flutter SDK,请设置 Flutter 插件的 Flutter SDK 路径
  • 配置 Android 框架。
  • 点击“Get dependencies”或“Run ‘flutter packages get'”。

然后重启 Android Studio。

运行入门版应用

以下说明假定您在 Android 模拟器或设备上测试,但如果您安装了 Xcode,则还可以在 iOS 模拟器或设备上进行测试。

1. 选择设备或模拟器。如果 Android 模拟器尚未运行,请依次选择 Tools -> Android -> AVD Manager创建虚拟设备并启动模拟器。如果 AVD 已存在,您可以直接从 Android Studio 中的设备选择器启动模拟器,如下一步所示。(对于 iOS 模拟器,如果它尚未运行,请依次选择 Flutter Device Selection -> Open iOS Simulator 在开发机器上启动模拟器。)

2. 启动 Flutter 应用:

  • 查看编辑器屏幕顶部的 Flutter Device Selection 下拉菜单,然后选择设备(例如,iPhone SE 或 Android SDK built for <版本>)。
  • Play 图标 ()。

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

Android

iOS

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

现在,如果您点击“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 看起来很棒,但还需要一个标题。

添加 Text 微件

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 实例中:

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

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

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

添加 GridView

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

将 Scaffold 正文的中心替换为 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 的 4 条边设置内边距。当然,您看不到尾部或底边的内边距,因为这两边旁边还没有 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'),
            ],
          ),
        ),
      ],
    ),
  )
],

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

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

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

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

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

保存您的项目:

Android

iOS

在此预览中,您可以看到卡片从边缘插入,带有圆角和阴影(这表示卡片的高度)。整个形状在 Material 中被称为“容器”。(不要与名为 Container 的实际微件类混淆。)

卡片通常以集合的形式和其他卡片一起出现,让我们在网格中给它们布局。

当屏幕上出现多张卡片时,它们就会组成一个或多个集合。集合中的卡片是共面的,这意味着卡片共享相同的静止高度(除非卡片被拾起或拖动,但在这里我们不会这么做)。

将卡片添加到集合中

现在,我们在 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 个单位的填充。

保存您的项目:

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

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

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

Android

iOS

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

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

后续步骤

通过顶部应用栏、卡片、文本字段和按钮,我们已经使用了 MDC-Flutter 库中的四个核心组件!您可以访问 Flutter 微件目录,探索更多组件。

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

下一个 Codelab

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

非常赞同 赞同 中立 不赞同 非常不赞同

我希望日后继续使用 Material 组件

非常赞同 赞同 中立 不赞同 非常不赞同