tech

Flutterのfreezedでイミュータブルなデータクラスを扱う

· 12min · Flutter, Dart, freezed
index

はじめに

Dartには標準でイミュータブルなデータクラスを簡潔に書く仕組みがありません。 自分でcopyWith==hashCodetoStringを書こうとすると、ボイラープレートが膨大になりがちです。

そこで登場するのがfreezedです。 コード生成によって、上記をすべて自動で用意してくれます。

本記事では、freezedの基本的な使い方とよくあるハマりどころを紹介します。

インストール

dependencies:
  freezed_annotation: ^2.4.1
  json_annotation: ^4.8.1

dev_dependencies:
  build_runner: ^2.4.6
  freezed: ^2.4.5
  json_serializable: ^6.7.1

パッケージを追加したら、build_runnerでコード生成を実行します。

dart run build_runner watch --delete-conflicting-outputs

watchにしておくと、ファイルの変更を検知して自動生成してくれるので便利です。

基本的な使い方

シンプルなユーザークラスを例に見てみます。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String id,
    required String name,
    int? age,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

これだけで、以下の機能が自動的に生成されます。

  • copyWith
  • ==hashCode
  • toString
  • fromJson / toJson

copyWithの使用例

final user = User(id: '1', name: 'Alice');
final updated = user.copyWith(name: 'Alicia');

ミュータブルにしないとフィールドを更新できないという悩みが解消されます。

Unionによる状態表現

freezedの強力な機能のひとつがUnion型です。 たとえばAPIの結果を表現したいとき、こんなふうに書けます。

@freezed
class Result<T> with _$Result<T> {
  const factory Result.success(T data) = Success<T>;
  const factory Result.failure(String message) = Failure<T>;
}

使う側ではパターンマッチングで分岐できます。

result.when(
  success: (data) => print('OK: $data'),
  failure: (msg)  => print('NG: $msg'),
);

whenは全パターンを漏らさず扱うように強制されるので、バグが起きづらくなります。 ローディング状態を追加するならこう。

@freezed
class AsyncState<T> with _$AsyncState<T> {
  const factory AsyncState.loading() = _Loading<T>;
  const factory AsyncState.data(T value) = _Data<T>;
  const factory AsyncState.error(Object error) = _Error<T>;
}

よくあるハマりどころ

1. part宣言を忘れる

part 'user.freezed.dart';
part 'user.g.dart'; // fromJson/toJsonを使うときだけ必要

freezed.freezed.dartを生成し、json_serializable.g.dartを生成します。 どちらもpart宣言が必要です。

2. const factoryにし忘れる

const factoryにしておくことで、コンパイル時定数として扱えます。 基本はすべてconst factoryでOKです。

3. カスタムメソッドを書きたい

メソッドを追加したい場合はプライベートコンストラクタを使います。

@freezed
class User with _$User {
  const User._();

  const factory User({
    required String id,
    required String name,
  }) = _User;

  String greeting() => 'Hello, $name!';
}

const User._();がないとカスタムメソッドを定義できないので注意です。

おわりに

freezedを使えば、Dartでも安全で簡潔にイミュータブルなクラスを扱えます。 個人的にはRiverpodと組み合わせて状態管理する時にも非常に相性が良く、Flutterプロジェクトには欠かせないパッケージになっています。

参考文献