深入了解 Dart 的模式和记录

1. 简介

Dart 3 将模式引入到该语言中,这是一种重要的新语法类别。除了这种新的 Dart 代码编写方式之外,还有其他一些语言增强功能,包括用于将不同类型的数据捆绑在一起的记录、用于控制访问权限的类修饰符,以及新的 switch 表达式 和 if-case 语句

这些功能可让您在编写 Dart 代码时有更多选择。在此 Codelab 中,您将学习如何使用它们来使您的代码更紧凑、精简、灵活。

此 Codelab 假设您对 Flutter 和 Dart 有一定的了解,但您不了解它们也无妨。在开始之前,我们建议您通过以下资源复习一下基础知识:

您将构建的内容

此 Codelab 将在 Flutter 中创建一个显示 JSON 文档的应用。该应用可以模拟来自外部来源的 JSON。JSON 包含文档数据,例如修改日期、标题、页眉和段落。您将编写代码,将数据整齐地打包到记录中,这样一来,每当您的 Flutter widget 需要这些数据时,系统均可传输和解压缩它们。

然后,当值与模式匹配时,您可以使用模式来构建适当的 widget。您还将了解如何使用模式将数据解构为局部变量。

您在此 Codelab 中构建的最终应用,其中显示了一个包含标题、最后修改日期、页眉和段落的文档。

学习内容

  • 如何创建记录来存储多个不同类型的值。
  • 如何使用记录从函数返回多个值。
  • 如何使用模式来匹配、验证和解构来自记录和其他对象的数据。
  • 如何将与模式匹配的值绑定到新变量或现有变量。
  • 如何使用新的 switch 语句功能、switch 表达式和 if-case 语句。
  • 如何充分利用详尽检查来确保 switch 语句或 switch 表达式中处理了每种情况。

2. 设置您的环境

  1. 安装 Flutter SDK
  2. 设置编辑器,例如 Visual Studio Code (VS Code)。
  3. 针对至少一个目标平台(iOS、Android、桌面设备或网络浏览器)完成平台设置步骤。

3. 创建项目

在深入了解模式、记录和其他新功能之前,请先花点时间设置您的环境和一个简单的 Flutter 项目,您将为该项目编写所有代码。

获取 Dart

  • 为了确保您使用的是 Dart 3,请运行以下命令:
flutter channel stable
flutter upgrade
dart --version # This should print "Dart SDK version: 3.0.0" or higher

创建 Flutter 项目

  1. 使用 flutter create 命令创建一个名为 patterns_codelab 的新项目。--empty 标记会导致无法在 lib/main.dart 文件中创建标准计数器应用,您必须将其移除。
flutter create --empty patterns_codelab
  1. 然后,使用 VS Code 打开 patterns_codelab 目录。
code patterns_codelab

VS Code 屏幕截图,其中显示了使用“flutter create”命令创建的项目。

设置最低 SDK 版本

  • 为项目设置 SDK 版本限制,以便依赖于 Dart 3 或更高版本。

pubspec.yaml

environment:
  sdk: ^3.0.0

4. 设置项目

在这一步中,您将创建两个 Dart 文件:

  • main.dart 文件,其中包含应用使用的 widget。
  • data.dart 文件,用于提供应用数据。

为应用定义数据

  • 创建一个新文件 lib/data.dart,并向其中添加以下代码:

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);
}

const documentJson = '''
{
  "metadata": {
    "title": "My Document",
    "modified": "2023-05-10"
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    {
      "type": "p",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
      "type": "checkbox",
      "checked": false,
      "text": "Learn Dart 3"
    }
  ]
}
''';

想象一个从外部来源接收数据的程序,如 I/O 流或 HTTP 请求。在此 Codelab 中,您将通过 documentJson 变量中的多行字符串来模拟传入的 JSON 数据,从而简化这一更为现实的用例。

JSON 数据在 Document 类中定义,稍后在此 Codelab 中,您将添加用于从解析的 JSON 返回数据的函数。此类用于在其构造函数中定义并初始化 _json 字段。

运行应用

flutter create 命令用于创建 lib/main.dart 文件作为默认 Flutter 文件结构的一部分。

  1. 若要为应用创建起始代码,请将 main.dart 的内容替换为以下代码:

lib/main.dart

import 'package:flutter/material.dart';

import 'data.dart';

void main() {
 runApp(const DocumentApp());
}

class DocumentApp extends StatelessWidget {
 const DocumentApp({super.key});

 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     theme: ThemeData(useMaterial3: true),
     home: DocumentScreen(
       document: Document(),
     ),
   );
 }
}

class DocumentScreen extends StatelessWidget {
 final Document document;

 const DocumentScreen({
   required this.document,
   Key? key,
 }) : super(key: key);

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Title goes here'),
     ),
     body: Column(
       children: [
         Center(
           child: Text('Body goes here'),
         ),
       ],
     ),
   );
 }
}

您向应用添加了以下两个 widget:

  • DocumentApp 用于设置 Material Design 的最新版本,以便设置界面的主题。
  • DocumentScreen 用于使用 Scaffold widget 提供页面的视觉布局。
  1. 为了确保一切顺利运行,请点击 Run and Debug,在宿主机上运行应用:

“Run and debug”按钮图片,该按钮位于左侧活动栏的“Run and debug”部分。

  1. 默认情况下,Flutter 会选择可用的目标平台。若要更改目标平台,请在状态栏中选择当前平台:

VS Code 中的目标平台选择器的屏幕截图。

您应该会看到一个空的框架,其中包含在 DocumentScreen widget 中定义的 titlebody 元素:

此步骤中构建的应用的屏幕截图。

5. 创建并返回记录

在这一步中,您将利用记录从函数调用返回多个值。然后,您将在 DocumentScreen widget 中调用该函数,以便访问这些值并将它们反映在界面中。

创建并返回记录

  • data.dart 中,向名为 getMetadata 的文档类添加一个用于返回记录的新函数:

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  var title = "My Document";
  var now = DateTime.now();

  return (title, modified: now);
}

此函数的返回值类型是包含两个字段的记录,其中一个字段的类型为 String,另一个字段的类型为 DateTime

return 语句能够将两个值括在括号中,从而创建一个新记录:(title, modified: now)

第一个字段是位置字段,而且未命名,第二个字段被命名为 modified

访问记录字段

  1. DocumentScreen widget 中,通过在 build 方法中调用 getMetadata(),可以获取记录并访问其值:

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var metadataRecord = document.getMetadata();

    return Scaffold(
      appBar: AppBar(
        title: Text(metadataRecord.$1),
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',
            ),
          ),
        ],
      ),
    );
  }

getMetadata() 函数会返回一条记录,系统会将其赋值给局部变量 metadataRecord。记录是一种从单个函数调用返回多个值并将其赋值给变量的简便方法。

若要访问记录中包含的各个字段,您可以使用记录的内置 getter 语法。

  • 若要获取位置字段(没有名称的字段,例如 title),请对记录使用 getter $<num>。这将仅返回未命名的字段。
  • 已命名字段(例如 modified)没有位置 getter,因此您可以直接使用其名称,比如 metadataRecord.modified

若要确定位置字段的 getter 的名称,请从 $1 开始,并跳过已命名的字段。例如:

var record = (named: v', ‘y', named2: x', ‘z');
print(record.$1); // prints y
print(record.$2) // prints z
  1. 热重载以查看应用中显示的 JSON 值。您每次保存文件时,VS Code Dart 插件都会热重载。

应用的屏幕截图,其中显示了标题和修改日期。

您可以看到每个字段确实都保持了其类型。

  • Text() 方法可将字符串作为其第一个参数。
  • modified 字段是 DateTime,并使用字符串插值转换为 String

要返回不同类型的数据,另一种类型安全的方式是定义一个类,这种方法更详细。

6. 通过模式进行匹配和解构

记录可以高效收集不同类型的数据,并轻松地传递它们。接下来,您将利用模式来改进您的代码

模式表示一个或多个值可以采用的结构,如蓝图。模式会与实际值进行比较,以确定它们是否匹配

有些模式(匹配时)会从匹配的值中提取数据,来解构匹配的值。通过解构,您可以解压缩对象中的值,以便将它们赋值给局部变量,或对它们执行进一步匹配。

将记录解构为局部变量

  1. 重构 DocumentScreenbuild 方法,以便调用 getMetadata(),并使用它来初始化模式变量声明

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title), // New
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified', // New
            ),
          ),
        ],
      ),
    );
  }

记录模式 (title, :modified) 包含两个变量模式,这些模式会与 getMetadata() 返回的记录字段进行匹配。

  • 该表达式与子模式匹配,因为结果是一个包含两个字段的记录,其中一个字段名为 modified
  • 因为它们匹配,所以变量声明模式会解构表达式,访问它的值,并将这些值绑定到具有相同类型和名称的新局部变量 String titleDateTime modified

变量模式 :modified 的语法是 modified: modified 的简写。如果您想要一个具有不同名称的新局部变量,可以改为写入 modified: localModified

  1. 热重载后会显示与上一步相同的结果。行为完全相同;但代码更加简短。

7. 使用模式提取数据

在某些上下文中,模式不仅可以匹配和解构,还可以根据模式是否匹配来决定代码执行什么操作。这些称为可反驳模式

您在上一步中使用的变量声明模式就是一个可反驳模式:值必须与模式匹配,否则就是错误的,并且不会发生解构。想想任何变量声明或赋值;如果它们的类型不相同,则不能为变量赋值。

另一方面,可反驳模式用于控制流上下文:

  • 它们期望作为比较对象的某些值不匹配。
  • 它们旨在根据值是否匹配来影响控制流。
  • 如果它们不匹配,它们不会中断执行并报错,而是会移到下一语句。
  • 它们可以解构和绑定仅在匹配时才可使用的变量

读取 JSON 值(不使用模式)

在这一部分中,您将在不进行模式匹配的情况下读取数据,看看模式如何帮助您处理 JSON 数据。

  • 将以前版本的 getMetadata() 替换为从 _json 映射读取值的版本。将这个版本的 getMetadata() 复制并粘贴到 Document 类:

lib/data.dart

(String, {DateTime modified}) getMetadata() {
  if (_json.containsKey('metadata')) {
    var metadataJson = _json['metadata'];
    if (metadataJson is Map) {
      var title = metadataJson['title'] as String;
      var localModified = DateTime.parse(metadataJson['modified'] as String);
      return (title, modified: localModified);
    }
  }
  throw const FormatException('Unexpected JSON');
}

此代码可在不使用模式的情况下验证数据的结构是否正确。在稍后的步骤中,您将使用模式匹配以更少的代码执行相同的验证。它在执行任何其他操作之前都会进行三项检查:

  • JSON 包含您期望的数据结构if (_json.containsKey('metadata'))
  • 数据具有您期望的类型if (metadataJson is Map)
  • 数据不为 null,这在前面的检查中已隐式确认。

使用映射模式读取 JSON 值

借助可反驳模式,您可以使用映射模式验证 JSON 是否具有预期的结构。

  • 使用以下代码替换以前版本的 getMetadata()

lib/data.dart

  (String, {DateTime modified}) getMetadata() {
    if (_json
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }
  }

在这里,您会看到一种新的 if 语句(在 Dart 3 中引入),即 if-case。仅在 case 模式与 _json 中的数据匹配时,case 主体才会执行。此匹配完成您在第一版 getMetadata() 中编写的相同检查,以验证传入的 JSON。此代码验证以下内容:

  • _json 是映射类型。
  • _json 包含一个 metadata 键。
  • _json 不为 null。
  • _json['metadata'] 也是一种映射类型。
  • _json['metadata'] 包含 titlemodified 键。
  • titlelocalModified 是字符串,并且不为 null。

如果值不匹配,模式将拒绝(拒绝继续执行),并前进到 else 子句。如果匹配成功,该模式将从映射中解构 titlemodified 的值,并将它们绑定到新的局部变量。

有关模式的完整列表,请参阅功能规范的模式部分中的表格

8. 让应用为更多模式做好准备

到目前为止,您处理的是 JSON 数据的 metadata 部分。在这一步中,您将进一步细化业务逻辑,以便处理 blocks 列表中的数据,并将其呈现到应用中。

{
  "metadata": {
    // ...
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    // ...
  ]
}

创建一个用于存储数据的类

  • data.dart 添加一个新类 Block,用于读取和存储 JSON 数据中其中一个块的数据。

lib/data.dart

class Block {
  final String type;
  final String text;
  Block(this.type, this.text);

  factory Block.fromJson(Map<String, dynamic> json) {
    if (json case {'type': var type, 'text': var text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

工厂构造函数 fromJson() 使用相同的 if-case,以及您在前面看到过的映射模式。

请注意,json 匹配映射模式,即使其中一个键 checked 未包含在模式中。映射模式会忽略映射对象中未在模式中明确说明的任何条目。

返回 Block 对象列表

  • 接下来,向 Document 类添加一个新函数 getBlocks()getBlocks() 将 JSON 解析为 Block 类的实例,并返回要在界面中呈现的块的列表:

lib/data.dart

  List<Block> getBlocks() {
    if (_json case {'blocks': List blocksJson}) {
      return <Block>[
        for (var blockJson in blocksJson) Block.fromJson(blockJson)
      ];
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }

getBlocks() 函数会返回 Block 对象列表,您稍后将使用这些对象来构建界面。熟悉的 if-case 语句会执行验证,并将 blocks 元数据的值转换为名为 blocksJson 的新 List(如果没有模式,则需要使用 toList() 方法进行转换)。

列表文字包含一个集合,以便用 Block 对象填充新列表。

本部分不介绍您尚未在此 Codelab 中尝试过的任何与模式相关的功能。在下一步中,您将准备在界面中呈现列表项。

9. 使用模式显示文档

现在,您使用 if-case 语句和可反驳的模式成功地解构和重组了 JSON 数据。但是,if-case 只是增强功能之一,用于控制模式附带的流结构。现在,您将对可反驳模式的了解应用于 switch 语句。

使用 switch 语句来控制使用模式呈现的内容

  • main.dart 中,创建一个新 widget BlockWidget,它根据每个块的 type 字段确定每个块的样式。

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(
        block.text,
        style: textStyle,
      ),
    );
  }
}

build 方法中的 switch 语句会打开 block 对象的 type 字段。

  1. 第一个 case 语句使用常量字符串模式。如果 block.type 等于常量值 h1,则模式匹配。
  2. 第二个 case 语句使用逻辑或模式,其中使用两个常量字符串模式作为其子模式。如果 block.type 匹配子模式 pcheckbox,则模式匹配。
  1. 最后一个 case 语句是通配符模式 _。switch case 中的通配符匹配所有其他内容。它们的行为与 default 子句相同,在 switch 语句中仍允许使用此类子句(它们只是更冗长一点)。

可在任何允许使用模式的地方使用通配符模式,例如在变量声明模式中:var (title, _) = document.getMetadata();

在此上下文中,通配符不绑定任何变量。它会舍弃第二个字段。

在下一部分,您将在显示 Block 对象后了解更多 switch 功能。

显示文档内容

通过在 DocumentScreen widget 的 build 方法中调用 getBlocks(),创建一个包含 Block 对象列表的局部变量。

  1. DocumentationScreen 中的现有 build 方法替换为此版本:

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var blocks = document.getBlocks(); // New

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          // New
          Text('Last modified: $modified'),
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }

BlockWidget(block: blocks[index]) 行用于为从 getBlocks() 方法返回的块列表中的每个条目构建一个 BlockWidget widget。

  1. 运行应用,然后您应该会看到屏幕上出现的块:

显示 JSON 数据“块”部分内容的应用屏幕截图。

10. 使用 switch 表达式

模式为 switchcase 添加了很多功能。为了让它们在更多地方可用,Dart 提供了 switch 表达式。可以通过一系列 case 语句直接向变量赋值语句或 return 语句提供值。

将 switch 语句转换为 switch 表达式

Dart 分析器提供辅助功能来帮助您对代码进行更改。

  1. 将光标移至上一部分中的 switch 语句。
  2. 点击灯泡图标,以查看可用的辅助功能。
  3. 选择 Convert to switch expression 辅助功能。

VS Code 中可用的“convert to switch expression”辅助功能的屏幕截图。

此代码的新版本如下所示:

TextStyle? textStyle;
textStyle = switch (block.type) {
  'h1' => Theme.of(context).textTheme.displayMedium,
  'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
  _ => Theme.of(context).textTheme.bodySmall
};

switch 表达式看起来类似于 switch 语句,但它去掉了 case 关键字,并使用 => 将模式与 case 主体分隔开来。与 switch 语句不同,switch 表达式返回一个值,并且可以在任何可以使用表达式的地方使用。

11. 使用对象模式

Dart 是一种面向对象的语言,因此模式适用于所有对象。在这一步中,您为一个对象模式编写 switch 表达式,并解构对象属性,以增强界面的日期呈现逻辑。

从对象模式中提取属性

在这一部分,您将使用模式来改进上次修改日期的显示方式。

  • formatDate 方法添加到 main.dart

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: var days) => '$days days from now',
  };
}

此方法会返回一个 switch 表达式,该表达式可打开值 difference(一个 Duration 对象)。它表示 today 与 JSON 数据中的 modified 值之间的时间跨度。

switch 表达式的每个 case 语句都使用一个对象模式,该模式通过在对象的属性 inDaysisNegative 上调用 getter 来进行匹配。语法看起来像是在构建一个 Duration 对象,但它实际上是访问 difference 对象的字段。

前三个 case 语句使用常量子模式 01-1 来匹配对象属性 inDays,并返回相应的字符串。

最后两个 case 处理在今天、昨天和明天之外的持续时间:

  • 如果 isNegative 属性匹配布尔常量模式 true,则意味着修改日期是过去的日期,它会显示“days ago”
  • 如果该 case 语句没有发现差异,则持续时间肯定为正数天数(无需使用 isNegative: false 明确验证),因此修改日期是未来的日期,它会显示“days from now”。

为周添加格式化逻辑

  • 为格式化函数添加两个新 case,用于识别超过 7 天的持续时间,以便界面可以将其显示为“weeks”

lib/main.dart

String formatDate(DateTime dateTime) {
  var today = DateTime.now();
  var difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: var days) when days > 7 => '${days ~/ 7} weeks from now', // New
    Duration(inDays: var days) when days < -7 => '${days.abs() ~/ 7} weeks ago', // New
      Duration(inDays: var days, isNegative: true) => '${days.abs()} days ago',
      Duration(inDays: var days) => '$days days from now',
  };
}

这段代码引入了 guard 子句

  • guard 子句在 case 模式之后使用 when 关键字。
  • 它们可用在 if-case、switch 语句和 switch 表达式中。
  • 它们只会向匹配的模式添加条件
  • 如果 guard 子句求值为 false,则整个模式会被反驳,并接着执行下一个 case。

将新格式化的日期添加到界面

  1. 最后,更新 DocumentScreen 中的 build 方法,以便使用 formatDate 函数:

lib/main.dart

  @override
  Widget build(BuildContext context) {
    var (title, :modified) = document.getMetadata();
    var formattedModifiedDate = formatDate(modified); // New
    var blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'), // New
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) =>
                BlockWidget(block: blocks[index]),
            ),
          ),
        ],
      ),
    );
  }
  1. 热重载以查看应用中的更改:

使用 formatDate() 函数显示字符串“Last modified: 2 weeks ago”的应用的屏幕截图。

12. 封闭类以编写详尽的 switch

请注意,您没有在最后一个 switch 的末尾使用通配符或默认 case。尽管最好始终针对可能失败的值包含一个 case,但在像这样的简单示例中不包含也可以,因为您知道您定义的 case 考虑了 inDays 可能具有的所有可能值。

当 switch 中的每个 case 都会被处理时,就称为详尽的 switch。例如,对于 bool 类型,包含分别与 truefalse 对应的 case 语句的 switch 就是详尽的 switch。对于 enum 类型,包含分别与枚举的每个值对应的 case 语句的 switch 就是详尽的 switch,因为枚举表示固定数量的常量值。

Dart 3 使用新的类修饰符 sealed,将详尽检查扩展到对象和类层次结构。将您的 Block 类重构为封闭的父类。

创建子类

  • data.dart 中,创建三个用于扩展 Block 的新类:HeaderBlockParagraphBlockCheckboxBlock

lib/data.dart

class HeaderBlock extends Block {
  final String text;
  HeaderBlock(this.text);
}

class ParagraphBlock extends Block {
  final String text;
  ParagraphBlock(this.text);
}

class CheckboxBlock extends Block {
  final String text;
  final bool isChecked;
  CheckboxBlock(this.text, this.isChecked);
}

这些类中的每一个都对应于原始 JSON 中的不同 type 值:'h1''p''checkbox'

封闭父类

  • Block 类标记为 sealed。然后,将 if-case 重构为一个 switch 表达式,用于返回与 JSON 中指定的 type 对应的子类:

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

sealed 关键字是一个类修饰符,这意味着您可以仅在同一个库中扩展或实现此类。由于分析器知道此类的子类型,因此如果 switch 未能涵盖其中一个子类型且不详尽,分析器会报告错误。

使用 switch 表达式来显示 widget

  1. 通过一个针对每种 case 使用对象模式的 switch 表达式,更新 main.dart 中的 BlockWidget 类:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      child: switch (block) {
        HeaderBlock(:var text) => Text(
          text,
          style: Theme.of(context).textTheme.displayMedium,
        ),
        ParagraphBlock(:var text) => Text(text),
        CheckboxBlock(:var text, :var isChecked) => Row(
          children: [
            Checkbox(value: isChecked, onChanged: (_) {}),
            Text(text),
          ],
        ),
      },
    );
  }
}

在第一版 BlockWidget 中,您为 Block 对象的一个字段编写了 switch 表达式,以便返回 TextStyle。现在,您为 Block 对象本身的实例编写 switch 表达式,并匹配表示其子类的对象模式,在此过程中提取对象的属性。

Dart 分析器可以检查是否每个子类都在 switch 表达式中得到处理,因为您将 Block 设成了封闭的类。

另外请注意,在这里使用 switch 表达式可以将结果直接传递给 child 元素,而之前则需要单独的 return 语句。

  1. 热重载以查看首次呈现的复选框 JSON 数据:

显示复选框“Learn Dart 3”的应用屏幕截图

13. 恭喜

您已成功尝试了模式、记录、增强的 switch 和 case,以及封闭的类。您了解了大量信息,但这只是触及了这些功能的皮毛。有关模式的更多信息,请参阅功能规范

不同的模式类型、它们可能出现的不同上下文,以及子模式的潜在嵌套,使得行为的可能性好像是无穷无尽的。但它们浅显易懂。

您可以想象各种使用模式在 Flutter 中显示内容的方式。利用模式,您可以安全地提取数据,以便用几行代码来构建界面。

接下来做什么?

  • 在 Dart 文档的语言部分查看关于以下内容的文档:模式、记录、增强的 switch 和 case,以及类修饰符。

参考文档

在相应代码库中查看完整示例。

如需关于每项新功能的详细规范,请查看原始设计文档: