1. はじめに
Dart 3 では、その言語に主要な新しい文法カテゴリである「パターン」を導入しています。この新たな Dart コードの記述方法に加えて、さまざまな型のデータを束縛するための「レコード」、アクセスを制御するための「クラス修飾子」、新たな switch 式と if-case 文などの言語拡張もあります。
こういった機能により、Dart コードを記述する際の選択肢が広がります。この Codelab では、これを使用してコードをよりコンパクト、効率的、柔軟にする方法を学びます。
この Codelab は、Flutter と Dart の知識を前提としていますが、必須ではありません。始める前に、以下の資料で基本を復習することをおすすめします。
作成するアプリの概要
この Codelab では、JSON ドキュメントを表示するアプリケーションを Flutter で作成します。このアプリケーションでは、外部から JSON が供給されるのをシミュレートします。この JSON には、更新日時、タイトル、ヘッダー、パラグラフなどのドキュメント データが含まれます。データを転送して Flutter ウィジェットが必要とする場所でパッキングを解除できるように、適切にレコードにパッキングするコードを記述します。
そして、値がパターンに一致したときに適切なウィジェットを構築するパターンを使用します。また、パターンを使用し、データを分離してローカル変数に納める方法も確認します。
学習内容
- さまざまな型の複数の値を格納するレコードの作成方法
- レコードを使用して、関数から複数の値を返す方法
- パターンを使用して、レコードやその他のオブジェクトのデータの照合、検証、分離を行う方法
- パターンに一致した値を新規または既存の変数に束縛する方法
- 新しい switch 機能、switch 式、if-case 文の使用方法
- 網羅性チェックを活用して switch 文または switch 式ですべての case が処理されていることを確認する方法
2. 環境をセットアップする
- Flutter SDK をインストールします。
- Visual Studio Code(VS Code)などのエディタをセットアップします。
- 少なくとも 1 つのターゲット プラットフォーム(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 プロジェクトを作成する
flutter create
コマンドを使用してpatterns_codelab
という新規のプロジェクトを作成します。--empty
フラグを指定するとlib/main.dart
ファイルに標準のカウンタアプリが作成されなくなりますが、どちらにせよ削除する必要があります。
flutter create --empty patterns_codelab
- 次に、VS Code を使用して
patterns_codelab
ディレクトリを開きます。
code patterns_codelab
最小 SDK バージョンを設定する
- Dart 3 以降に依存するようにプロジェクトの SDK バージョン制約を設定します。
pubspec.yaml
environment:
sdk: ^3.0.0
4. プロジェクトをセットアップする
このステップでは、2 つの 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 リクエストなど、外部からデータを受信するプログラムを想像してください。この Codelab では、送られてくる JSON データを documentJson
変数に入った複数行の文字列で模倣して、そのような現実に近いユースケースを簡素化しています。
JSON データは Document
クラスで定義され、この Codelab の後半で、解析済みの 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,
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'),
),
],
),
);
}
}
次の 2 つのウィジェットをアプリに追加します。
DocumentApp
は、マテリアル デザインの最新版をセットアップして、UI のテーマ設定をします。DocumentScreen
は、Scaffold
を使用してページを視覚的にレイアウトします。
- すべてが順調に進むように、[Run and Debug] をクリックして、ホストマシンでアプリを実行します。
- デフォルトでは、Flutter は利用可能なターゲット プラットフォームのいずれかを選びます。ターゲット プラットフォームを変更するには、ステータスバーの現在のプラットフォームを選択します。
DocumentScreen
ウィジェットで定義された title
要素と body
要素を含んだ空のフレームが表示されます。
5. レコードを作成して返す
このステップでは、レコードを使用して、関数呼び出しから複数の値を返します。そして、それらの値にアクセスして UI に反映させるために、DocumentScreen
ウィジェット内でその関数を呼び出します。
レコードを作成して返す
data.dart
で、レコードを返すgetMetadata
という新規の関数を追加します。
lib/data.dart
(String, {DateTime modified}) getMetadata() {
var title = "My Document";
var now = DateTime.now();
return (title, modified: now);
}
この関数の戻り値の型は、String
型のフィールドと DateTime
型のフィールドの 2 つのフィールドがあるレコードです。
return 文では、2 つの値を括弧で囲むことで((title, modified: now)
)、新規のレコードを構築しています。
1 つ目のフィールドは無名の位置フィールドで、2 つ目は modified
という名前のフィールドです。
レコードのフィールドにアクセスする
DocumentScreen
ウィジェットの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
に代入されます。レコードは、1 回の関数呼び出しで複数の値を返して変数に代入するための軽量で簡便な手段です。
そのレコードを構成する個別のフィールドにアクセスするには、レコードの組み込みのゲッター構文を使用できます。
- 位置フィールド(
title
のように名前のないフィールド)を取得するには、レコードにゲッター$<num>
を使用します。これは無名フィールドのみを返します。 modified
のような名前付きフィールドに位置ゲッターはないので、metadataRecord.modified
のようにして名前を直接使用します。
位置フィールドのゲッターの名前は、$1
から始まり、名前付きフィールドはスキップされます。次に例を示します。
var record = (named: ‘v', ‘y', named2: ‘x', ‘z');
print(record.$1); // prints y
print(record.$2) // prints z
- ホットリロードを行って、アプリに JSON 値が表示されるのを確認してください。VS Code Dart プラグインは、ファイルが保存されるたびにホットリロードを行います。
各フィールドが実際にその型を維持していることがわかります。
Text()
メソッドは、1 つ目の引数として String を取ります。modified
フィールドは DateTime ですが、文字列補間を使用してString
に変換されます。
さまざまな型のデータを型安全性を確保したまま返すには、クラスを定義するという方法もありますが、冗長になってしまいます。
6. パターンで照合と分離を行う
レコードを使用すると、異なる型のデータを効率的にまとめて、簡単に配ることができます。ここでは、パターンを使用してコードを改良します。
Blueprint と同じように、パターンは、1 つ以上の値を取る構造体です。パターンは、一致するかどうかを判断するために、実際値と比較します。
パターンには、それが一致したときにデータを抽出して一致した値を分離するものがあります。分離することにより、オブジェクトから値を取り出して、ローカル変数に代入したり、さらに照合を行ったりできます。
レコードから分離してローカル変数に代入する
DocumentScreen
のbuild
メソッドをリファクタリングして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()
から返されたレコードのフィールドと照合する変数パターンが 2 つ含まれています。
- 結果は 2 つのフィールド(その一方には
modified
という名前が付いている)があるレコードであるため、この式はサブパターンと照合されます。 - どちらも一致するため、変数宣言パターンは式を分離し、その各値にアクセスして、同じ型で同じ名前の新しいローカル変数である
String title
とDateTime modified
に束縛します。
変数パターン :modified
は、modified: modified
の省略形です。新しいローカル変数を別の名前にする場合は modified: localModified
とします。
- ホットリロードすると前のステップと同じ結果が表示されます。挙動は完全に同じであり、コードを簡潔にしただけです。
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');
}
このコードでは、パターンを使用せずにデータが正しく構造化されていることを検証しています。以降のステップでは、パターン照合を使用して、同じ検証を少ないコードで実行します。他の処理の前に、以下の 3 つのチェックを行います。
- 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 本体が実行されます。この照合により、入力された JSON を検証するために記述した最初のバージョンの getMetadata()
と同じチェックを行うことができています。このコードでは以下のことを確認しています。
_json
は Map 型である。_json
にキーmetadata
が含まれている。_json
は null でない。_json['metadata']
も Map 型である。_json['metadata']
にキーtitle
とキーmodified
が含まれている。title
とlocalModified
は文字列であり、null ではない。
値が一致しなかった場合、パターンは反駁して(実行の継続を拒否して)、else
節に進みます。一致した場合は、パターンはマップから title
と modified
の値を分離して新規のローカル変数に束縛します。
パターンの全機能については、機能仕様のパターンのセクションの表をご覧ください。
8. アプリをより多くのパターンに対応させる
ここまでは、JSON データの metadata
の部分を扱いました。このステップでは、blocks
リスト内のデータを処理して、それをアプリでレンダリングするために、ビジネス ロジックを少し洗練させます。
{
"metadata": {
// ...
},
"blocks": [
{
"type": "h1",
"text": "Chapter 1"
},
// ...
]
}
データを格納するクラスを作成する
Block
という新規のクラスをdata.dart
に追加します。このクラスは、JSON データ内のブロックの 1 つに対して、データの読み取りと格納を行うために使用されます。
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 を使用します。
注目すべきは、キーの 1 つである checked
がパターンで考慮されていないのに、json
がマップパターンに一致していることです。マップパターンでは、パターンで明示的に考慮されていない場合、マップ オブジェクト内のそのようなエントリが無視されます。
Block オブジェクトのリストを返す
- 次に、
getBlocks()
という新規の関数をDocument
クラスに追加します。getBlocks()
は、JSON を解析してBlock
クラスのインスタンスを作成し、UI でレンダリングするブロックのリストを返します。
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
オブジェクトのリストを返します。このリストは、後で UI の構築に使用します。馴染み深い if-case 文では、blocks
メタデータの値を検証し、blocksJson
という名前の新規の List
にキャストします(パターンを使用しない場合は toList()
メソッドでキャストする必要があります)。
リストのリテラルには、新規リストを Block
オブジェクトで埋めるためのコレクション for が含まれています。
このセクションでは、この Codelab でまだ試していないパターン関連の機能を紹介していません。次のステップでは、UI でリストアイテムをレンダリングするための準備を行います。
9. ドキュメントの表示にパターンを使用する
これで、if-case 文と反駁可能パターンを使用して、JSON データの分離と再合成を行うことができました。ただし、if-case は、パターンが付随する制御フロー構造の強化のうちの一つにすぎません。次は、反駁可能パターンの知識を switch 文に応用します。
switch 文にパターンを使用してレンダリングの対象を制御する
main.dart
で、type
フィールドに基づいて各ブロックのスタイル設定を決定する新しいウィジェット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) {
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
と等しい場合にパターンが一致します。 - 2 番目の case 文では、サブパターンとして 2 つの定数文字列パターンを使用する論理和パターンを使用しています。
block.type
がサブパターンのp
かcheckbox
に一致する場合にパターンが一致します。
- 最後の case はワイルドカード パターン
_
です。switch case のワイルドカードは、その他のすべてに一致します。その挙動はdefault
節(少し冗長ですが switch 文では引き続き使用可能です)と同じです。
ワイルドカード パターンは、パターンが使用可能な場所であれば使用できます。例: var (title, _) = document.getMetadata();
この場合、ワイルドカードは変数を束縛しません。2 番目のフィールドは破棄されます。
次のセクションでは、Block
オブジェクトを表示した後で、switch のその他の機能について学びます。
ドキュメントの内容を表示する
DocumentScreen
ウィジェットの build
メソッドで getBlocks()
を呼び出して、Block
オブジェクトのリストを含んだローカル変数を作成します。
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
ウィジェットを作成します。
- アプリケーションを実行すると、画面にブロックが表示されます。
10. switch 式を使用する
パターンにより多くの機能が switch
と case
に追加されます。それが使用可能な場所を増やすために、Dart には switch 式が導入されています。一連の case により、変数割り当てや return 文に値を直接指定できます。
switch 文を switch 式に書き換える
Dart アナライザには、コードの変更に役立つ支援機能があります。
- 前のセクションの switch 文にカーソルを移動します。
- 利用可能な支援機能を表示するために電球をクリックします。
- [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 はオブジェクト指向言語なので、パターンはすべてのオブジェクトに適用されます。このステップでは、UI のロジックをレンダリングする日付を強化するために、オブジェクト パターンに基づいて切り替え、オブジェクトのプロパティを分離します。
オブジェクト パターンからプロパティを抽出する
このセクションでは、パターンを使用して、最終更新日の表示を改善します。
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',
};
}
このメソッドは、Duration
オブジェクトである値 difference
で場合分けする switch 式を返しています。このオブジェクトは、today
と、JSON データの modified
値との時間間隔を表します。
switch 式の各 case では、このオブジェクトの inDays
プロパティと isNegative
プロパティのゲッターを呼び出して、照合するオブジェクト パターンを使用しています。構文は Duration オブジェクトの構築に似ていますが、実際は、difference
のフィールドにアクセスしています。
最初の 3 つの case では、オブジェクト プロパティ inDays
と照合して、対応する文字列を返すために、定数サブバターン 0
、1
、-1
を使用しています。
最後の 2 つの case では today、yesterday、tomorrow 以外の期間を扱っています。
isNegative
プロパティがブール値定数パターンtrue
と一致した場合、つまり更新日が過去だった場合、days ago と表示します。- その case でも difference が処理されなかった場合、duration は必ず正の日数なので(明示的な
isNegative: false
の確認は不要)、更新日は未来であり、days from now と表示します。
週数の書式設定ロジックを追加する
- 期間が 7 日を超えていることを確認して、UI に weeks と表示できるようにするために、書式設定関数に 2 つの case を追加します。
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',
};
}
このコードではガード句を導入しています。
- ガード句では、case パターンの後に
when
キーワードを使用します。 - if-case, switch 文、switch 式で使用できます。
- 一致した後にのみ、条件をパターンに追加します。
- ガード句が false と評価されると、パターン全体が反駁され、実行は次の case に進みます。
新しい書式設定がされた日付を UI に追加する
- 最後に、
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]),
),
),
],
),
);
}
- ホットリロードしてアプリの変更点を確認します。
12. 網羅的な場合分けのためにクラスをシールする
直前の switch の最後では、ワイルドカードもデフォルト ケースも使用していませんでした。最後にたどり着く値の case を常に含めることは良い習慣ですが、このような簡単な例では、定義した case で inDays
が取りうる値がすべて考慮されていることがわかるため、問題ありません。
switch 内のすべての case が処理されている場合、これを網羅的 switch と呼びます。たとえば、bool
型に関する switch が網羅的なのは、true
と false
の case がある場合です。enum
型に関する switch は、enum は固定数の定数値を表現しているため、各 enum 値の case がある場合に網羅的となります。
Dart 3 では、新たなクラス修飾子 sealed
で、網羅性チェックをオブジェクトとクラスの階層に拡張しています。Block
をリファクタリングして、シール スーパークラスにします。
サブクラスを作成する
data.dart
で、Block
を拡張する 3 つのクラスHeaderBlock
、ParagraphBlock
、CheckboxBlock
を作成します。
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
に対応するサブクラスを返す switch 式にリファクタリングします。
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 式を使用する
main.dart
の BlockWidget クラスを、各 case にオブジェクト パターンを使用する switch 式で更新します。
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
の最初のバージョンでは、TextStyle
を返すために Block
のフィールドで場合分けしていました。今回は、Block
オブジェクト自身のインスタンスで場合分けし、そのサブクラスを表すオブジェクト パターンと照合して、その過程でオブジェクトのプロパティを抽出します。
Block
をシールクラスにしたので、Dart アナライザでは、switch 式で各サブクラスが処理されていることを確認できます。
また、ここで switch 式を使用することで、child
要素に直接結果を渡すことが可能になり、以前のような個別の return 文が不要になっています。
- ホットリロードして、チェックボックスの JSON データが初めてレンダリングされるのを確認します。
13. 完了
パターン、レコード、拡張された switch と case、シールクラスを試すことができました。多くのことを学びましたが、これらの機能のほんの一部をかじったにすぎません。パターンの詳細については、機能仕様をご覧ください。
パターンの種類の違い、それらが現れる文脈の違い、サブパターンがネストする可能性から、挙動の可能性は無限にあるように見えるでしょう。しかし、心配する必要はありません。
パターンを使って Flutter でコンテンツを表示する方法は、すべて想像できます。パターンを使用すれば、わずかなコードで、UI を構築するためのデータの抽出を安全に行うことができます。
次のステップ
- Dart のドキュメントの言語のセクションで、パターン、レコード、拡張された switch と case、クラス修飾子のドキュメントをご覧ください。
リファレンス ドキュメント
完全なサンプルについては、リポジトリをご覧ください。
各新機能の詳細な仕様については、元の設計ドキュメントをご覧ください。