前回の投稿(我流Flutter学習ステップ(2)RiverpodとGoRouter)で、「状態」としてオブジェクトを用いる場合、当該オブジェクトはimmutable(不変)として扱うことが必要である点に触れましたが、このようなオブジェクトを簡単に実装する方法としてFreezedなるパッケージを使用する方法があります。
今回はこのFreezedを使ってみたいと思います。
まずは、恒例のBardさんによるFreezedの解説です。
Freezedは、Flutterで使用できるDartのパッケージです。データクラスを生成するためのコードジェネレーターで、データの不変性やシリアライズなどの機能を提供します。
Freezedを使用すると、データクラスを簡単に生成することができます。データクラスは、不変なデータ型です。つまり、一度作成されたデータは変更できません。これは、データの安全性と堅牢性を向上させます。
また、Freezedはシリアライズ機能を提供します。シリアライズとは、データを別の形式に変換することです。Freezedを使用すると、データクラスをJSONやXMLなどの形式にシリアライズすることができます。これにより、データの保存や送信を簡単に行うことができます。
Freezedは、Flutterでデータクラスを扱うための強力なツールです。データの安全性、堅牢性、利便性を向上させることができます。
単に不変性を実現するだけでなく、シリアライズに関連する機能も提供されるようです。
パッケージのインストール
Freezedのインストール方法は以下の通り。
# flutter pub add freezed
Freezed自体のインストールは上記で完了ですが、実はFreezedの対象となるオブジェクトの実装では規定の書式で対象クラスに関する内容を記述した上で「build_runner」なるツールを使用して関連コードを自動生成するという手続きが必要になります。
この「build_runner」を合わせてインストールしておきます。
# flutter pub add build_runner
処理内容
サンプルアプリとしては前回の投稿をそのまま踏襲し、カウンターの状態管理に関する部分のみをFreezedを使用するように書き換えてみたいと思います。
状態(カウンター)定義(lib/state/counter_state.dart)
まず、当該ファイル(lib/state/counter.dart)から状態(CounterState)の定義部分のみを抜き出して、新規にファイル「lib/state/counter_state.dart」を作成します。
内容は以下の通り。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'counter_state.freezed.dart';
@freezed
class CounterState with _$CounterState {
factory CounterState({
required int count,
}) = _CounterState;
}
Freezedの対象とするクラス定義には「@freezed」なるアノテーションを記述します。
これにより「build_runner」に対して該当部分がFreezed関連のコード生成対象であることを示す訳です。
また、上記アノテーションを使用するために「freezed_annotation.dart」を読み込んでいます。
それ以外の部分にも色々と特徴がありますが、この辺は全て約束事として覚えると言うことで良いかと思います。
テンプレート化するのであれば以下のような感じでしょうか。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'counter_state.freezed.dart';
@freezed
class `Class名` with _$`Class名` {
factory `Class名`({
// 状態の内容
}) = _`Class名`;
}
上記の「`Class名`」の部分を適宜書き換え、具体的な状態の内容(状態を構成する各種データの型や名称等)をfactoryの引数として定義すれば状態オブジェクトとしての形が整います。
「build_runner」を実行することで「counter_state.dart」に対して「counter_state.freezed.dart」なるファイルが自動生成されます。
それを読み込む必要がありますが、それが以下の部分です。
part 'counter_state.freezed.dart';
通常、他のファイルを読み込む操作においては「import」を使用しますが、「import」と「part」とは何が違うのでしょう?
非常に大雑把に言えばプライベートな要素に関する参照可・不可の違いのようです。
Flutter(Dart)では識別子(変数、関数、クラスなどの名前)の前にアンダースコア(_)を付けることで、それをプライベートなものとして扱うようになります。
クラス内のプロパティやメソッドをプライベート化した場合、該当するプロパティやメソッドは当該クラス内でしかアクセスできなくなります。
では、グローバル変数をプライベート化した場合はどうなるでしょう?
この場合、基本的には当該グローバル変数は当該ファイル内からしかアクセスできなくなります。つまり、あるファイル(A)内にプライベートグローバル変数が定義されていたとしても、それを「import」した別のファイル(B)からは当該グローバル変数はアクセスできません。
しかし、上記のようなケースでもファイルの読み込み方法を「part」にすることでBからAのプライベートグローバル変数にアクセス可能になります。「import」は単に別のファイルを読み込むことを意味するのに対し、「part」はもともと一つのファイルであったものを必要に応じて分割し、ビルドに際して再結合している、と言った意味の操作と言えます。
なお、partで読み込まれる側(上記例ではA)では自身がどのファイルの一部であるかを示す書式として「part of」の宣言が必要です。
実際に、「counter_state.dart」に対応して生成される「counter_state.freezed.dart」内には以下の記述があります。
part of 'counter_state.dart';
では実際に「build_runner」を実行してみましょう。
# flutter pub run build_runner build
ファイル内の記述に問題がなければ「counter_state.freezed.dart」が生成されるかと思います。
状態(カウンター)管理(lib/state/counter.dart)
先に定義したCounterStateを使用するように状態管理(lib/state/counter.dart)の方も書き換えます。
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'counter_state.dart';
final counterProvider = StateNotifierProvider<CounterNotifier, CounterState>(
(ref) => CounterNotifier());
class CounterNotifier extends StateNotifier<CounterState> {
CounterNotifier() : super(CounterState(count: 0));
increment() {
state = CounterState(count: state.count + 1);
}
}
CounterStateの定義が別ファイル化されたことを除けば書式的には大きくは変わっていません。
ただ、前回の例では状態の本体(count)がCounterStateのプロパティであったため以下のような間違った操作ができていました。
state.count += 1;
Freezedを使用したケースではcountはあくまでコンストラクタの引数であってプロパティとして存在する訳ではないため、上記のような不適切な操作を行おうとしてもエラーになります。
一方、「state.count」を参照することはできています。
CounterStateに「count」なるゲッター(メソッド)が自動で生成されており、これを呼び出して実際の値を取得しているようです。
まとめ
ネットでFreezedに関して調べると「データクラス(何らかのデータを保持することを主目的とするクラス)および同データ処理として必要となる標準的な処理メソッド(JSONエンコード等)を簡易に生成できるパッケージ」というような文脈で語られていることも多く、必ずしも不変性のみが着目されている訳でもなさそうです。
とは言うものの、FreezedがFlutterにおけるimmutable(不変)なオブジェクトの生成方法の代表格であることは間違いありません。
RiverpodのStateNotifierProviderにおける状態実装方法としてはスタンダードと考えて良いでしょう。
今回は状態としてカウンターという1つの数値を扱うだけの例でしたが、実際のプロジェクトで扱われる状態はもっと複雑なものになるかと思います。
今回学習した基本形からの応用範囲かと思いますが、もし特殊な事例が出てきたら改めて記事にしたいと思います。