😶🌫️
sealed classしか勝たん
Flutterエンジニアの小田島です。
みなさんsealed class使ってますか?
僕は先月初めて存在を知りました。
- sealed classって何?
Dart公式ドキュメント
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公式ドキュメント
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公式のサンプルコード。よくある認証状態の場合分けですが、こんな感じで成功の時しかデータはない、エラーの時しか例外はない、みたいに状態によってフィールドを変えたい時に使い勝手が良さそうです。
- ユースケース
上述の認証状態のようなケースに加えて、色んなデータを同じリストで表示したい、みたいなフロントエンドでよくあるユースケースにも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