この記事では、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講座を公開しております。こちらも開発の参考になれば光栄です!
参考
公式サイト
Android StudioでのFlutter開発でfreezed用のLive Templateを準備する
build.yamlによる最適化
新しいメンバー変数を追加したときのエラー
項目を消す方法