【入門編】Dart 3のsealed classと変数宣言を組み合わせた、網羅的な状態管理の実装 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

こんにちは!FlutterやDartの開発現場で日々コードを書いていますか?
今回は、Dart 3で導入されてから私たちの開発スタイルを劇的に変えた「sealed class(シール堂クラス)」と、変数宣言(`final`など)を組み合わせた最先端の状態管理術についてお話しします。

他の言語(TypeScriptやRust、Kotlinなど)からDartに入ってきた方なら、「おっ、Algebraic Data Types(代数的データ型)やパターンマッチングに近い概念だな!」とピンとくるかもしれません。

「状態管理って難しそう…」「`switch`文の書き漏らしで予期せぬバグを踏んだことがある…」そんなモヤモヤを抱えているなら、ここをクリアすればあなたのDartのコードは一気に堅牢で美しいものになりますよ。さあ、一緒にマスターしていきましょう!

—

1. なぜ「sealed class」と変数宣言が最強のコンビなのか?

アプリを作っていると、必ずと言っていいほど「非同期通信の状態」に直面しますよね。

  • 今、データを読み込み中(Loading)なのか?
  • 無事にデータが取れた(Success)のか?
  • エラーが発生した(Error)のか?

これらを従来のフラットなクラスや、適当なフラグ(`isLoading = true` など)で管理しようとすると、「エラーなのにデータが入っている」というような、矛盾した状態(不都合な真実)を型システムが許容してしまいがちです。

ここで登場するのが `sealed class` です。
sealed classを使うと、「このクラスを継承できるのは、同じファイル内で定義された特定のサブクラスたちだけですよ」という制限をDartのコンパイラ(AOTコンパイラ)に教えることができます。

そして、Dart 3の強力なパターンマッチング(`switch`式)と組み合わせることで、コンパイラが「すべての状態が網羅されているか」をコンパイル時に厳密にチェックしてくれるようになります。

—

2. 実装コードで体感する:安全な状態管理の作法

百聞は一見にしかず。実際にコードを書いてその挙動を覗いてみましょう。
ここでは、ユーザー情報を取得する非同期処理の状態をモデル化してみます。

import ‘package:flutter/foundation.dart’;

// 1. sealed classで状態の「世界線」を定義する
sealed class UserState {}

// 2. それぞれの状態をサブクラスとして定義する
// finalをつけておくことで、このファイル外からの意図しない拡張を防ぎます
final class UserInitial extends UserState {}

final class UserLoading extends UserState {}

final class UserSuccess extends UserState {
final String userName;
UserSuccess(this.userName);
}

final class UserError extends UserState {
final String message;
UserError(this.message);
}

変数宣言とswitch式による網羅的ハンドリング

さて、定義した状態を実際のUIやビジネスロジックでどう扱うか見てみましょう。ここで `final` や `var` といった変数宣言の知識が活きてきます。

void handleUserState(UserState state) {
// Dart 3の `switch` は「文(Statement)」だけでなく「式(Expression)」としても使えます!
// 評価された結果を変数に直接代入できるため、非常にスマートです。
final String displayMessage = switch (state) {
// 初期状態
UserInitial() => ‘ボタンを押してデータを取得してください。’,

// ローディング中
UserLoading() => ‘読み込み中…’,

// 成功時(subclassのプロパティにもパターンマッチで安全にアクセスできます)
UserSuccess(userName: final name) => ‘ようこそ、$nameさん!’,

// エラー時
UserError(message: final err) => ‘エラーが発生しました: $err’,

// 【重要】もしここに `UserError` の処理を書き忘れると、
// コンパイラが「Missing case: UserError」と怒ってコンパイルを通しません!
};

print(displayMessage);
}

図解:コンパイル時の網羅性チェックのイメージ

[ sealed class UserState ]
│
├─► UserInitial ──┐
├─► UserLoading ──┼──► switch (state) ── [ すべて網羅されているか? ]
├─► UserSuccess ──┤ │
└─► UserError ──┘ ├─ 抜けがある ──► ❌ コンパイルエラー(バグを未然に防止!)
└─ すべてOK ──► ◯ 安全にビルド完了

このように、将来もし新しい状態(例えば `UserTimeout` など)を `UserState` の下に追加した場合、Dartのコンパイラは「おい、新しい状態が追加されたのに、あのファイルの `switch` 式でハンドリングし忘れているぞ!」と、実行時ではなく開発中のコンパイル時に教えてくれます。

—

3. 陥りやすい罠と知っておくべきDartの仕様

ここで、現場でやりがちなミスや、知っておくと一目置かれるDart VMの裏側の話を少しだけ。

トラップ1: `default` や `_` を安易に使わない

他の言語の `switch` に慣れていると、網羅性を強制されている最中に面倒臭くなって、ついつい以下のように書きたくなります。

// ❌ 避けるべきアンチパターン
final message = switch (state) {
UserInitial() => ‘初期状態’,
// 面倒だからその他は全部これで…という書き方
_ => ‘その他の状態’,
};

これをしてしまうと、せっかく `sealed class` が持つ「新しい状態を追加した時にハンドリング漏れを防ぐ機能(網羅性チェック)」が無効化されてしまいます。将来、状態を追加したときにコンパイラが警告してくれなくなるため、`_` や `default` によるフォールバックは、本当に予期せぬ値があり得る場合を除いて、極力避けるのがプロの作法です。

トラップ2: `final` 変数との相性

状態の保持には `final` 変数を使いましょう。不可変(Immutability)なデータ構造と `sealed class` は相性抜群です。Dart VMは、不変であることが保証されたオブジェクトに対して最適化(メモリ上の効率的な配置やガベージコレクションの負荷軽減)を働きかけやすくするため、パフォーマンスの観点からも `final` や `const` を積極的に活用するべきです。

—

4. まとめ:ここをクリアすれば、Dartの基本はバッチリマスターできますよ!

今回は、Dart 3の `sealed class` と変数宣言を組み合わせた堅牢な状態管理について解説しました。

  • `sealed class` で、そのクラスが取り得る状態の「世界線」を完全に閉じ込める。
  • `switch` 式と `final` などの変数宣言を組み合わせることで、安全かつ簡潔に値を導出する。
  • コンパイラの網羅性チェックにより、人の記憶違いや実装漏れによるバグをコンパイル時に根絶する。

このパターンを自分の引き出しに持っているだけで、あなたの書くFlutter/Dartコードの信頼性は一段と跳ね上がります。「なんとなく動くコード」から「型に守られた美しいコード」へ。

ぜひ、日々の開発の非同期処理や画面の状態管理で試してみてくださいね。それでは、また次回の極限の知見でお会いしましょう!

タイトルとURLをコピーしました