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

1. 簡介

Dart 3 為語言提供模式,這種語言是新的主要文法類別。除了這種新方法來編寫 Dart 程式碼,還有幾項語言強化功能,包括:

  • 記錄,用於合併不同類型的資料。
  • 用於控制存取權的類別修飾符
  • 新增切換運算式if-case 陳述式

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

本程式碼研究室假設您對 Flutter 和 Dart 有一定程度的瞭解。如果你覺得有點信任感,不妨利用下列資源複習基本知識:

建構項目

本程式碼研究室會建立一個應用程式,以在 Flutter 中顯示 JSON 文件。應用程式會模擬來自外部來源的 JSON。JSON 檔案包含文件資料,例如修改日期、標題、標頭和段落。您可以編寫程式碼,將資料妥善封裝至記錄中,以便在任何 Flutter 小工具中傳輸及解壓縮。

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

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

課程內容

  • 如何建立可以儲存不同類型值的記錄。
  • 如何使用記錄從函式傳回多個值。
  • 如何使用模式比對、驗證記錄和其他物件中的資料,並加以銷毀。
  • 如何將模式比對值繫結至新的或現有變數。
  • 如何使用新的 Switch 陳述式功能、切換運算式和 if-case 陳述式。
  • 如何利用完整性檢查功能,確保每個案件都可在 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」(執行及偵錯),在主體機器上執行應用程式:

「執行並偵錯」的圖片位於「執行及偵錯」的按鈕部分

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

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

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

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

5. 建立及傳回記錄

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

建立及傳回記錄

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

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() 方法會使用 String 做為第一個引數。
  • 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)
  • 該資料不是 null,而是在之前的檢查中明確確認。

使用對應模式讀取 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 中導入),如果案例已導入。只有在案例模式與 _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"
    },
    // ...
  ]
}

建立可儲存資料的類別

  • 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': final type, 'text': final text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

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

請注意,即使其中一個鍵 (checked),json 仍會與對應模式相符。地圖模式會忽略地圖物件中未明確計入模式的任何項目。

傳回 Block 物件清單

  • 接下來,請將新函式 getBlocks() 新增至 Document 類別。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 資料。不過,如果情況只是控管運用模式的流程結構其中一項強化功能,現在,你可以運用有關可反駁的模式知識來切換陳述式。

使用 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 方法中的切換陳述式會切換 block 物件的 type 欄位。

  1. 第一個 case 陳述式使用常數字串模式。如果 block.type 等於常數值 h1,則模式會配對。
  2. 第二個案例陳述式使用邏輯或模式,搭配兩個常數字串模式做為子模式。模式會比對 block.typepcheckbox 任一子模式相符。
  1. 最終的情況是「萬用字元模式」 _。切換保護殼的萬用字元與所有其他項目相同。這類子句的行為與 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 資料的區段

10. 使用切換運算式

模式會為 switchcase 新增許多功能。為了在更多地方使用,Dart 提供了切換運算式。一系列案例可以提供值給變數指派或回傳敘述。

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

Dart 分析工具提供「輔助」功能,方便您變更程式碼。

  1. 將遊標移至上一節的切換陳述式。
  2. 只要按一下燈泡,即可查看可用的協助。
  3. 選取「Convert to Switchexpression」輔助輔助。

「Convert to 切換運算式」的螢幕截圖所提供的輔助。

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

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 陳述式類似,但會排除 case 關鍵字,並使用 => 來區分模式與案件主體。與切換陳述式不同,切換運算式會傳回一個值,可在可使用運算式的任何位置使用。

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 物件) 的切換運算式。代表 today 和 JSON 資料中 modified 值之間的時間範圍。

每個切換運算式的情況都使用一種物件模式,該模式會比對物件的屬性 inDaysisNegative 來比對。該語法看起來可能是建構 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 case、切換陳述式和切換運算式。
  • 這些條件只會在相符後的模式中新增條件。
  • 如果守衛子句評估為 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. 熱門重新載入頁面,查看應用程式的變更:

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

12. 將類別密封,以利完整切換

請注意,您在上一個開關結束時,並未使用萬用字元或預設大小寫。雖然建議您一律將可能流入的值全部納入案例,這裡也適用一個簡單的範例,因為您知道自己定義的情況會考量 inDays所有可能值

每當切換轉換的過程都發生時,就算是一次全面轉換。舉例來說,如果 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 元素,而不是先前需要的個別回傳敘述。

  1. 熱重新載入畫面,查看第一次轉譯的 JSON 資料核取方塊:

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

13. 恭喜

您已成功實驗不同的模式、記錄、強化開關和保護殼,以及密封類別。您之前提到了很多資訊,但還完全忽略了這些功能的面貌。如要進一步瞭解模式,請參閱功能規格

不同的模式類型、不同的內容顯示情境,以及子模式的巢狀結構可能使行為看似「無限」。但顯而易見。

讓您想像以各種方式在 Flutter 中顯示內容,都可以運用圖案。使用模式可以安全地擷取資料,而且只需加入幾行程式碼即可建構 UI。

後續步驟

  • 請參閱 Dart 說明文件的「語言」部分,瞭解模式、記錄、強化版切換和案例以及類別修飾符的說明文件。

參考文件

如需完整的程式碼範例,請參閱 flutter/codelabs 存放區

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