所感箱

所感を書きためていきます

Flutter開発で利用しているfreezedの備忘録

普段はkotlinでのAndroid開発に慣れているため、kotlinに言語仕様を使いたくなることがあり、擬似的にenumやsealed class的な扱いができるのが気に入っている。
ライブラリ自体は、immutable なオブジェクト用のコード生成を主題としているので、そういうお悩みも解決できるのが素敵だ。

今回は、sealed class的な扱い方の備忘録も兼ねてブログに残しておく。

pub.dev

freezedでの記述方法

例えば、ひとつのListViewにViewTypeに応じて、ウィジェットを切りたい変えたくなった場合、freezedを使うと下記のように書くことができる。

クラス定義

@freezed
class HomePageListItem with _$HomePageListItem {
  const factory HomePageListItem.header(String header) = Header;
  const factory HomePageListItem.task(String header, String description) = Task;
}

利用時

final items = List<HomePageListItem>.generate(
  1000,
      (i) => i % 6 == 0
      ? HomePageListItem.header('Heading $i')
      : HomePageListItem.task('Sender $i', 'Message body $i'),
);

// whenで各クラスtypeに応じた分岐処理をかける
final item = items[index];
return item.when(
    header: (title) { return ListTile(title: Text(title), subtitle: const SizedBox.shrink()); },
    task: (title, description) { return ListTile(title: Text(title), subtitle: Text(description)); }
);

whenというfunctionの名前付き引数がViewの種別を表していて、どのViewTypeでどのウィジットが生成されるかがわかりやすくなると感じている。

どうやって実現されているか。

では、sealed classやwhen構文のないdartでどのように実現しているのだろうか。
結論を先に書くと、freezedはcode generatorの機能でコードを生成している。

コード生成のコマンド

flutter pub run build_runner build --delete-conflicting-outputs

このコマンドを実行すると、hoge.freezed.dartというファイルに、コードが生成される。 コードの中身をseald に関係するのみ抜粋する下記のようになる。

生成されるコード

// 各クラスに機能を提供するためのmixin
mixin _$HomePageListItem {
  String get header => throw _privateConstructorUsedError;

  @optionalTypeArgs
  TResult when<TResult extends Object?>({
    required TResult Function(String header) header,
    required TResult Function(String header, String description) task,
  }) =>
      throw _privateConstructorUsedError;
}

// _$Headerと_$Taskのクラスでwhenをoverrideして実装。
// 実行時はクラスタイプに応じたfunctionが呼び出される
class _$Header with DiagnosticableTreeMixin implements Header {
  const _$Header(this.header);

  @override
  @optionalTypeArgs
  TResult when<TResult extends Object?>({
    required TResult Function(String header) header,
    required TResult Function(String header, String description) task,
  }) {
    //  headrに指定したfunctionのみ実行
    return header(this.header);
  }
}

class _$Task with DiagnosticableTreeMixin implements Task {
  const _$Task(this.header, this.description);

@override
  @optionalTypeArgs
  TResult when<TResult extends Object?>({
    required TResult Function(String header) header,
    required TResult Function(String header, String description) task,
  }) {
    // taskに指定したfunctionのみ実行
    return task(this.header, description);
  }
}

つまりwhenのcall時にクラスタイプに応じて引数で渡したfunctionのいずれをcallするかを分岐して、sealed class的な whenを実現しているようだった。

// itemがHeaderの場合: header引数のfunction
// temがTaskの場合: task引数のfunction
final item = items[index];
return item.when(
    header: (title) { return ListTile(title: Text(title), subtitle: const SizedBox.shrink()); },
    task: (title, description) { return ListTile(title: Text(title), subtitle: Text(description)); }
);

所感

使ってみて以下のメリデメがあると感じている。

メリット

  • dartを使いながらimmutableなコードを扱える
  • sealed classやenumのように分岐をかける

デメリット

  • code generatorでコード生成する使用上、dartのバージョンアップに依存する

対象言語のバージョンに依存するのは、code generatorの辛みでもあるが、サービス開発で使う場合は、慎重検討をした方が良さそうに思った。dartが公式で同等の機能を実装してくれると良いのだけれど。