Dart のパターンとレコードを使ってみる

1. はじめに

Dart 3 では、その言語に主要な新しい文法カテゴリである「パターン」を導入しています。この新たな Dart コードの記述方法に加えて、さまざまな型のデータを束縛するための「レコード」、アクセスを制御するための「クラス修飾子」、新たな switch 式と if-case 文などの言語拡張もあります。

こういった機能により、Dart コードを記述する際の選択肢が広がります。この Codelab では、これを使用してコードをよりコンパクト、効率的、柔軟にする方法を学びます。

この Codelab は、Flutter と Dart の知識を前提としていますが、必須ではありません。始める前に、以下の資料で基本を復習することをおすすめします。

作成するアプリの概要

この Codelab では、JSON ドキュメントを表示するアプリケーションを Flutter で作成します。このアプリケーションでは、外部から JSON が供給されるのをシミュレートします。この JSON には、更新日時、タイトル、ヘッダー、パラグラフなどのドキュメント データが含まれます。データを転送して Flutter ウィジェットが必要とする場所でパッキングを解除できるように、適切にレコードにパッキングするコードを記述します。

そして、値がパターンに一致したときに適切なウィジェットを構築するパターンを使用します。また、パターンを使用し、データを分離してローカル変数に納める方法も確認します。

この Codelab で作成するアプリケーションの最終版、タイトル付きのドキュメント、最終更新日、ヘッダー、パラグラフ。

学習内容

  • さまざまな型の複数の値を格納するレコードの作成方法
  • レコードを使用して、関数から複数の値を返す方法
  • パターンを使用して、レコードやその他のオブジェクトのデータの照合、検証、分離を行う方法
  • パターンに一致した値を新規または既存の変数に束縛する方法
  • 新しい switch 機能、switch 式、if-case 文の使用方法
  • 網羅性チェックを活用して switch 文または switch 式ですべての case が処理されていることを確認する方法

2. 環境をセットアップする

  1. Flutter SDK をインストールします。
  2. Visual Studio Code(VS Code)などのエディタをセットアップします。
  3. 少なくとも 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 プロジェクトを作成する

  1. flutter create コマンドを使用して patterns_codelab という新規のプロジェクトを作成します。--empty フラグを指定すると lib/main.dart ファイルに標準のカウンタアプリが作成されなくなりますが、どちらにせよ削除する必要があります。
flutter create --empty patterns_codelab
  1. 次に、VS Code を使用して patterns_codelab ディレクトリを開きます。
code patterns_codelab

「flutter create」コマンドで作成されたプロジェクトを表示している VS Code のスクリーンショット。

最小 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 ファイルが作成されます。

  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'),
         ),
       ],
     ),
   );
 }
}

次の 2 つのウィジェットをアプリに追加します。

  • DocumentApp は、マテリアル デザインの最新版をセットアップして、UI のテーマ設定をします。
  • DocumentScreen は、Scaffold を使用してページを視覚的にレイアウトします。
  1. すべてが順調に進むように、[Run and Debug] をクリックして、ホストマシンでアプリを実行します。

左側のアクティビティ バーの [Run and debug] セクションにある [Run and debug] ボタンの画像。

  1. デフォルトでは、Flutter は利用可能なターゲット プラットフォームのいずれかを選びます。ターゲット プラットフォームを変更するには、ステータスバーの現在のプラットフォームを選択します。

VS Code のターゲット プラットフォーム セレクタのスクリーンショット。

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 という名前のフィールドです。

レコードのフィールドにアクセスする

  1. 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
  1. ホットリロードを行って、アプリに JSON 値が表示されるのを確認してください。VS Code Dart プラグインは、ファイルが保存されるたびにホットリロードを行います。

タイトルと更新日を表示しているアプリのスクリーンショット。

各フィールドが実際にその型を維持していることがわかります。

  • Text() メソッドは、1 つ目の引数として String を取ります。
  • modified フィールドは DateTime ですが、文字列補間を使用して String に変換されます。

さまざまな型のデータを型安全性を確保したまま返すには、クラスを定義するという方法もありますが、冗長になってしまいます。

6. パターンで照合と分離を行う

レコードを使用すると、異なる型のデータを効率的にまとめて、簡単に配ることができます。ここでは、パターンを使用してコードを改良します。

Blueprint と同じように、パターンは、1 つ以上の値を取る構造体です。パターンは、一致するかどうかを判断するために、実際値と比較します。

パターンには、それが一致したときにデータを抽出して一致した値を分離するものがあります。分離することにより、オブジェクトから値を取り出して、ローカル変数に代入したり、さらに照合を行ったりできます。

レコードから分離してローカル変数に代入する

  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 とします。

  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');
}

このコードでは、パターンを使用せずにデータが正しく構造化されていることを検証しています。以降のステップでは、パターン照合を使用して、同じ検証を少ないコードで実行します。他の処理の前に、以下の 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 フィールドで場合分けします。

  1. 最初の case 文では、定数文字列パターンを使用しています。block.type が定数値 h1 と等しい場合にパターンが一致します。
  2. 2 番目の case 文では、サブパターンとして 2 つの定数文字列パターンを使用する論理和パターンを使用しています。block.type がサブパターンの p か checkbox に一致する場合にパターンが一致します。
  1. 最後の case はワイルドカード パターン _ です。switch case のワイルドカードは、その他のすべてに一致します。その挙動は default 節(少し冗長ですが switch 文では引き続き使用可能です)と同じです。

ワイルドカード パターンは、パターンが使用可能な場所であれば使用できます。例: var (title, _) = document.getMetadata();

この場合、ワイルドカードは変数を束縛しません。2 番目のフィールドは破棄されます。

次のセクションでは、Block オブジェクトを表示した後で、switch のその他の機能について学びます。

ドキュメントの内容を表示する

DocumentScreen ウィジェットの 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 ウィジェットを作成します。

  1. アプリケーションを実行すると、画面にブロックが表示されます。

JSON データの「blocks」セクションの内容を表示しているアプリのスクリーンショット。

10. switch 式を使用する

パターンにより多くの機能が switch と case に追加されます。それが使用可能な場所を増やすために、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 はオブジェクト指向言語なので、パターンはすべてのオブジェクトに適用されます。このステップでは、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 に追加する

  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 の最後では、ワイルドカードもデフォルト ケースも使用していませんでした。最後にたどり着く値の 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 式を使用する

  1. 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 文が不要になっています。

  1. ホットリロードして、チェックボックスの JSON データが初めてレンダリングされるのを確認します。

「Learn Dart 3」というチェックボックスを表示するアプリのスクリーンショット。

13. 完了

パターン、レコード、拡張された switch と case、シールクラスを試すことができました。多くのことを学びましたが、これらの機能のほんの一部をかじったにすぎません。パターンの詳細については、機能仕様をご覧ください。

パターンの種類の違い、それらが現れる文脈の違い、サブパターンがネストする可能性から、挙動の可能性は無限にあるように見えるでしょう。しかし、心配する必要はありません。

パターンを使って Flutter でコンテンツを表示する方法は、すべて想像できます。パターンを使用すれば、わずかなコードで、UI を構築するためのデータの抽出を安全に行うことができます。

次のステップ

  • Dart のドキュメントの言語のセクションで、パターン、レコード、拡張された switch と case、クラス修飾子のドキュメントをご覧ください。

リファレンス ドキュメント

完全なサンプルについては、リポジトリをご覧ください。

各新機能の詳細な仕様については、元の設計ドキュメントをご覧ください。