【Flutter＆Dart】enumのフィールド設定方法とswitch式を用いた条件分岐の実例解説

## はじめに

インターネットやAIを駆使して学習している過程で知らず知らずのうちに古い知識で
実装していたりコーディングしていたりすることがあると思います。
そんな中で今回は現場で教えてもらったenumのフィールドの書き方と、switch式を使った書き方を紹介していきたいと思います。

### 記事の対象者

- Flutter&Dartの初学者の方
- enumを便利に使いたい方
- switch式の書き方を実際の使用方法で知りたい方

### 記事を執筆時点での筆者の環境

- macOS 14.3.1
- Xcode 15.2
- Swift 5.9
- iPhone11 pro ⇒ iOS 17.2.1
- Flutter 3.19.0
- Dart 3.3.0
- Pixel 7a ⇒ Android

## 1. サンプルプロジェクト

ユーザーへ何かしらを通知するダイアログを汎用性を高める形で作成しました。

![enumのプロパティとswitch式.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3058346/f731ab38-e8f3-2e55-8e28-aed54ab87ae3.gif)

主な機能は以下のとおりです。

- ダイアログのタイトルの文字列、内容の文字列、ボタンの表示の文字列をカスタマイズできる
- 告知の種類を指定すると上記の３つを決められた内容で指定できる
- 告知の種類を指定しなければ、上記の３つを自由に入れられる


ソースコードはこちらです。

https://github.com/HaruhikoMotokawa/shared_widgets

なおこのサンプルプロジェクトは以前以下の記事で使ったものでもあります。

https://qiita.com/Harx02663971/items/85c1c4c04b868ca2160f

## 2. フィールドを持たせたenumを定義

まず、通知の種類を定義します。今回は３つ用意します。
そして、その３つの種類それぞれにタイトルの文字列、内容の文字列、ボタンの表示の文字列をフィールドで持たせます。
enumにフィールドを持たせることがDart2.17以降でできるようになりました。（2022年５月以降）
しかし、私は古い教材を使って勉強していたのでこのことを知りませんでした。
古い書き方はextensionを使って書くかきたでコード量が増えてしまいますし、わかりづらくなっていました。
今回は新旧両方の書き方を載せておきますが、基本的に静的な値を扱う場合は最新の書き方が簡素でわかりやすいのでそちらを使いましょう。


※ *静的な値、とは一度決めた値から変更がされないことを保証されている値、固定値のことです。*
### 2-1. 最新の書き方(Dart2.17以降)

```lib/shared_widgets/notification_dialog.dart
/// 通知の種類
///
/// Dart2.17以降ではこう書ける(2022年5月以降)
enum NotificationType {
  /// 通信失敗
  communicationFailure(
    title: '通信の失敗',
    content: '通信が失敗しました。ネットワークを確認してください。',
    doneButtonText: '閉じる',
  ),

  /// データ取得失敗
  dataRetrievalFailure(
    title: 'データの取得に失敗',
    content: '原因不明のエラーによりデータの取得に失敗しました。もう一度お試しください。',
    doneButtonText: '閉じる',
  ),

  /// ダウンロードの終了
  finishDownload(
    title: 'ダウンロード完了',
    content: 'ダウンロードが正常に完了しました。',
    doneButtonText: 'OK',
  ); // <-　最後は';'で区切る

  const NotificationType({
    required this.title,
    required this.content,
    required this.doneButtonText,
  });

  /// 通知のタイトル
  final String title;

  /// 通知の内容
  final String content;

  /// ダイアログの閉じるボタンの表示
  final String doneButtonText;
}
```

### 2-2. 古い書き方（Dart2.17より前のバージョン）

```lib/shared_widgets/notification_dialog.dart
enum NotificationTypeOld {
  communicationFailure,
  dataRetrievalFailure,
  finishDownload,
}
extension NotificationDialogTypeExtension on NotificationTypeOld {
  String get title {
    switch (this) {
      case NotificationTypeOld.communicationFailure:
        return '通信の失敗';
      case NotificationTypeOld.dataRetrievalFailure:
        return 'データの取得に失敗';
      case NotificationTypeOld.finishDownload:
        return 'ダウンロード完了';
    }
  }

  String get content {
    switch (this) {
      case NotificationTypeOld.communicationFailure:
        return '通信が失敗しました。ネットワークを確認してください。';
      case NotificationTypeOld.dataRetrievalFailure:
        return '原因不明のエラーによりデータの取得に失敗しました。もう一度お試しください。';
      case NotificationTypeOld.finishDownload:
        return 'ダウンロードが正常に完了しました。';
    }
  }

  String get doneButtonText {
    switch (this) {
      case NotificationTypeOld.communicationFailure:
        return '閉じる';
      case NotificationTypeOld.dataRetrievalFailure:
        return '閉じる';
      case NotificationTypeOld.finishDownload:
        return 'OK';
    }
  }
}

```

::: note
動的な値を扱う場合は今回のように`extension`と`getter`を使って定義する必要があります。
こちらについては以下の記事で解説しています。
:::

https://qiita.com/Harx02663971/items/7064c529dfeab69bf5bc

## 3. switch式を使った条件分岐

Dartのswitch式はDart 3.0から使えるようになりました（2023年5月）。
switch式は書き方に慣れると様々なところで処理を簡潔に書けるようになります。

### 3-1. switch式を使ってダイアログの内容に汎用性を持たせる

switch式を使ってタイトルの文字列、内容の文字列、ボタンの表示の文字列に関して以下を実現します。

- 告知の種類を指定すると上記の３つを決められた内容で指定できる
- 告知の種類を指定しなければ、上記の３つを自由に入れられる

```lib/shared_widgets/notification_dialog.dart
class NotificationDialog extends StatelessWidget {
  const NotificationDialog({
    this.type,
    this.title,
    this.content,
    this.doneButtonText,
    super.key,
  });

  final Widget? title;
  final Widget? content;
  final String? doneButtonText;
  final NotificationType? type;

  @override
  Widget build(BuildContext context) {
    const padding = EdgeInsets.all(24);

	// 🐦switch式
    final (titlePadding, contentPadding) = switch ((title, content)) {
      (null, _?) => (null, padding),
      (_?, null) => (padding, null),
      _ => (null, null),
    };

	// 🐦switch式
    // ここで引数typeのnullチェックをswitch式で行う
    // 引数のtypeが
    final titleWidget = switch (type) {
      // final hoge?でnullを剥がす => つまり引数に値が入っていた場合
      final type? => Text(type.title),
      // それ以外、つまりtypeはnullだった場合
      _ => title,
    };

	// 🐦switch式
    final contentWidget = switch (type) {
      final type? => Text(type.content),
      _ => content,
    };

	// 🐦switch式
    final buttonText = switch (type) {
      final type? => type.doneButtonText,
      _ => doneButtonText ?? '閉じる',
    };

    // PopScopeはAndroidの戻るボタンが無効になる
    return PopScope(
      canPop: false,
      child: AlertDialog(
        title: titleWidget,
        titlePadding: titlePadding,
        content: contentWidget,
        contentPadding: contentPadding,
        actions: [
          Center(
            child: ElevatedButton(
              style: ButtonStyle(
                minimumSize:
                    MaterialStateProperty.all(const Size.fromHeight(48)),
                backgroundColor: MaterialStateProperty.all(Colors.blue),
                foregroundColor: MaterialStateProperty.all(Colors.white),
                textStyle: MaterialStateProperty.all(
                  Theme.of(context)
                      .textTheme
                      .labelLarge!
                      .copyWith(fontWeight: FontWeight.w600),
                ),
              ),
              onPressed: () {
                Navigator.pop(context);
              },
              child: Text(buttonText),
            ),
          ),
        ],
      ),
    );
  }
}
```

### 3-2. 部分解説

```dart
	// 🐦switch式
    // ここで引数typeのnullチェックをswitch式で行う
    // 引数のtypeが
    final titleWidget = switch (type) {
      // final hoge?でnullを剥がす => つまり引数に値が入っていた場合
      final type? => Text(type.title),
      // それ以外、つまりtypeはnullだった場合
      _ => title,
    };
```

定数`titleWidget`にswitch式で値を格納しています。
switchの条件は引数である`final NotificationType? type;`を指しています。
引数typeの値が存在している場合は`NotificationType`のフィールド`title`を使ったTextウィジェットが入ります。

アンダースコアは上記で述べられた条件以外の場合を指しますが、今回で行けば`type`に何も値が入っていない場合を指しています。
そしてその場合は`final Widget? title;`が入ります。

ここをStringではなくWidgetにしているのは汎用性を高めるためにあえてそうしています。
また、null許容なので入れても入れなない場合も動くようにしています。

## 4. 実装する

### 4-1. ダイアログを呼び出す関数をグローバルで定義

```lib/shared_widgets/notification_dialog.dart
/// ユーザーへの告知ダイアログを表示する
///
/// - [type] を指定するとダイアログのタイトル、コンテント、ボタンのテキストが自動で設定される
/// - [title] タイトルのウイジェットで[type]が指定されている場合は無視される
/// - [content]コンテントのウイジェットで[type]が指定されている場合は無視される
/// - [doneButtonText] ボタンのテキストで[type]が指定されている場合は無視される
Future<void> showNotificationDialog(
  BuildContext context, {
  NotificationType? type,
  Widget? title,
  Widget? content,
  String? doneButtonText,
}) async {
  await showDialog<void>(
    context: context,
    barrierDismissible: false,
    builder: (context) {
      return NotificationDialog(
        type: type,
        title: title,
        content: content,
        doneButtonText: doneButtonText,
      );
    },
  );
}
```

### 4-2. 実際にenumをしてした場合としなかった場合の処理

ActionItemは内容を指定したListTileウィジェットです。詳細はGitHubでご確認ください。

```lib/my_home_page/my_home_page.dart
class _NotificationSelectButton extends StatelessWidget {
  const _NotificationSelectButton();

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () async {
        showActionBottomSheet(
          context,
          actions: [
            ActionItem(
              icon: Icons.wifi_off,
              text: '通信失敗',
              onTap: () => showNotificationDialog(
                context,
                type: NotificationType.communicationFailure,
              ),
            ),
            ActionItem(
              icon: Icons.error_outline,
              text: 'データの取得に失敗',
              onTap: () => showNotificationDialog(
                context,
                type: NotificationType.dataRetrievalFailure,
              ),
            ),
            ActionItem(
              icon: Icons.download_done,
              text: 'ダウンロード完了',
              onTap: () => showNotificationDialog(
                context,
                type: NotificationType.finishDownload,
              ),
            ),
            ActionItem(
              icon: Icons.edit,
              text: '独自に入力',
              onTap: () => showNotificationDialog(
                context,
                title: const Text('アップロードの完了'),
                content: const Text('クラウドへ写真データをアップロードしました。'),
                doneButtonText: 'とじーーる',
              ),
            ),
          ],
        );
      },
      child: const Text('通知ダイアログ'),
    );
  }
}
```

## 終わりに

この記事では、最新のDartの機能を活用して、enumのフィールドの設定方法とswitch式を使った条件分岐の書き方を解説しました。また、それらを用いて通知ダイアログを作成する例を紹介しました。これらの方法を用いることで、コードが簡潔になり、読みやすくなることが期待できます。

この記事が誰かのお役に立てれば幸いです。