😶‍🌫️

sealed classしか勝たん

に公開

Flutterエンジニアの小田島です。

みなさんsealed class使ってますか?
僕は先月初めて存在を知りました。

  1. sealed classって何?

Dart公式ドキュメント
https://dart.dev/language/class-modifiers#sealed

sealed class Vehicle {}

class Car extends Vehicle {}

class Truck implements Vehicle {}

class Bicycle extends Vehicle {}

// ERROR: Can't be instantiated.
Vehicle myVehicle = Vehicle();

// Subclasses can be instantiated.
Vehicle myCar = Car();

String getVehicleSound(Vehicle vehicle) {
  // ERROR: The switch is missing the Bicycle subtype or a default case.
  return switch (vehicle) {
    Car() => 'vroom',
    Truck() => 'VROOOOMM',
  };
}

公式のサンプルコードですが、これだとenumと何が違うのかイメージしづらいですね。

Kotlin公式ドキュメント

https://kotlinlang.org/docs/sealed-classes.html#state-management-in-ui-applications

sealed class UIState {
    data object Loading : UIState()
    data class Success(val data: String) : UIState()
    data class Error(val exception: Exception) : UIState()
}

fun updateUI(state: UIState) {
    when (state) {
        is UIState.Loading -> showLoadingIndicator()
        is UIState.Success -> showData(state.data)
        is UIState.Error -> showError(state.exception)
    }
}

Kotlin公式のサンプルコード。よくある認証状態の場合分けですが、こんな感じで成功の時しかデータはない、エラーの時しか例外はない、みたいに状態によってフィールドを変えたい時に使い勝手が良さそうです。

  1. ユースケース
    上述の認証状態のようなケースに加えて、色んなデータを同じリストで表示したい、みたいなフロントエンドでよくあるユースケースにもsealed classは有用だと思いました。

下記はChat GPTにFlutterでsealed classを使って、ニュース一覧の中に「ニュース」「広告」「もっと見る」の3種類のデータが混在する表を実装してください、と入力して作成したコードです。

import 'package:flutter/material.dart';

/// Sealed class for different item types in the news feed
sealed class FeedItem {}

class NewsItem extends FeedItem {
  final String title;
  final String summary;

  const NewsItem(this.title, this.summary);
}

class AdItem extends FeedItem {
  final String imageUrl;

  const AdItem(this.imageUrl);
}

class LoadMoreItem extends FeedItem {
  const LoadMoreItem();
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: NewsFeedPage(),
    );
  }
}

class NewsFeedPage extends StatelessWidget {
  const NewsFeedPage({super.key});

  final List<FeedItem> feedItems = const [
    NewsItem("AIが世界を変える", "最新のAI技術が社会に与える影響とは?"),
    AdItem("https://via.placeholder.com/300x100.png?text=広告"),
    NewsItem("Flutter 3.13登場", "パフォーマンスと安定性が向上した最新版がリリース"),
    LoadMoreItem(),
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("ニュースフィード")),
      body: ListView.builder(
        itemCount: feedItems.length,
        itemBuilder: (context, index) {
          final item = feedItems[index];

          return switch (item) {
            NewsItem(:final title, :final summary) => ListTile(
                leading: const Icon(Icons.article),
                title: Text(title),
                subtitle: Text(summary),
              ),
            AdItem(:final imageUrl) => Padding(
                padding: const EdgeInsets.symmetric(vertical: 8),
                child: Image.network(imageUrl),
              ),
            LoadMoreItem() => Center(
                child: TextButton(
                  onPressed: () {
                    // 実際には次ページを読み込む処理など
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text("もっと見るがタップされました")),
                    );
                  },
                  child: const Text("もっと見る"),
                ),
              ),
          };
        },
      ),
    );
  }
}

良い感じですね。
同じコードをsealed classを使わずに書いてくださいと入力した結果が下記になります。

import 'package:flutter/material.dart';

enum FeedItemType { news, ad, loadMore }

class FeedItem {
  final FeedItemType type;

  // データ本体
  final String? title;
  final String? summary;
  final String? imageUrl;

  const FeedItem.news({required this.title, required this.summary})
      : type = FeedItemType.news,
        imageUrl = null;

  const FeedItem.ad({required this.imageUrl})
      : type = FeedItemType.ad,
        title = null,
        summary = null;

  const FeedItem.loadMore()
      : type = FeedItemType.loadMore,
        title = null,
        summary = null,
        imageUrl = null;
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: NewsFeedPage(),
    );
  }
}

class NewsFeedPage extends StatelessWidget {
  const NewsFeedPage({super.key});

  final List<FeedItem> feedItems = const [
    FeedItem.news(
      title: "AIが世界を変える",
      summary: "最新のAI技術が社会に与える影響とは?",
    ),
    FeedItem.ad(
      imageUrl: "https://via.placeholder.com/300x100.png?text=広告",
    ),
    FeedItem.news(
      title: "Flutter 3.13登場",
      summary: "パフォーマンスと安定性が向上した最新版がリリース",
    ),
    FeedItem.loadMore(),
  ];

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("ニュースフィード")),
      body: ListView.builder(
        itemCount: feedItems.length,
        itemBuilder: (context, index) {
          final item = feedItems[index];

          switch (item.type) {
            case FeedItemType.news:
              return ListTile(
                leading: const Icon(Icons.article),
                title: Text(item.title ?? ""),
                subtitle: Text(item.summary ?? ""),
              );
            case FeedItemType.ad:
              return Padding(
                padding: const EdgeInsets.symmetric(vertical: 8),
                child: Image.network(item.imageUrl ?? ""),
              );
            case FeedItemType.loadMore:
              return Center(
                child: TextButton(
                  onPressed: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text("もっと見るがタップされました")),
                    );
                  },
                  child: const Text("もっと見る"),
                ),
              );
          }
        },
      ),
    );
  }
}

なんだか見覚えのあるコードになりましたね。。
こんなコードを今までに50回くらい書いたような気がします(ごめんなさい)

sealed classは扱う対象が複雑になる程効果を発揮するので、例えばExcelのような複数行の表などでも有効に使えそうだなと思います。

Discussion