Flutter のアプリを作成する上で、ベストプラクティスではないけれど、こんな感じで使っています。という実装メモです。
今回はenumについてです。
属性の拡張
Dartのenumはデフォルトでname属性を持ちますが、それ以外の要素も付加できるので、アプリ内固定で持つ情報などは色々定義しておくと便利に思います。
例えば映画を管理するアプリを考えた時に、ジャンルをカテゴリーとしてenumで定義した場合を想定してみました。表示するカテゴリ名と並び替え順序を定義しています。
カテゴリー情報を持つenum Category
enum Category {
action(displayName: 'アクション', sortNumber: 1),
adventure(displayName: 'アドベンチャー', sortNumber: 2),
sf(displayName: 'SF', sortNumber: 3),
comedy(displayName: 'コメディ', sortNumber: 4),
horror(displayName: 'ホラー', sortNumber: 5),
other(displayName: 'その他', sortNumber: 9),
;
final String displayName;
final int sortNumber;
const Category({required this.displayName, required this.sortNumber});
}
映画データを持つMovieクラス
class Movie {
final String title;
final Category category;
Movie({
required this.title,
required this.category,
});
factory Movie.fromJson(String json) {
final map = jsonDecode(json) as Map<String, dynamic>;
return Movie(
title: map['title'],
category: Category.values.byName((map['category'])),
);
}
@override
String toString() {
return 'title=$title, category=${category.name} : ${category.displayName}';
}
}
メイン関数
void main() {
List<Movie> movies = [
Movie(title: 'MAD MAX', category: Category.action),
Movie(title: 'Stand by me', category: Category.adventure),
Movie(title: 'STAR WARDS', category: Category.sf),
Movie(title: 'Home Alone', category: Category.comedy),
Movie(title: 'Jaws', category: Category.horror),
];
for (Movie element in movies) {
print(element.toString());
}
}
コンソール出力
title=MAD MAX, category=action : アクション
title=Stand by me, category=adventure : アドベンチャー
title=STAR WARDS, category=sf : SF
title=Home Alone, category=comedy : コメディ
title=Jaws, category=horror : ホラー
メソッドの拡張
上記では、Category.values.byName(String name) で文字列からenumを取得しています。
しかし、データソースを外部から取得する時などでenum定義に変換先がなかった場合、ここではエラーが発生します。
例えば以下のようなJsonを取得した場合
// enumにないカテゴリーのデータ
const unknownCategoryData = '''
{
"title": "THIS IS IT",
"category": "documentary"
}
''';
// enumは小文字で定義しているが、取得したデータが大文字の場合
const upperCaseCategoryData = '''
{
"title": "Spider-Man",
"category": "ACTION"
}
''';
完全に正規化されていないデータを扱うことは多いと思いますので、enum全体で対応できるように以下の拡張を定義しました。
// Stringの値からenumの値に変換する。
// マッチする値がない場合、nullを返す
extension EnumByName<T extends Enum> on Iterable<T> {
T? searchByName(String name) {
for (var value in this) {
if (value.name.toLowerCase() == name.toLowerCase()) return value;
}
return null;
}
}
これにより、enumにない文字列が渡ってきた場合はnullを返します。
Movieクラスの場合はnullをotherとすることで必須パラメータに対応しています。この辺りは各クラスの作りによると思いますが、ハンドリングはしやすいと思います。
MovieクラスのJsonパースメソッド
factory Movie.fromJson(String json) {
final map = jsonDecode(json) as Map<String, dynamic>;
return Movie(
title: map['title'],
// enum Category にない値であれば null が返るので、Category.otherを設定
category: Category.values.searchByName(map['category']) ?? Category.other,
);
また、byNameメソッドでは小文字と大文字を区別するものになっていますので、無視できる作りとしています。もちろんこの場合は、enum内で「小文字・大文字の区分なく一意になる」状態で定義する必要があります。
メイン関数
void main() {
List<Movie> movies = [
Movie.fromJson(unknownCategoryData),
Movie.fromJson(upperCaseCategoryData),
];
for (Movie element in movies) {
print(element.toString());
}
}
コンソール出力
title=THIS IS IT, category=other : その他
title=Spider-Man, category=action : アクション
無事にenumを設定できました。なにかと便利なenumクラスは積極的に使用していきたいと思います。
今回は以上です。
補足
ここまで人力で色々設定していますが、そもそも、JsonからのパースをFreezedで自動生成する場合は、アノテーション一つで同じようなことが可能です。便利なものは積極的に使用していくべきですね。
参考: https://blog.dalt.me/3138
以下、コード全文
import 'dart:convert';
enum Category {
action(displayName: 'アクション', sortNumber: 1),
adventure(displayName: 'アドベンチャー', sortNumber: 2),
sf(displayName: 'SF', sortNumber: 3),
comedy(displayName: 'コメディ', sortNumber: 4),
horror(displayName: 'ホラー', sortNumber: 5),
other(displayName: 'その他', sortNumber: 9),
;
final String displayName;
final int sortNumber;
const Category({required this.displayName, required this.sortNumber});
}
// Stringの値からenumの値に変換する。
// マッチする値がない場合、nullを返す
extension EnumByName<T extends Enum> on Iterable<T> {
T? searchByName(String name) {
for (var value in this) {
if (value.name.toLowerCase() == name.toLowerCase()) return value;
}
return null;
}
}
class Movie {
final String title;
final Category category;
Movie({
required this.title,
required this.category,
});
factory Movie.fromJson(String json) {
final map = jsonDecode(json) as Map<String, dynamic>;
return Movie(
title: map['title'],
// category: Category.values.byName((map['category'])
// enum Category にない値であれば null が返るので、Category.otherを設定
category: Category.values.searchByName(map['category']) ?? Category.other,
);
}
@override
String toString() {
return 'title=$title, category=${category.name} : ${category.displayName}';
}
}
// enumにないカテゴリーのデータ
const unknownCategoryData = '''
{
"title": "THIS IS IT",
"category": "documentary"
}
''';
// enumは小文字定義で入ってきたデータが大文字の場合のデータ
const upperCaseCategoryData = '''
{
"title": "Spider-Man",
"category": "ACTION"
}
''';
void main() {
List<Movie> movies = [
Movie(title: 'MAD MAX', category: Category.action),
Movie(title: 'Stand by me', category: Category.adventure),
Movie(title: 'STAR WARDS', category: Category.sf),
Movie(title: 'Home Alone', category: Category.comedy),
Movie(title: 'Jaws', category: Category.horror),
Movie.fromJson(unknownCategoryData),
Movie.fromJson(upperCaseCategoryData),
];
for (Movie element in movies) {
print(element.toString());
}
}
参考元
https://qiita.com/b4tchkn/items/d0cb7b124e9dcc3ab73a
https://qiita.com/ambleside138/items/153625257d1cad3e0856