純粋主義者のためのFlutterの状態管理
※この記事は「Flutter state management for purists」を翻訳したものです。
状態管理は当たり前のことだった
“自分の仕事は自分でしなさい。誰もやってくれないから」。
このようなことを教えられたことはありませんか?私は、特にプログラミングの世界では当たり前のことだと思っていました。特に、「状態管理」(UIに表示すべきデータを管理するという意味で使われています)について考えてみましょう。
例えば、Javaでは、UIに表示されたデータの状態を次のように管理することが私は多いです。
// A listener interface
public interface DataChangeListener {
void onDataChanged();
}
// A model class
public class SampleModel {
private String name;
private String email;
private List<DataChangeListener> listeners = new ArrayList<DataChangeListener>();
public void setData(String name, String email){
this.name = name;
this.email = email;
}
public void addListener(DataChangeListener listener){
this.listeners.add(listener);
}
public notifyAllListeners(){
for(DataChangeListener listener : listeners){
listener.onDataChanged();
}
}
:
}
// A UI class
public class SampleUI implements DataChangeListener{
private SampleModel model;
SampleUI(SampleModel model){
this.model = model;
this.model.addListener(this);
}
:
@overide
public void onDataChanged(){
// update UI with model's data
:
}
:
}
これはモデルとUIを別々に管理するという、昔ながらの、しかし自然な方法です。Visual Basic、C#、C++などで、私はいつもこの方法で状態管理を行ってきました。この程度の仕組みなら、どんなプログラミング言語を使っていても自分で簡単に考案できると思っていたのですが……。
Flutterは違います
私がFlutterを学び始めたのは比較的最近のことです。すぐに、そのスマートなスタイルのアーキテクチャのために、Flutterに惚れ込みました。しかし、Flutterのステート管理を学び始めたときには驚きました。彼らは、”ステート管理パッケージのいずれかを使用する必要がある “と言ったのです。疑問に思いながらも、私は最も人気があり、Googleのお墨付きのパッケージであるProviderを学びました。しかし、深く知れば知るほど、そして使えば使うほど、特に以下のクラス構造を見たときに、何かが間違っていると感じました。
このクラスは、複数の状態をつなぐためのものですが、私にはどう見ても苦肉の策にしか見えませんでした。
その頃、Riverpodという、大雑把に言うとProviderの次のバージョンの話を聞きました。悔しさを晴らすために、私も勉強を始めたのですが、このフローチャートを見て、諦めました。😣(UPDATE:現在はフローチャートは削除されているようです。
私の経験では、どのような状態管理メカニズムも非常にシンプルで理解しやすかったです。しかし、Flutterでは、ProviderやRiverpodだけでなく、状態管理に関するすべてのパッケージが非常に複雑です。だんだんと、これは何かおかしいのではないかと思うようになりました。
ミニマリストのためのFlutterの状態管理!
そんなある日、Suragchさんが書いた「Flutter state management for minimalists」という素晴らしい記事を見つけました。これは私が長い間探していたものでした。彼はFlutterの状態管理についてこう言っています。
選択肢が多すぎるか、舞台裏であまりにも多くの魔法が使われているかのどちらかでした。私の脳は、シンプルでわかりやすいものを求めていました。最小限のものが必要だったのです。
そして、Flutterに内蔵されているStatefulWidget、ValueNotifier、ValueListenableBuilderクラスを使って、状態管理をうまく行うことができると記事の中で説明しています。
私はこのアイデアにすぐに魅了されました。そこで、これらの要素をさらに詳しく調べてみました。この記事の残りの部分では、StatefulWidgetとValueNotifierについての私の洞察と、「純粋主義者のためのFlutterの状態管理」とは何かについて説明しています。
(これらを使ってどのように状態管理を実現するかについては、彼の記事を参照してください)
StatefulWidgetについて
ステートマネジメントパッケージを使用している人は、ステートが変更されると常に自分自身をリビルドするため、StatefulWidgetの使用を避けるべきだと言います。しかし、彼らは StatefulWidget が setState() が呼ばれたときにのみリビルドするという事実や、StatefulWidget には setState() を使用しない別のユースケースがあることを見落としています。
実際、公式ドキュメントではこのように説明されています。
StatefulWidgetsには、主に2つのカテゴリがあります。
1つ目は、State.initStateでリソースを割り当て、State.disposeでリソースを破棄しますが、InheritedWidgetsに依存せず、State.setStateも呼び出さないものです。
だから「ミニマリストのためのFlutterの状態管理」にはこう書かれています。
最後に、ページが最初にロードされたときに何かを初期化する必要がある場合は、ステートフル・ウィジェットを使用します。
ValueNotifierについて
恥ずかしながら、私はその記事を読むまで、状態変化を通知するための非常に便利なクラスであるValueNotifierについて知りませんでした。
ValueNotifierはプリミティブな値のために作られていると勘違いしている人がいるかもしれません。しかし、それは真実ではありません。
ValueNotifierのset valueメソッドは以下の通りです。
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
ご覧のように、クラスのパラメータTは何にでもなれるのです。if文の比較を理解すれば、値が不変である限り、どんなクラスでもTとして使うことができることが分かります。
そう、Flutterは最初からStatefulWidgetとValueNotifierを使ったシンプルでわかりやすい状態管理の方法を提供してくれているのです!
純粋主義者のための😶
「ミニマリストのためのFlutterの状態管理」では、サードパーティのパッケージを使わずに自力で状態管理を行う方法を紹介していました。何度も何度も強調しますが、かなり素晴らしい記事です。しかし、その解決策は、すべてのクラスがモデルクラスやサービスクラスをどこでも使えるようにするために、GetItパッケージを使用しています。シングルトンクラスの代わりにパッケージを使わない理由を説明しています。
その気になれば、GetIt をシングルトンに置き換えることもできます。しかし、シングルトンよりもGetItの方がテストしやすいという利点があります。
確かに、GetItはDartの世界ではシンプルで良いService Locatorのようです。しかし、サードパーティのパッケージを使わずに状態管理ができるのであれば、自分たちでシンプルなService Locatorを作ることができるのではないでしょうか?
シングルトンクラスはテストが難しくなる、というのは本当ですか?
「シングルトンクラスはユニットテストでモックできないから使わないほうがいい」と言う人は多いです。しかし、それは本当でしょうか?私はそうは思いません。
私の個人的な好みですが、テストに多くのモックオブジェクトを使うのは好きとは言えません。なぜなら、(1)テストが複雑になるし、(2)使えば使うほど実世界の動作から遠ざかっていくからです。とはいえ、時にはモックオブジェクトを作ってテストに使わなければならないこともあります。では、シングルトンクラスをモック化するにはどうすればいいのでしょうか?それほど難しいことではありません。以下のコードをご覧ください。
abstract class ASingletonService {
// switch for using mock
static bool useMock = false;
// singleton logic
static ASingletonService? _instance;
static ASingletonService instance() {
_instance ??= useMock ? _ASingletonServiceMock() : _ASingletonServiceImpl();
return _instance!;
}
// interfaces
void methodA();
:
}
class _ASingletonServiceImpl extends ASingletonService{
@override
void methodA(){
// write production code here.
}
}// this class is located in the test code.
class _ASingletonServiceMock extends ASingletonService{
@override
void methodA(){
// write mock code here.
}
}
簡単ですよね。mockプロパティにあなたのmockオブジェクトを設定することで、シングルトンにモックを使用するかどうかを変更できます。このような作業は、GetItなどのサードパーティ製パッケージのインストールやセットアップとさほど変わりません。そして嬉しいことに、それらのパッケージの最新情報やバージョン管理に気を配る必要もありません! 👍
UPDATE: 私のWordpress読者のコメントを受けて、シングルトンテストの実装を外部から注入可能なものに作り変えました。
サンプルプロジェクト
上記のテクニックを実演するために、とても簡単なアプリを作ってみました。このアプリの構成は以下の通りです。
このアプリでは、擬似サーバーから2つの状態(テキストとボタンの色)を取得し、それぞれ2つのウィジェット(ButtonContainerとServerDataWidget)に反映させています。SampleServiceクラスはシングルトンなので、テストコードに1つコードを書くだけで、モックとしての動作を変更することができます(widget_test.dartに既に書いてあります)。
このアプリは、状態管理にサードパーティのパッケージを一切使用していません ぜひ、GitHubでチェックしてみてください。
結論
確かに、Flutterの状態管理パッケージは非常に絶妙です。しかし、どんなパッケージでも、本当に必要なときだけ使うことにはメリットがあります。なるべくパッケージへの依存度を下げて、自分で作れるところは自分で作ってみてはいかがでしょうか。
この記事がその参考になれば幸いです😊。