深入探索 Dart's 的規律和記錄

深入瞭解 Dart 的模式和記錄

程式碼研究室簡介

subject上次更新時間:4月 4, 2025
account_circle作者:John Ryan and Marya Belanger

1. 簡介

Dart 3 為語言引進模式,這是一項全新的主要文法類別。除了這項 Dart 程式碼編寫的新方法之外,還有其他幾項語言增強功能,包括:

  • 記錄:用於將不同類型的資料彙整在一起
  • 類別修飾符 (用於控管存取權),以及
  • 新的 switch 運算式if-case 陳述式

這些功能可讓您在編寫 Dart 程式碼時有更多選擇。在本程式碼研究室中,您將瞭解如何使用這些功能,讓程式碼更精簡、流暢且具彈性。

本程式碼研究室假設您對 Flutter 和 Dart 有一定的瞭解。如果您覺得自己有點生疏,不妨參考下列資源溫習基礎知識:

建構項目

本程式碼研究室會建立一個應用程式,在 Flutter 中顯示 JSON 文件。應用程式會模擬來自外部來源的 JSON。JSON 檔案包含文件資料,例如修改日期、標題、標題列和段落。您可以編寫程式碼,將資料整齊地打包至記錄,以便在 Flutter 小工具需要時傳輸及解開。

然後,當值符合該模式時,您可以使用模式建構適當的小工具。您也會瞭解如何使用模式將資料解構為本機變數。

您在本程式碼研究室中建構的最終應用程式,以及包含標題、上次修改日期、標題和段落的文件。

課程內容

  • 如何建立可儲存不同類型值的記錄。
  • 如何使用記錄從函式傳回多個值。
  • 如何使用模式比對、驗證及解構記錄和其他物件的資料。
  • 如何將符合模式的值繫結至新變數或現有變數。
  • 如何使用新的 switch 陳述式功能、switch 運算式和 if-case 陳述式。
  • 如何利用完整性檢查功能,確保在 switch 陳述式或 switch 運算式中處理每個情況。

2. 設定環境

  1. 安裝 Flutter SDK
  2. 設定編輯器,例如 Visual Studio Code (VS Code)。
  3. 針對至少一個目標平台 (iOS、Android、電腦或網頁瀏覽器) 完成平台設定步驟。

3. 建立專案

在深入探討模式、記錄和其他新功能之前,請先花點時間建立一個簡單的 Flutter 專案,並在其中編寫所有程式碼。

建立 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 檔案,其中包含應用程式的小工具,以及
  • 提供應用程式資料的 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 要求) 接收資料。在本程式碼研究室中,您可以使用 documentJson 變數中的多行字串模擬傳入的 JSON 資料,簡化這個更貼近實際情況的用途。

JSON 資料是在 Document 類別中定義。在本程式碼研究室的後續內容中,您將新增可從剖析的 JSON 傳回資料的函式。這個類別會在建構函式中定義及初始化 _json 欄位。

執行應用程式

flutter create 指令會在預設的 Flutter 檔案結構中建立 lib/main.dart 檔案。

  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,
   
super.key,
 
});

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

您已將下列兩個小工具新增至應用程式:

  • DocumentApp 會設定最新版本的 Material Design,用於設定 UI 主題。
  • DocumentScreen 會使用 Scaffold 小工具提供網頁的視覺版面配置。
  1. 如要確保一切運作順暢,請按一下「Run and Debug」,在主機電腦上執行應用程式:

「Run and Debug」按鈕的圖片,位於左側活動列的「Run and Debug」部分。

  1. 根據預設,Flutter 會選擇可用的目標平台。如要變更目標平台,請在狀態列中選取目前的平台:

VS Code 中目標平台選取器的螢幕截圖。

您應該會看到空白的畫面,其中包含 DocumentScreen 小工具中定義的 titlebody 元素:

在這個步驟中建構的應用程式螢幕截圖。

5. 建立及傳回記錄

在這個步驟中,您會使用記錄從函式呼叫傳回多個值。接著,您可以在 DocumentScreen 小工具中呼叫該函式,存取值並在 UI 中顯示。

建立及傳回記錄

  • data.dart 中,將新的 getter 方法新增至名為 metadata 的 Document 類別,該方法會傳回記錄:

lib/data.dart

import 'dart:convert';

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

 
(String, {DateTime modified}) get metadata {           // Add from here...
   
const title = 'My Document';
   
final now = DateTime.now();

   
return (title, modified: now);
 
}                                                      // to here.
}

這個函式的傳回類型是含有兩個欄位的記錄,一個是 String 類型,另一個是 DateTime 類型。

傳回陳述式會將兩個值括入 (title, modified: now) 中,藉此建構新記錄。

第一個欄位是位置欄位且未命名,第二個欄位則命名為 modified

存取記錄欄位

  1. DocumentScreen 小工具中,呼叫 build 方法中的 metadata getter 方法,以便取得記錄並存取其值:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final metadataRecord = document.metadata;              // Add this line.

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

metadata getter 方法會傳回記錄,並指派給區域變數 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 方法,以便呼叫 metadata,並使用該方法初始化 模式變數宣告

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, modified: modified) = document.metadata;   // Modify

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

記錄模式 (title, modified: modified) 包含兩個變數模式,可與 metadata 傳回的記錄欄位相符。

  • 由於結果是包含兩個欄位的記錄,其中一個欄位名為 modified,因此運算式與子模式相符。
  • 由於兩者相符,變數宣告模式會解構運算式,存取其值,並將這些值繫結至具有相同類型和名稱的新本機變數 String titleDateTime modified

如果欄位名稱與填入該欄位的變數相同,則可使用簡寫。請按照下列方式重構 DocumentScreenbuild 方法。

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;            // Modify

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

變數模式 :modified 的語法是 modified: modified 的簡寫形式。如果您想要使用其他名稱的新本機變數,可以改為寫入 modified: localModified

  1. 熱重新載入,即可查看與上一個步驟相同的結果。行為完全相同,只是讓程式碼更精簡。

7. 使用模式擷取資料

在某些情況下,模式不僅會比對及解構,還可以根據模式是否相符,決定程式碼的功能這類模式稱為可駁斥模式

您在上一節使用的變數宣告模式是不可否認的模式:值必須符合模式,否則會發生錯誤,且不會進行重構。請想想任何變數宣告或指派,如果變數類型不一致,就無法指派值。

另一方面,可駁斥的模式則用於控制流程內容:

  • 他們預期比較的部分值不會相符。
  • 這些方法會根據值是否相符,影響控制流程
  • 如果不相符,則不會中斷執行程序,而是直接移至下一個陳述式。
  • 這些函式可解構及繫結「僅在相符時可用」的變數

讀取不含模式的 JSON 值

在本節中,您將讀取資料而不進行模式比對,瞭解模式如何協助您處理 JSON 資料。

  • 將舊版 metadata 替換為可讀取 _json 地圖值的版本。將這個版本的 metadata 複製貼到 Document 類別中:

lib/data.dart

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

  (String, {DateTime modified}) get metadata {
    if (_json.containsKey('metadata')) {                     // Modify from here...
      final metadataJson = _json['metadata'];
      if (metadataJson is Map) {
        final title = metadataJson['title'] as String;
        final localModified =
            DateTime.parse(metadataJson['modified'] as String);
        return (title, modified: localModified);
      }
    }
    throw const FormatException('Unexpected JSON');          // to here.
  }
}

此程式碼可驗證資料結構是否正確,而不需要使用模式。在後續步驟中,您將使用模式比對功能,以較少的程式碼執行相同的驗證作業。在執行其他操作之前,會先執行三項檢查:

  • JSON 包含預期的資料結構if (_json.containsKey('metadata'))
  • 資料具有您預期的類型if (metadataJson is Map)
  • 資料「非空值」,這項資訊已在先前的檢查中隱含確認。

使用對應模式讀取 JSON 值

您可以使用可駁斥的模式,透過對應模式驗證 JSON 是否具有預期的結構。

  • 使用下列程式碼取代先前的 metadata 版本:

lib/data.dart

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

  (String, {DateTime modified}) get metadata {
    if (_json                                                // Modify from here...
        case {
          'metadata': {
            'title': String title,
            'modified': String localModified,
          }
        }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }                                                        // to here.
  }
}

這裡顯示一種新的 if 陳述式 (在 Dart 3 中推出),即 if-case。只有在個案模式與 _json 中的資料相符時,系統才會執行個案主體。這項比對會執行與第一個版本 metadata 相同的檢查作業,以驗證傳入的 JSON。這段程式碼會驗證下列項目:

  • _json 是地圖類型。
  • _json 包含 metadata 鍵。
  • _json 不是空值。
  • _json['metadata'] 也是地圖類型。
  • _json['metadata'] 包含 titlemodified 鍵。
  • titlelocalModified 是字串,且非空值。

如果值不相符,模式會駁回 (拒絕繼續執行),並繼續執行 else 子句。如果比對成功,模式會從對應項目中解構 titlemodified 的值,並將這些值繫結至新的本機變數。

如需完整的模式清單,請參閱功能規格中的「模式」部分表格。

8. 為應用程式準備更多模式

到目前為止,您已處理 JSON 資料的 metadata 部分。在這個步驟中,您將進一步調整商業邏輯,以便處理 blocks 清單中的資料,並將資料算繪至應用程式。

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

建立儲存資料的類別

  • 將新類別 Block 新增至 data.dart,用於讀取及儲存 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': final type, 'text': final text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

工廠建構函式 fromJson() 會使用與先前所見相同的 if-case 和地圖模式。

您會發現 JSON 資料看起來與預期的模式相似,但其中有額外資訊 checked,而這項資訊並未出現在模式中。這是因為當您使用這類模式 (稱為「對應模式」) 時,系統只會在乎您在模式中定義的特定項目,並忽略資料中的其他項目。

傳回 Block 物件清單

  • 接著,請在 Document 類別中新增 getBlocks() 函式。getBlocks() 會將 JSON 剖析為 Block 類別的例項,並傳回要在 UI 中顯示的區塊清單:

lib/data.dart

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

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

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

getBlocks() 函式會傳回 Block 物件清單,您稍後可用於建構 UI。熟悉的 if-case 陳述式會執行驗證,並將 blocks 中繼資料的值轉換為名為 blocksJson 的新 List (如果沒有模式,您需要使用 toList() 方法進行轉換)。

清單字面值包含的集合,以便使用 Block 物件填入新清單。

本節不會介紹您尚未在本程式碼研究室中嘗試的任何模式相關功能。在下一個步驟中,您將準備在 UI 中算繪清單項目。

9. 使用模式顯示文件

您現在已成功使用 if-case 陳述式和可駁斥的模式,解構及重組 JSON 資料。不過,if-case 只是控制流程結構的強化功能之一,而這類結構是模式的一部分。接著,您將運用可駁斥模式的知識,應用於 switch 陳述式。

使用切換語句的模式控管要算繪的內容

  • main.dart 中,建立新的 BlockWidget 小工具,根據 type 欄位決定每個區塊的樣式。

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    super.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. 最後一個案例是 萬用字元模式 _。萬用字元會與 switch 陳述式中的所有其他項目相符。它們的運作方式與 default 子句相同,後者仍可用於 switch 陳述式 (只是會比較冗長)。

萬用字元模式可用於允許模式的任何位置,例如變數宣告模式:var (title, _) = document.metadata;

在這個情況下,萬用字元不會繫結任何變數。並捨棄第二個欄位。

在下一節中,您將在顯示 Block 物件後,進一步瞭解更多切換功能。

顯示文件內容

DocumentScreen 小工具的 build 方法中呼叫 getBlocks(),藉此建立包含 Block 物件清單的本機變數。

  1. DocumentationScreen 中現有的 build 方法替換為這個版本:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final blocks = document.getBlocks();                           // Add this line

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

BlockWidget(block: blocks[index]) 行會為 getBlocks() 方法傳回的區塊清單中每個項目建構 BlockWidget 小工具。

  1. 執行應用程式,畫面上就會顯示方塊:

應用程式螢幕截圖,顯示 JSON 資料「blocks」部分的內容。

10. 使用切換運算式

模式可為 switchcase 新增許多功能。為了讓這些元素可在更多地方使用,Dart 提供了切換運算式。一系列的例項可以直接為變數指派或傳回陳述式提供值。

將 switch 陳述式轉換為 switch 運算式

Dart 分析器會提供輔助功能,協助您修改程式碼。

  1. 將游標移至上一個部分的 switch 陳述式。
  2. 按一下燈泡圖示,即可查看可用的輔助功能。
  3. 選取「轉換為切換運算式」輔助功能。

螢幕截圖:VS Code 中提供的「轉換為切換運算式」輔助功能。

這段程式碼的新版本如下所示:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

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

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;                                          // Modify from here
    textStyle = switch (block.type) {
      'h1' => Theme.of(context).textTheme.displayMedium,
      'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
      _ => Theme.of(context).textTheme.bodySmall
    };                                                             // to here.

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

switch 運算式與 switch 陳述式類似,但會移除 case 關鍵字,並使用 => 將模式與情況主體分開。與 switch 陳述式不同,switch 運算式會傳回值,且可用於任何可使用運算式的地方。

11. 使用物件模式

Dart 是一種以物件為導向的語言,因此模式會套用至所有物件。在這個步驟中,您會啟用物件模式並解構物件屬性,以強化 UI 的日期算繪邏輯。

從物件模式中擷取屬性

在本節中,您將改善使用模式顯示上次修改日期的方式。

  • formatDate 方法新增至 main.dart

lib/main.dart

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

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

這個方法會傳回切換運算式,根據 difference 值切換 Duration 物件。代表 JSON 資料中 todaymodified 值之間的時間間隔。

切換運算式的每個情況都會使用物件模式,該模式會在物件的屬性 inDaysisNegative 上呼叫 getter,以便進行比對。這個語法看起來像是建構 Duration 物件,但實際上是存取 difference 物件上的欄位。

前三種情況會使用常數子模式 01-1 比對物件屬性 inDays,並傳回對應的字串。

後兩種情況會處理超過今天、昨天和明天的時間長度:

  • 如果 isNegative 屬性符合 布林常數模式true,表示修改日期是在過去,因此會顯示「幾天前」
  • 如果該情況未偵測到差異,則持續時間必須是正數天數 (無需透過 isNegative: false 明確驗證),因此修改日期會在未來,並顯示「從現在起算的天數」

新增週的格式邏輯

  • 在格式設定函式中新增兩個案例,以便識別超過 7 天的時間長度,讓 UI 顯示為「週」

lib/main.dart

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

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

以下程式碼會引入防護子句

  • 防護子句會在情況模式後使用 when 關鍵字。
  • 可用於 if 陳述式、switch 陳述式和 switch 運算式。
  • 只有在模式「比對成功後」,才會為模式新增限制條件。
  • 如果防護子句的值為 false,整個模式會遭到「駁回」,執行作業會繼續進行下一個案例。

將新格式的日期新增至 UI

  1. 最後,請更新 DocumentScreen 中的 build 方法,以便使用 formatDate 函式:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

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

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final formattedModifiedDate = formatDate(modified);            // Add this line
    final blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'),           // Modify this line
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }
}
  1. 使用即時重新載入功能查看應用程式中的變更:

應用程式螢幕截圖,使用 formatDate() 函式顯示「Last modified: 2 weeks ago」字串。

12. 封存類別,以便進行完整切換

請注意,您並未在最後一個切換的結尾使用萬用字元或預設情況。雖然建議您一律為可能會略過的值加入例外狀況,但在像這樣簡單的例子中,您可以不必這麼做,因為您知道定義的例外狀況已考量 inDays 可能會採用的所有可能值

當 switch 中的每個 case 都已處理時,就稱為完整 switch。舉例來說,如果 bool 類型包含 truefalse 的情況,開啟此類型就會耗盡。當每個列舉值都有個案時,開啟 enum 類型會耗盡所有情況,因為列舉代表固定數量的常數值

Dart 3 使用新的類別修飾符 sealed,將完整性檢查擴充至物件和類別階層。將 Block 類別重構為密封的父類別。

建立子類別

  • data.dart 中,建立三個新的類別 (HeaderBlockParagraphBlockCheckboxBlock),擴充 Block

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 重構為切換運算式,傳回與 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 關鍵字是類別修飾符,表示您只能在同一個程式庫中擴充或實作此類別。由於分析器知道這個類別的子類型,因此如果切換未涵蓋其中一個子類型,且未涵蓋所有子類型,就會回報錯誤。

使用切換運算式來顯示小工具

  1. 使用切換運算式更新 main.dart 中的 BlockWidget 類別,以便在每個情況下使用物件模式:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

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

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

在第一個版本的 BlockWidget 中,您開啟了 Block 物件的欄位,以便傳回 TextStyle。您現在可以切換 Block 物件本身的例項,並比對代表其子類別的物件模式,在此過程中擷取物件的屬性。

由於您已將 Block 設為封閉類別,Dart 分析器可以檢查每個子類別是否在切換運算式中處理。

另請注意,在此使用切換運算式可讓您直接將結果傳遞至 child 元素,而不需要先前所需的個別 return 陳述式。

  1. 使用熱重新整理功能,查看第一次算繪的核取方塊 JSON 資料:

應用程式的螢幕截圖,顯示「Learn Dart 3」核取方塊

13. 恭喜

您已成功嘗試使用模式、記錄、強化的 switch 和 case,以及封閉類別。您提供了很多資訊,但只略微提及這些功能。如要進一步瞭解模式,請參閱功能規範

不同的模式類型、可出現的不同情境,以及子模式的潛在巢狀結構,讓行為的可能性變得無窮無盡。但很容易看見。

您可以想像在 Flutter 中使用模式顯示內容的各種方式。您可以使用模式,安全地擷取資料,以便透過幾行程式碼建構 UI。

後續步驟

  • 請參閱 Dart 說明文件的語言部分,瞭解模式、記錄、強化的 switch 和 case,以及類別修飾符的說明文件。

參考文件

請參閱 flutter/codelabs 存放區中的完整範例程式碼,瞭解每個步驟。

如要深入瞭解各項新功能的規格,請參閱原始設計文件: