【Flutter/Dart】状態管理に必要なfreezedパッケージを徹底解説 クラスの作成・生成方法からチップスまで【2022年8月版】

この記事では、Immutableなクラスを作成するパッケージfreezedを解説します。分かっている人には、以下にチートシートを作ってますので、そちらをご覧下さい。
Flutter freezed のチートシート、もとい、知っている人向けのメモ

なぜ必要か、Immutableなクラスとは

Flutterの中でsetStateやriverpodで状態管理をしても、たまに値の変更が検出されず、画面が更新されず、途方に暮れる場合があります。そのときに最初に疑って欲しいのが、値の変更が不可能になるImmutableクラスを使っているか、です。

  • Immutalbeクラス: 値変更ができないクラス。値を変更する場合、新しいインスタンスを生成する必要がある
  • Mutableクラス : 変更ができるクラス。メソッドなどで値を変更する。

Flutterの状態管理では、参照先の「インスタンスが変わったか」によって、値の変更を検出します。しかし、インスタンス内で値が変わっても、そのインスタンスが変更されないため、状態管理としては、「値は変わっていない」と判断されます。分かりづらいので、例を出しましょう。

setState((){
	instance.setValue(1);
});

上のような書き方では、検出できません。以下のように書く必要があります。

setState((){
	instance = NewInstance(1);
});

新しいインスタンスを作成して、StatefuleWidgetのメンバー変数に設定すれば、変更が検出され、画面が更新されます。
これだけのソースでは間違えませんが、これがソースの量が多くなったり、Listを使い始めると、ハマったりします。記事にもするソースだったので、なるべくライブラリを使用しないようにしようとfreezedを使わずに作ったら、Listで変更が検知されず、動かなくて途方に暮れました。諦めてfreezedを使用してImmutableクラスにすると、あっさり動きました。画面に変更が期待される場合、ハマる前に使用した方が無駄な時間を過ごさなくて済むので、最初の段階から入れた方が良いかと思っています。
また、Immutableなクラスを作成するだけでなく、より便利に使えるよう以下も自動生成してくれます。

  • copyWith:新しいインスタンスの作成時に、ベースのインスタンスを一部変数を更新してコピーする
  • ==: 全てのメンバー変数が同じ事を確認する
  • hashCode: メンバー変数が全て同じか手早く判断するためのコード
  • Jsonのデコード、エンコード

インストール方法

ターミナルで以下を1行ずつ実行します。ただし、5行をまとめて実行できません。

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

作成するクラスの解説

それでは、具体的にどのように作成するか見ていきます。

ソースのテンプレート

ソースのテンプレートは以下です。templateとTemplateを、自分の作成するクラスに合わせて修正してください。また、便利に使えるよう、最後の方にLiveTemplate(stlでStatelessWidgetのテンプレートが表示されるやつ)の登録方法も記載します。

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'template.freezed.dart';
part 'template.g.dart';

@freezed
class Template with _$Template {
  const factory Template({
    required String title,
  }) = _Template;

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

実際のコード

それでは、実際のコードを見てみましょう。色々なオプションも追加しておきます。

@freezed
class Authore with _$Authore{
  @Assert('(0 < famousBook.length) || (0 < books.length)',
      'famousBookかbooksのどちらかは設定する必要がある')
  const factory Authore({
    // requiedか?をつける
    required String name,
    int? age,
    @Default('') String famousBook,
    // ListもMapも読み込める。素晴らしい!
    List books,
    // オブジェクトもJson読込が定義されていれば、入れられる
    required Map<String, ResultInfoData> resultInfo,
  }) = _WorkbookData;
}

メンバー変数の定義

引数には以下のどれかをつける

  • required: Nullが許与されないメンバー変数につける
  • ?:Nullでもよいメンバー変数につける
  • @Default: 引数がnullだった場合、代入する値を設定する
  • @Deprecated: 非推奨のメンバー(reuiredや?は必要)

Assert

作成されたメンバー変数の条件を定義できる。
第一引数の式が展開される。ただの文字列のため、コード生成時にはエラーにならず、生成したソース内でコンパイルエラーが発生する。
(0 < famousBook.length) || (0 < books.length)
というのは、famousBookかbooksのどちらかが0以上の長さであることが条件です。

コード生成

通常のコード生成

とりあえず実行すると、初回は必ず実行される

flutter pub run build_runner build

コードの生成(強制)

生成されるファイルで消える可能性があるときは、「通常のコード生成」ではエラーができる場合がある。そのときに実行する。ただ、生成するファイルをエディタで開いている、などロックを掛けていると、生成はされない。

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

変更を監視してコード生成

ファイルを保存したら、自動でbuildされる。細かく何度も修正する場合に重宝する。

flutter pub run build_runner watch

Jsonの読み書き

リストでないJsonの読込

var file = File('assets/data.json');
WorkboodData workboodData =
	WorkbookData.fromJson(json.decode(file.readAsStringSync()));

リストのJsonの読込

var file = File('assets/list.json');
List jsonData = json.decode(file.readAsStringSync());
List overviewData =
	jsonData.map((data) => OverviewData.fromJson(data)).toList();

以下のようにまとめて書いたら、エラーになった。

json.decode(file.readAsStringSync()).map((data) => OverviewData.fromJson(data)).toList();

Jsonの書込

var output = File('test/result/test.json');
output.writeAsStringSync(json.encode(copy));

メソッドを追加する

メソッド、といっても、freezedはImmutableなので、内部の値を変更するメソッドは書けません。読み取り専用や新しいインスタンスを返すメソッドになります。その場合、内部コンストラクタを作れば良いです。

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';

part 'fraction.freezed.dart';
part 'fraction.g.dart';

@freezed

///分数
class Fraction with _$Fraction {
  const Fraction._(); // これ。
  const factory Fraction({
    ///分母
    required int numerator,

    ///分子
    required int denominator,
  }) = _TestData;

  factory Fraction.fromJson(Map<String, dynamic> json) =>
      _$FractionFromJson(json);

  void printValue() {
    print('$denominator/$numerator');
  }

  Fraction setDenominator(int denominator) {
    return copyWith(denominator: denominator);
  }
}

Live Template

使い始めると、何度もテンプレートをコピペすることになった。手を抜くために、Android Studioの機能のLive Templateを使用して、一発で書けるようにします。ついでにクラスに対応したJsonConverter(後で詳しく書きます)も作成するようにします。
まず、Android Studioで以下を入力する「File->Settings (Mac では、Preferences) -> Editor -> Live Templates -> Flutter」を選択して「+ -> LiveTemplate」を選択する。
Abbreviant: frz
Description: New freezed class
Applicable in dart: Dart -> top-level

Template text:

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
import 'dart:convert';

part '$FILE_NAME$.freezed.dart';
part '$FILE_NAME$.g.dart';

@freezed
class $CLASS_NAME$ with _$$$CLASS_NAME$ {
  const $CLASS_NAME$._(); //メソッド不要の場合、削除
  const factory $CLASS_NAME$({
    required String title,
  }) = _$CLASS_NAME$;

 factory $CLASS_NAME$.fromJson(Map<String, dynamic> json) => 
        _$$$CLASS_NAME$FromJson(json);
}
class $CLASS_NAME$Converter implements JsonConverter<$CLASS_NAME$, String> {
  const $CLASS_NAME$Converter();

  @override
  $CLASS_NAME$ fromJson(String jsonData) {
    return $CLASS_NAME$.fromJson(json.decode(jsonData));
  }

  @override
  String toJson($CLASS_NAME$ object) {
    return json.encode(object.toJson());
  }
}

Edit Variables:

Name FILE_NAME CLASS_NAME
Expression fileNameWithoutExtension() underscoresToCamelCase(String)
Default value fileNameWithExtension() capitalize(underscoresToCamelCase(fileNameWithoutExtension()))
Skip if defined check check

JsonConverterとは。freezedで自作クラスをメンバー変数に持ちたいとき

freezedで自動作成するクラスのメンバー変数に自作クラスがある場合、そのままではJsonへの変換が作成されない。自作クラス用のJsonConverterが必要になる。メンバー変数にアノテーションでJsonConverterを指定する。
以下はその例。
Test: Freezedで生成するクラス
TestObject: 自作クラス
TestObjectConverter: 自作クラスのJson変換を実施するクラス

@freezed
class Test with _$Test {
  const factory Test({
    @TestObjectConverter() required TestObject testObject,
  }) = _Test;

  factory Test.fromJson(Map json) => _$TestFromJson(json);
}

class TestObject {
  final String value;
  TestObject(this.value);
}

class TestObjectConverter implements JsonConverter {
  const TestObjectConverter();

  @override
  TestObject fromJson(String value) {
    return TestObject(value);
  }

  @override
  String toJson(TestObject value) {
    return value.value;
  }
}

ファイル数が多くなったときの対策

プロジェクト全体のファイル数が多くなると、freezedのファイルを探すのに時間が掛かるようになります。そこでfreezedのファイルを特定のディレクトリに置いて、以下のファイルを作成して、build対象のファイルを狭くします。
<プロジェクトのルート>/build.yaml

targets:
  $default:
    builders:
      freezed:
        generate_for:
          include:
            - lib/data/*.dart
      json_serializable:
        generate_for:
          include:
            - lib/data/*.dart
        options:
          any_map: true
          explicit_to_json: true

この場合、data直下のみなので、*.dartと書いてます。その下にもディレクトリがある場合「lib/data/**/*.dart」のようなワイルドカードを使います。

Json変換の設定

jsonの項目名を変更する

Jsonに出力するときに、デフォルトでは変数名が項目名になる。以下のように設定すると、特定の文字列を項目名にできる。

 @JsonKey(name: 'phone_number') required String phoneNumber,

特定の項目を出力しない

クラスの外にnullに変換するtoJson用の関数を定義し、値がnullの場合はJsonに出力しないように設定する。そうすると、指定の項目がJsonに出力されなくなった。

Function? toNull(_) => null;

@freezed
class LoginUser with _$LoginUser {
  const factory LoginUser({
    @JsonKey(toJson: toNull, includeIfNull: false) required List cart,
  }) = _LoginUser;

}

(蛇足) @JsonKey(ignore: true) でいけるかと思ったが、ソース生成で失敗するようになったので、使えなかった。

発生したエラー

新しいメンバー変数を追加したときのエラー

error: The redirected constructor '_SettingData Function({String deviceId, String userId})' has incompatible parameters with 'SettingData Function({String deviceId, bool rememberPassword, String userId})'. (redirect_to_invalid_function_type at [bell] lib\data\setting_data.dart:17)

rememberPasswordというメンバー変数を追加したときに発生した。Android Studioを再起動したら消えた。

type ‘UnspecifiedInvalidResult’ is not a subtype of type ‘LibraryElementResult’ in type cast

ソース生成時に以下のエラーが発生した。

type 'UnspecifiedInvalidResult' is not a subtype of type 'LibraryElementResult' in type cast
[SEVERE] json_serializable:json_serializable on lib/provider.dart:

pubspec.lockを削除→flutter clean→flutter pub getという、いつもの動作?で直った。

まとめ

ということで、改めて、freezedについてまとめました。まとめるのに際して、改めて検索しましたが、ついでに自分でもLiveTemplateやbuild.yamlも導入しました。 あなたの参考になれば、嬉しいです。
また、FreezedとRiverpodをつかって、FlutterでMVVMパターンを実現するWEB講座を公開しております。こちらも開発の参考になれば光栄です!

さくしんのUdemyのFlutter講座【クーポンコード付】

参考

公式サイト
Android StudioでのFlutter開発でfreezed用のLive Templateを準備する
build.yamlによる最適化
新しいメンバー変数を追加したときのエラー
項目を消す方法