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

1. 简介

logo_components_color_2x_web_96dp.png

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 开发移动应用,您需要执行以下操作:

  1. 下载并安装 Flutter SDK。
  2. 更新 PATH 以包含 Flutter SDK。
  3. 安装 Android Studio 并添加 Flutter 和 Dart 插件,或者选择您喜欢的编辑器。
  4. 安装 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.导航至 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'"(运行 ‘flutter packages get')。

然后重新启动 Android Studio。

运行起始应用

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

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

2.启动 Flutter 应用:

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

成功!您应该会在模拟器中看到 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 点的填充。

保存项目:

产品数据已经显示,但图像周围有多出的空间。默认情况下(在本例中), 图像会由 .scaleDownBoxFit 绘制。我们将其更改为 .fitWidth,以便将图像放大一些,然后删除多余的空白。

fit: 字段添加到图像,并将值设为 BoxFit.fitWidth

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

Android

iOS

我们的产品此时会在应用中完美地显示!

7. 总结

我们的应用的基本流程是,将用户从登录屏幕带至主屏幕,然后用户可以在主屏幕浏览产品。 只需要几行代码,我们就添加了顶部应用栏(包含标题和三个按钮)和卡片(用于显示我们应用的内容)。我们的主屏幕现在是一个简单的功能性屏幕,具有基本结构和可操作的内容。

后续步骤

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

虽然我们的应用功能齐全,但尚未表现任何特定的品牌或观点。在 MDC-103:Material Design 主题中的颜色、形状、高度和类型中,我们将自定义这些组件的样式,以表现充满活力的现代品牌。

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

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

我未来会继续使用 Material Components

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