パッケージとドキュメント

Freezed とは

Code generation for immutable classes

イミュータブルなデータクラスを作成するためのコードジェネレータです。

データクラスを用意しようとすると数十行のコードを書く必要がありますが、それを毎回手書きしていては大変ですし、タイポによるバグも起きかねません。

データクラスで書くべきコードは定型のボイラープレートのため、それならば自動生成してしまったほうが早く、かつ間違いもないということで、そのためのコードジェネレータが Freezed です。

具体的には、データクラスのもとになるわずかな量のコードを書いた後にコマンドを叩くと、数十行のデータクラスが書かれたファイルが自動生成されるというものです。

このように Freezed はあくまでも簡略化のためのツールであり、どのプロジェクトでも導入が必須というわけではありません。しかしながらその利便性から多くのユーザに使用されています。

補足:データクラスとは

主にデータの保持することだけが目的のクラスのこと。フィールドの他に、いくつかの基本的なメソッド(*)を持つだけのクラス。

* equals(), hashCode(), toString(), copy() などの類

1. インストールとリント設定の編集

以降の内容は公式の説明沿っていますが、一部カスタマイズしている部分があります。

1-1. インストール

flutter pub add freezed_annotation
flutter pub add --dev build_runner
flutter pub add --dev freezed

データクラスに fromJson/toJson メソッドも持たせるのであれば以下も合わせてインストール。

flutter pub add json_annotation
flutter pub add --dev json_serializable

(以降の手順では上記もインストールした前提で進める)

インストールしたパッケージの一覧

1-2. リントの設定を編集

If you plan on using Freezed in combination with json_serializable, recent versions of json_serializable and meta may require you to disable the invalid_annotation_target warning.

To do that, you can add the following to an analysis_options.yaml at the root of your project:

analysis_options.yaml に以下を追記します。

analyzer:
  errors:
    invalid_annotation_target: ignore

ここまででインストールと初期設定は完了です。

2. Freezed でデータクラスを作成する

新規でデータクラスを作成するときは以降の手順を踏みます。

2-1. データクラスのもとを作成する

lib ディレクトリ直下に model ディレクトリを作成し、その中に person.dart ファイルを作成します。

なお、Freezed のファイルは lib ディレクトリ配下のどこに作成しても問題ありません。専用のフォルダを作成したのは分かりやすさのためです。

そしてperson.dart ファイルに以下を記載します。

// This file is "lib/model/person.dart"
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

// required: associates our `person.dart` with the code generated by Freezed
part 'person.freezed.dart';
// optional: Since our Person class is serializable, we must add this line.
// But if Person was not serializable, we could skip it.
part 'person.g.dart';

@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;

  factory Person.fromJson(Map<String, Object?> json) => _$PersonFromJson(json);
}

この時点では IDE が何かしらエラーを表示していると思いますが、次の作業で解消されますのでこのまま進めてください。

なお上記コードが意味するところについて、公式ドキュメントで以下の通り説明されています。

The following snippet defines a model named Person:

  • Person has 3 properties: firstName, lastName and age
  • Because we are using @freezed, all of this class’s properties are immutable.
  • Since we defined a fromJson, this class is de/serializable. Freezed will add a toJson method for us.
  • Freezed will also automatically generate:
    • a copyWith method, for cloning the object with different properties
    • a toString override listing all the properties of the object
    • an operator == and hashCode override (since Person is immutable)

From this example, we can notice a few things:

  • It is necessary to annotate our model with @freezed (or @Freezed/@unfreezed, more about that later). This annotation is what tells Freezed to generate code for that class.

  • We must also apply a mixin with the name of our class, prefixed by _$. This mixin is what defines the various properties/methods of our object.

  • When defining a constructor in a Freezed class, we should use the factory keyword as showcased (const is optional). The parameters of this constructor will be the list of all properties that this class contains. Parameters don’t have to be named and required. Feel free to use positional optional parameters if you want!

補足

上記は immutable なデータクラスのもとになるものでした。

このほかにも公式ドキュメントでは例えば次のような内容も説明されています。分かりやすく記載されていますので、困ったときには公式ドキュメントを参照するのが良いです。

  • (immutable ではなく)mutable なデータクラスを生成するときの書き方
  • データクラスに独自のメソッドを持たせたいときの書き方

2-2. build_runner を実行する

ターミナルにて以下のコマンドを実行します。

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

すると、person.dart の隣に person.freezed.dart および person.g.dart が自動生成されており、次のようにファイルが配置されているはずです。

  • person.dart
  • person.freezed.dart
  • person.g.dart

補足:build_runner による自動再生成

watch でコマンドを実行すると build_runner が監視状態となり、ファイルに変更を加えるとそれを自動検知してデータクラスを再生成してくれます。

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

3. Freezed で生成されたデータクラスを使用する

試しに main.dartmain() の中で確認をしてみると以下になります。

import 'model/person.dart';

void main() {

  // 確認ここから

  const jane = Person(firstName: 'Jane', lastName: 'Doe', age: 20);

  print(jane.firstName); // Jane
  print(jane.age); // 20
  print(jane.toString()); // Person(firstName: Jane, lastName: Doe, age: 20)
  print(jane.toJson()); // {firstName: Jane, lastName: Doe, age: 20}

  final agedJane = jane.copyWith(age: 80);

  print(agedJane.firstName); // Jane
  print(agedJane.age); // 80
  print(jane == agedJane); // false

  // 確認ここまで

  runApp(const MyApp());
}

ここまでで基本的なフローは以上です。

以降はおまけで Tips をご紹介します。

4. Freezed と json_serializable で生成されるファイルの場所を変える

開発が進み Freezed で生成するファイルが多くなってくると model ディレクトリの中身がごちゃつきがちです。

その対策として、自動生成されるファイルの作成先を変更してみたいと思います。

まずはプロジェクト直下に build.yaml というファイルを作成し、そのファイルに下記を記述してください。

targets:
  $default:
    builders:
      freezed:
        options:
          build_extensions:
            'lib/{{dir}}/{{file}}.dart': 'lib/{{dir}}/generated/{{file}}.freezed.dart'
      source_gen|combining_builder:
        options:
          build_extensions:
            'lib/{{dir}}/{{file}}.dart': 'lib/{{dir}}/generated/{{file}}.g.dart'

その後 source_gen というパッケージを追加でインストールします。

flutter pub add --dev source_gen

そして lib/model/person.dartpart 部分を更新します。

// This file is "lib/model/person.dart"
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

// required: associates our `person.dart` with the code generated by Freezed
part 'generated/person.freezed.dart'; // 👈 変更する。変更前は 'person.freezed.dart' だった。
// optional: Since our Person class is serializable, we must add this line.
// But if Person was not serializable, we could skip it.
part 'generated/person.g.dart'; // 👈 変更する。変更前は 'person.g.dart' だった。

@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;

  factory Person.fromJson(Map<String, Object?> json) => _$PersonFromJson(json);
}

そして build_runner のコマンドを実行すると、今度は以下のようにファイルが作成されるはずです。

lib/
 └ model/
    ├ person.dart
    └ generated/
       ├ person.freezed.dart
       └ person.g.dart

こうすることで model ディレクトリ直下が整理され、見通しが良くなりました。

参考:build.yaml の設定について

https://github.com/dart-lang/build/blob/master/docs/writing_a_builder.md#configuring-outputs

補足:IDE の設定で非表示にすることもできる

生成されるファイルの出力先を変える他にも、IDE の設定からファイルを非表示にすることで、見かけ上のファイルを消すことができます。

例えば VS Code の settings.json にて files.exclude という設定を追加すれば Freezed と json_serializable で生成されたファイルを非表示にすることが可能です。

特定のプロジェクトだけで非表示にしたい場合は、プロジェクトの直下に以下のようにファイルを作成しましょう。

.vscode/settings.json

{
  "files.exclude": {
    "**/*.freezed.dart": true,
    "**/*.g.dart": true
  }
}

5. Freezed と json_serializable のビルド速度を改善する

ファイル数の増加に伴ってビルドに時間がかかるようになりますが、ビルド対象のファイルの場所を指定してあげることで、ビルドの速度が大幅に改善されます。

build.yaml を以下のように変更します。

targets:
  $default:
    builders:
      freezed:
        generate_for:
          include:
            - lib/model/*.dart
        options:
          build_extensions:
            'lib/{{dir}}/{{file}}.dart': 'lib/{{dir}}/generated/{{file}}.freezed.dart'
      json_serializable:
        generate_for:
          include:
            - lib/model/*.dart
      source_gen|combining_builder:
        options:
          build_extensions:
            'lib/{{dir}}/{{file}}.dart': 'lib/{{dir}}/generated/{{file}}.g.dart'