程式碼研究室簡介
1. 簡介
Dart 3 為語言引進模式,這是一項全新的主要文法類別。除了這項 Dart 程式碼編寫的新方法之外,還有其他幾項語言增強功能,包括:
- 記錄:用於將不同類型的資料彙整在一起
- 類別修飾符 (用於控管存取權),以及
- 新的 switch 運算式和 if-case 陳述式。
這些功能可讓您在編寫 Dart 程式碼時有更多選擇。在本程式碼研究室中,您將瞭解如何使用這些功能,讓程式碼更精簡、流暢且具彈性。
本程式碼研究室假設您對 Flutter 和 Dart 有一定的瞭解。如果您覺得自己有點生疏,不妨參考下列資源溫習基礎知識:
建構項目
本程式碼研究室會建立一個應用程式,在 Flutter 中顯示 JSON 文件。應用程式會模擬來自外部來源的 JSON。JSON 檔案包含文件資料,例如修改日期、標題、標題列和段落。您可以編寫程式碼,將資料整齊地打包至記錄,以便在 Flutter 小工具需要時傳輸及解開。
然後,當值符合該模式時,您可以使用模式建構適當的小工具。您也會瞭解如何使用模式將資料解構為本機變數。
課程內容
- 如何建立可儲存不同類型值的記錄。
- 如何使用記錄從函式傳回多個值。
- 如何使用模式比對、驗證及解構記錄和其他物件的資料。
- 如何將符合模式的值繫結至新變數或現有變數。
- 如何使用新的 switch 陳述式功能、switch 運算式和 if-case 陳述式。
- 如何利用完整性檢查功能,確保在 switch 陳述式或 switch 運算式中處理每個情況。
2. 設定環境
- 安裝 Flutter SDK。
- 設定編輯器,例如 Visual Studio Code (VS Code)。
- 針對至少一個目標平台 (iOS、Android、電腦或網頁瀏覽器) 完成平台設定步驟。
3. 建立專案
在深入探討模式、記錄和其他新功能之前,請先花點時間建立一個簡單的 Flutter 專案,並在其中編寫所有程式碼。
建立 Flutter 專案
- 使用
flutter create
指令建立名為patterns_codelab
的新專案。--empty
旗標會防止在lib/main.dart
檔案中建立標準計數器應用程式,因此您還是必須將其移除。
flutter create --empty patterns_codelab
- 接著,使用 VS Code 開啟
patterns_codelab
目錄。
code patterns_codelab
設定最低 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
檔案。
- 如要為應用程式建立起始點,請將
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
小工具提供網頁的視覺版面配置。
- 如要確保一切運作順暢,請按一下「Run and Debug」,在主機電腦上執行應用程式:
- 根據預設,Flutter 會選擇可用的目標平台。如要變更目標平台,請在狀態列中選取目前的平台:
您應該會看到空白的畫面,其中包含 DocumentScreen
小工具中定義的 title
和 body
元素:
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
。
存取記錄欄位
- 在
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
- 使用熱重新載入功能,查看應用程式中顯示的 JSON 值。每次儲存檔案時,VS Code Dart 外掛程式都會熱重新載入。
您可以看到,每個欄位確實都維持了原有的類型。
Text()
方法會使用字串做為第一個引數。modified
欄位是 DateTime,會使用字串內插轉換為String
。
另一種用來傳回不同類型資料的類型安全方法,是定義較冗長的類別。
6. 使用模式進行配對和解構
記錄可有效地收集不同類型的資料,並輕鬆傳遞資料。接下來,請使用模式改善程式碼。
模式代表一或多個值可採用的結構,類似於藍圖。模式會與實際值進行比較,判斷兩者是否「相符」。
部分模式在比對時,會透過提取資料來解構相符的值。結構重組可讓您將物件的值解包,並將這些值指派給本機變數,或進一步進行比對。
將記錄解構為本機變數
- 重構
DocumentScreen
的build
方法,以便呼叫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 title
和DateTime modified
。
如果欄位名稱與填入該欄位的變數相同,則可使用簡寫。請按照下列方式重構 DocumentScreen
的 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; // Modify
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
Center(
child: Text(
'Last modified $modified',
),
),
],
),
);
}
}
變數模式 :modified
的語法是 modified: modified
的簡寫形式。如果您想要使用其他名稱的新本機變數,可以改為寫入 modified: localModified
。
- 熱重新載入,即可查看與上一個步驟相同的結果。行為完全相同,只是讓程式碼更精簡。
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']
包含title
和modified
鍵。title
和localModified
是字串,且非空值。
如果值不相符,模式會駁回 (拒絕繼續執行),並繼續執行 else
子句。如果比對成功,模式會從對應項目中解構 title
和 modified
的值,並將這些值繫結至新的本機變數。
如需完整的模式清單,請參閱功能規格中的「模式」部分表格。
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
欄位。
- 第一個 case 陳述式使用常數字串模式。如果
block.type
等於常數值h1
,模式就會相符。 - 第二個 case 陳述式使用邏輯或模式,其中包含兩個常數字串模式做為子模式。如果
block.type
與任一子模式p
或checkbox
相符,則模式會相符。
- 最後一個案例是 萬用字元模式
_
。萬用字元會與 switch 陳述式中的所有其他項目相符。它們的運作方式與default
子句相同,後者仍可用於 switch 陳述式 (只是會比較冗長)。
萬用字元模式可用於允許模式的任何位置,例如變數宣告模式:var (title, _) = document.metadata;
在這個情況下,萬用字元不會繫結任何變數。並捨棄第二個欄位。
在下一節中,您將在顯示 Block
物件後,進一步瞭解更多切換功能。
顯示文件內容
在 DocumentScreen
小工具的 build
方法中呼叫 getBlocks()
,藉此建立包含 Block
物件清單的本機變數。
- 將
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
小工具。
- 執行應用程式,畫面上就會顯示方塊:
10. 使用切換運算式
模式可為 switch
和 case
新增許多功能。為了讓這些元素可在更多地方使用,Dart 提供了切換運算式。一系列的例項可以直接為變數指派或傳回陳述式提供值。
將 switch 陳述式轉換為 switch 運算式
Dart 分析器會提供輔助功能,協助您修改程式碼。
- 將游標移至上一個部分的 switch 陳述式。
- 按一下燈泡圖示,即可查看可用的輔助功能。
- 選取「轉換為切換運算式」輔助功能。
這段程式碼的新版本如下所示:
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 資料中 today
和 modified
值之間的時間間隔。
切換運算式的每個情況都會使用物件模式,該模式會在物件的屬性 inDays
和 isNegative
上呼叫 getter,以便進行比對。這個語法看起來像是建構 Duration 物件,但實際上是存取 difference
物件上的欄位。
前三種情況會使用常數子模式 0
、1
和 -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
- 最後,請更新
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]);
},
),
),
],
),
);
}
}
- 使用即時重新載入功能查看應用程式中的變更:
12. 封存類別,以便進行完整切換
請注意,您並未在最後一個切換的結尾使用萬用字元或預設情況。雖然建議您一律為可能會略過的值加入例外狀況,但在像這樣簡單的例子中,您可以不必這麼做,因為您知道定義的例外狀況已考量 inDays
可能會採用的所有可能值 。
當 switch 中的每個 case 都已處理時,就稱為完整 switch。舉例來說,如果 bool
類型包含 true
和 false
的情況,開啟此類型就會耗盡。當每個列舉值都有個案時,開啟 enum
類型會耗盡所有情況,因為列舉代表固定數量的常數值。
Dart 3 使用新的類別修飾符 sealed
,將完整性檢查擴充至物件和類別階層。將 Block
類別重構為密封的父類別。
建立子類別
- 在
data.dart
中,建立三個新的類別 (HeaderBlock
、ParagraphBlock
和CheckboxBlock
),擴充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
關鍵字是類別修飾符,表示您只能在同一個程式庫中擴充或實作此類別。由於分析器知道這個類別的子類型,因此如果切換未涵蓋其中一個子類型,且未涵蓋所有子類型,就會回報錯誤。
使用切換運算式來顯示小工具
- 使用切換運算式更新
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 陳述式。
- 使用熱重新整理功能,查看第一次算繪的核取方塊 JSON 資料:
13. 恭喜
您已成功嘗試使用模式、記錄、強化的 switch 和 case,以及封閉類別。您提供了很多資訊,但只略微提及這些功能。如要進一步瞭解模式,請參閱功能規範。
不同的模式類型、可出現的不同情境,以及子模式的潛在巢狀結構,讓行為的可能性變得無窮無盡。但很容易看見。
您可以想像在 Flutter 中使用模式顯示內容的各種方式。您可以使用模式,安全地擷取資料,以便透過幾行程式碼建構 UI。
後續步驟
- 請參閱 Dart 說明文件的語言部分,瞭解模式、記錄、強化的 switch 和 case,以及類別修飾符的說明文件。
參考文件
請參閱 flutter/codelabs
存放區中的完整範例程式碼,瞭解每個步驟。
如要深入瞭解各項新功能的規格,請參閱原始設計文件: