Flutterをやっていて、ずっと悩み続けるのが状態管理だと思います。
この記事では、いろいろな状態管理手法を使って同じ機能を実装し、それぞれの方法のメリット・デメリットをまとめていこうと思います。
Flutterアプリとして動作させる性質上どうしてもコードが多少長くなって読むのが面倒になりますので
記事内で取り上げるのは重要部分だけにとどめ、コード全体はGithubにあげておきますので適宜参照してください。Flutterプロジェクトまるごと上がっているのでcloneやダウンロードすれば動きます。
それぞれの実装パターンはmy_app_x.dartという形でファイル別になってます(xに数字が入る)
作るアプリの構造
こんな感じにします。
機能
buttonを押すと、countが1ずつ増えていきます。それだけです。
buttonとcountはWidgetツリー内の離れた位置にあるので、状態管理手法が必要になります。
WidgetDが「WidgetD」という文字を出力しており、この文字は変更されません。
各ウィジェットの役割
MaterialAppとScaffold
MaterialAppとScaffoldはアプリとしての体裁を整えるためにあるだけで大して意味はないです。
MyAppX
MyAppX以下の部分は実装が毎回代わり、
MyApp1→グローバルキーを利用する方法
MyApp2→setState()用関数を渡していく方法
のような感じで、番号を変えながら色々なパターンを作ってそれぞれScaffoldの下にくっつけ、比較してみます。
WidgetA〜WidgetD
WidgetA〜WidgetDは、build関数内にprint('WidgetA is built.');のように書いておき、build関数が走るたびにコンソールにメッセージが出るようにします。これで各Widgetがリビルドされたかどうかがわかります。
方針
状態管理手法を決めた所で、できるコードは一通りではありません。ですが、ある程度方針は決めておきましょう。
WidgetA〜WidgetDはできるだけリビルドされないようにする。
理想的には、リビルドされる必要があるのはcount部分の文字だけです。WidgetでいうとTextになります。
ですが、それがやりにくい場合は、リビルドされるWidgetを極力少なくします。
constは使わない
今回は、それぞれの構造に対して何がリビルドされるのが把握したいので、constは使わないことにします。
constを使うことでも無用なリビルドを防ぐことができるので、普段は使える限りどんどん使うべきですが
今回は、構造のおかげでリビルドを防げたのか、それともconstのおかげなのか、わからなくなるとまずいので、constは使わないことにします。
では前置きはここまでにして、各種構造の比較に参りましょう。
0. グローバル変数としてStateを持つ(非推奨)
WidgetCをStatefulWidgetにして、そのStateをグローバル変数にします。
そうすれば、どこからでもアクセスできますから、WidgetAから遠隔でアクセスして更新するのも楽勝ですね。
コードはこんな感じ。
class WidgetC extends StatefulWidget {
WidgetC({this.child});
final Widget child;
@override
// グローバル変数であるStateを返す
_WidgetCState createState() => _widgetCState;
}
// Stateをグローバル変数として持つ
_WidgetCState _widgetCState = _WidgetCState();
class _WidgetCState extends State<WidgetC> {
int _count = 0;
void increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
print('WidgetCState is built.');
return Column(
children: <Widget>[
Text(_count.toString()),
widget.child,
],
);
}
}
WidgetCのchildには、親からあらかじめ作ったWidgetDのインスタンスを渡しておくことで、WidgetCのリビルド時にWidgetDまでリビルドされるのを防ぎます。
操作する側のWidgetAはこんな感じ
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('WidgetA is built.');
return RaisedButton.icon(
icon: Icon(Icons.plus_one),
label: Text('plus 1'),
onPressed: () {
// グローバル変数 _widgetCStateが持つ更新関数を直接呼ぶ
_widgetCState.increment();
},
);
}
}
メリット
- リビルドされるのはWidgetCのStateだけ
- 何をやってるかわかりやすい
- どこからでも自由に触れる
デメリット
- どこからでも触れる→誰が触ってるかわからない→デバッグ困難
- グローバル変数は不要になってもずっと保持されてパフォーマンスがイマイチ
- 一般的なグローバル変数のデメリットそのまま
グローバル変数を使うとコードがしっちゃかめっちゃかになりデバッグが困難になります。残念。
1. 更新対象にグローバルキーを持たせる(非推奨)
FlutterにはKeyという概念がありまして、これを使ってWidgetのインスタンスを区別することができます。
GlobalKeyはアプリ全体を通してユニークであることが保証され、逆にGlobalKeyから、それを付与されているWidgetにアクセスできます。
// WidgetCに渡すためのGlobalKeyを生成しておく
GlobalKey<_WidgetCState> _globalKey = GlobalKey<_WidgetCState>();
// (中略)
// WidgetCに生成したGlobalKeyを渡しておく
WidgetB(child: WidgetC(child: _widgetD, key: _globalKey)),
class WidgetA extends StatelessWidget {
// (中略)
onPressed: () {
// GlobalKeyを使って、更新関数を直接呼ぶ
_globalKey.currentState.increment();
},
メリット
- どこからでも触れる
- リビルドされるのはWidgetCのStateだけ
デメリット
- 誰が触ってるかわからない→デバッグ困難
- GlobalKey自体の動作コストが高い
ということでグローバル変数のデメリットが解決してません。
2. ancestorStateOfType(非推奨)
グローバルなんたらと名のつくものをむやみに使うのはあまり良くないということがわかりました。
ちゃんとWidgetツリーをたどって状態管理をすることにしましょう。
ということで、ツリー内のどこかで状態を持つ必要があります。状態というのは、今回の場合は、ボタンを押した回数ですね。
ボタンからも、表示用Widgetからもアクセスできなきゃいけないので、MyAppXの下に持つことにします。
MyAppXの直下にあるWidgetHogeに持ってもらうことにします。
HogeはStatelessWidgetだったりStatefulWidgetだったりします。
また、Hogeが直接_counter変数を持つこともあれば、_counterを含む別のクラスを持つこともあります。
今回は、HogeはStatefulWidgetにして、_counterを直接持つことにしましょう。
WidgetCは単体でリビルドされるわけではないので、StatelessWidgetにしておきます。
class HogeStatefulWidget extends StatefulWidget {
@override
_HogeStatefulWidgetState createState() => _HogeStatefulWidgetState();
}
class _HogeStatefulWidgetState extends State<HogeStatefulWidget> {
// WidgetAとWidgetDのコンストラクタはbuild()内で呼ばないので、setState()時にリビルドされない
final WidgetA _widgetA = WidgetA();
final WidgetD _widgetD = WidgetD();
int _counter = 0;
void increment() => setState(() => _counter++);
// (略)
WidgetAはなんとかしてこのincrement()を呼べば良いわけです。
そこで、context.ancestorStateOfType()というメソッドを使います。
これは、contextの地点からWidgetツリーを先祖に向かって遡って、該当する型を持つ最初のStateを見つけて返します。
Flutterでは、このメソッドを呼ぶためのメソッドを、見つける対象であるStateの方にofという名前で持たせるのが一般的でしょう。
class _HogeStatefulWidgetState extends State<HogeStatefulWidget> {
//(中略)
// 与えられた「context」の地点からWidgetツリーを遡ってこのStateを見つけてくるstatic関数
static _HogeStatefulWidgetState of(BuildContext context) =>
context.ancestorStateOfType(TypeMatcher<_HogeStatefulWidgetState>());
}
呼ぶ側はこう。
class WidgetA extends StatelessWidget {
//(略)
// ここからWidgetツリーを遡ってStateを探し、increment()を呼びます。
onPressed: () => _HogeStatefulWidgetState.of(context).increment(),
//(略)
}
メリット
- グローバル変数を使っていない
- Widgetツリーを遡ってStateを見つけに行く、という動作が理解しやすい
デメリット
- Widgetツリーを遡るのに
O(N)のコストがかかる - 中間層の
WidgetBまでリビルドされる - 呼ぶ側のWidgetが別ファイルに定義されている場合、Stateの型を指定するためにimportが必要になるので、Stateの定義をプライベートにできない
中間層のWidgetBのリビルドは、うまくやれば防げるのかもしれないですが、思いつきませんでした。
この方法がFlutter関連の文章で推奨されているのを見たことがないです。
一番致命的なデメリットは1つ目のO(N)のコストがかかる所です。
O(1)のコストでアクセスできる方法がある(後述)のだから、それを使いましょうということです。
3. 更新用メソッドをコンストラクタ経由で順次渡していく
「素朴なsetState()パターン」のような言い方でたまに言及されるのがこのやり方です。
HogeWidgetは引き続きStatefulWidgetとします。
一つ前のやり方(2.)の時は、WidgetAの側から上に向かって遡ってincrement()にアクセスしましたが、今回は上から下にincrement()そのものを渡してしまいます。
ということはWidgetAはincrement()を受け取れるような形につくっておく必要があります。
class WidgetA extends StatelessWidget {
// WidgetAはコンストラクタでincrement関数を受け取れる
WidgetA({this.incrementer});
final void Function() incrementer;
@override
Widget build(BuildContext context) {
print('WidgetA is built.');
return RaisedButton.icon(
//(略)
// すでに受け取っているincrement関数を呼ぶだけ
onPressed: () => incrementer(),
);
}
}
親であるHogeの側はこんな感じ
class _HogeStatefulWidgetState extends State<HogeStatefulWidget> {
// _widgetAに最初からincrement関数を渡すのは不可能なので、initState内で初期化する
// (↑ initializerの中ではstaticメソッドしか渡せないから)
WidgetA _widgetA;
void increment() => setState(() => _counter++);
void initState() {
super.initState();
// WidgetAのコンストラクタにincrement関数を渡しておく
_widgetA = WidgetA(incrementer: increment);
}
//(関係ない所略)
メリット
-
O(1)で更新メソッドにアクセス可能
デメリット
- 中間層
WidgetBもリビルドされる -
Hogeからボタンまでの全ての中間Widgetに、コンストラクタで更新用メソッドを渡していく必要があり地獄
Widgetツリーがある程度深くなるととてもやってられないです。
先ほどと同じく、中間層WidgetBをリビルドしない方法があるかもしれませんが思いつかなかったです。
4. InheritedWidget
Flutterで理解に苦しむ要素の一つ、InheritedWidgetです。
InheritedWidgetには、データをWidgetツリーの下層に流してO(1)でアクセスできるようにする機能と、変更通知機能があります。
下層では、データの変更通知を受け取ってリビルドを実行することができるわけです。
理解したくていろいろ読んでるけどよくわからないという人は、次のポイントを押さえるといいと思います。
- InheritedWidgetを継承したクラスを自分で作って使う。
- 一度作られたInheritedWidgetは不変
- 状態更新の時は、新しいInheritedWidgetが作られて古いものと置き換えられる
- 置き換えの際に古いものとの比較が行われ、更新を通知するかどうか判断される
- その判断基準は自分で書く(
updateShouldNotify)
ということで、InheritedWidgetを作ったり交換したりする係をHogeStatefulWidgetとそのStateにやってもらいます。
class _HogeStatefulWidgetState extends State<HogeStatefulWidget> {
//(略)
@override
Widget build(BuildContext context) {
// setStateが呼ばれるたびにこのbuild関数が呼ばれ、新しい_HogeInheritedWidgetが作られて古いものと交換される
return _HogeInheritedWidget(
counter: _counter,
child: Column(
children: <Widget>[
_widgetA,
_widgetB,
],
),
);
}
}
//肝心のInheritedWidget
class _HogeInheritedWidget extends InheritedWidget {
_HogeInheritedWidget({Widget child, this.counter}) : super(child: child);
final int counter;
@override
bool updateShouldNotify(_HogeInheritedWidget oldWidget) =>
oldWidget.counter != counter;
static _HogeInheritedWidget of(BuildContext context) =>
context.inheritFromWidgetOfExactType(_HogeInheritedWidget);
}
//(略)
class WidgetC extends StatelessWidget {
//(略)
@override
Widget build(BuildContext context) {
//(略)
// ここでWidgetCが_HogeInheritedWidgetのリビルド対象として登録される
Text(_HogeInheritedWidget.of(context).counter.toString()),
],
);
}
}
メリット
- 中間層を経由してデータを渡さなくても下層からいきなりStateにアクセスできる
- O(1)でそれができる
- 中間層の
WidgetBがリビルドされない!
デメリット
- わかりにくい。
更新用メソッドもInheritedWidgetで渡せる
これよく見ると実は、「更新用メソッドをコンストラクタで順次渡していく」という面倒がまったく解決してません。
それに関しては、_HogeInheritedWidgetに_HogeStatefulWidgetStateそのもの(this)を持たせて、次のように呼び出せば解決します。
(context
.ancestorInheritedElementForWidgetOfExactType(_HogeInheritedWidget)
.widget as _HogeInheritedWidget)
.hogeStatefulWidgetState
.increment();
呼び出しのコードが長くなるのが嫌な場合は_HogeInheritedWidgetの側にof2などとしてもう一つofを定義しておけば良いです。ofの引数を増やして呼び出すメソッドを分ける方法もあります。
githubのソースコードのmy_app_4_2.dartにそのバージョンを作りましたのでご確認ください。
(参考にした記事:InheritedWidget/InheritedModelとは何か)
なお、InheritedWidgetの使い方について詳しく説明した記事を書きましたので是非ご覧ください。
InheritedWidgetの目的と使い方【Flutter】
5. ChangeNotifier
Flutterには、ChangeNotifierという仕組みがあります。
ChangeNotifierはWidgetではありません。状態管理クラスとして使います。
ということで、長らくStatefulWidgetだったHogeをStatelessWidgetに戻し、状態管理クラスとしてChangeNotifierを継承したクラスを持たせましょう。
これは先ほどと同じ図ですが、今回は生の_counterではなく、状態管理クラスを持つわけですね。
Widgetツリーの下層からこの状態管理クラスにアクセスする方法としては、また以前のように、状態管理クラスをコンストラクタで渡していくことにしましょう。
class HogeWidget extends StatelessWidget {
// 状態管理クラスとしてChangeNotifierを継承したクラスを持つ
final _HogeChangeNotifier _hogeChangeNotifier = _HogeChangeNotifier();
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
// 下層にはコンストラクタを経由して渡していく
WidgetA(_hogeChangeNotifier),
WidgetB(_hogeChangeNotifier),
],
);
}
}
// 肝心のChangeNotifier
class _HogeChangeNotifier extends ChangeNotifier {
int _counter = 0;
void increment() {
_counter++;
// increment()が呼ばれると、Listenerたちに変更を通知する
notifyListeners();
}
}
更新通知を受け取るのはWidgetCだけでいいので、WidgetCをStatefulWidgetにして、その更新用関数をaddListenerで_HogeChangeNotifierに登録します。
class WidgetC extends StatefulWidget {
WidgetC(this._hogeChangeNotifier, {this.child});
final _HogeChangeNotifier _hogeChangeNotifier;
//(略)
}
class _WidgetCState extends State<WidgetC> {
void rebuildC() => setState(() {});
@override
Widget build(BuildContext context) {
// listnerとしてリビルドを登録したいので、WidgetCをStatefulWidgetにしてsetStateをlistner登録
widget._hogeChangeNotifier.addListener(rebuildC);
return Column(
children: <Widget>[
Text(widget._hogeChangeNotifier._counter.toString()),
//(略)
// 古い_WidgetCStateが捨てられるたびに、登録した古いlistenerを解除しておく
@override
dispose() {
widget._hogeChangeNotifier.removeListener(rebuildC);
super.dispose();
}
}
メリット
- 中間層
WidgetBがリビルドされない -
addListenerとnotifyListenersを使って、通知を受け取る人や通知を送るタイミングの操作が自由自在
デメリット
- ChangeNotifierを渡す方法は別途考える必要がある
「ChangeNotifierを渡す方法」としてはInheritedWidgetが使えると思います。でも、InheritedWidget自体に変更通知機能があることを考えると、ChangeNotifier + InheritedWidgetという組み合わせに大きなメリットがあるかは疑問…。
6. ChangeNotifierをProviderで下層に渡す(本命)
下の公式解説記事でも「あなたがFlutterの入門者だったり、他の方法を採用する強い動機が別に無いなら、この方法を使うべきでしょう。」と書いてあるほどの大本命です。(2019年9月3日現在)
この方法にはProviderパッケージが必要になるのでpubspec.yamlに加えておいてください。
ChangeNotifierProviderを使って、状態管理クラスであるChangeNotifierをHogeから下層に渡します。
InheritedWidgetの時と同様に、下層からはどこからでもアクセスできるので、コンストラクタ経由で渡していく必要はありません。
class HogeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ここからChangeNotifierを下層に渡す
return ChangeNotifierProvider<_HogeChangeNotifier>(
builder: (_) => _HogeChangeNotifier(),
child: Column(
children: <Widget>[
WidgetA(),
WidgetB(),
],
),
);
}
}
_HogeChangeNotifier自体は先ほどと同じです。
コンストラクタで受け取らなくていいのでWidgetAやWidgetBの設計もシンプルになります。
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton.icon(
// Provider経由でincrement関数を呼ぶ。listen:falseにより、こちらはリビルドされない。
onPressed: () =>
Provider.of<_HogeChangeNotifier>(context, listen: false).increment(),
);
}
}
また、WidgetCもStatelessWidgetでよくなります。
代わりにWidgetCの内部でConsumerを使を使います。
Consumerは、ChangeNotifierProviderから渡されたChangeNotifierを受け取り、notifyListeners()に応答して、自分の子孫をリビルドします。
明示的なaddListener()は不要です。
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
// このConsumerの傘下だけがリビルドされる。
Consumer<_HogeChangeNotifier>(
builder: (_, _HogeChangeNotifier hoge, __) =>
Text(hoge._counter.toString())),
WidgetD(),
],
);
}
}
メリット
-
notifyListenersを使って、通知を送るタイミングの操作が自由自在 - リビルド範囲を最小限に抑えられる。
WidgetCすらリビルドされない - 状態管理クラスに下層のどこからでもアクセスできる
デメリット
- 特になし。
7. BLoC
Flutterの状態管理を調べてると出てくるのがこのBLoCパターンです。
状態管理クラスを使うという点では5.や.6に似ています。
状態管理クラスとしてChangeNotifierではなくBLoCと呼ばれている仕組みを使うのがBLoCパターンです。
ChangeNotifierがFlutterの機能なのに対し、BLoCで用いるStreamはDartの機能なので、他のDartプロジェクトとコードが共有できるというメリットもあります。(現状、他のDartフレームワークなんてAngularDartくらいしかありませんが…)
まずは.5と同様に、状態管理クラスをコンストラクタで渡していく方法を見てみましょう。
HogeWidgetがまたStatefulWidgetになりました。今回はdisposeを実装するため、ちょっと長くなってます。
// Blocが不要になった時にdispose()を呼ぶため、Stateのdispose関数を利用する。そのためのStatefulWidget
class HogeWidget extends StatefulWidget {
@override
_HogeWidgetState createState() => _HogeWidgetState();
}
class _HogeWidgetState extends State<HogeWidget> {
final _HogeBloc _hogeBloc = _HogeBloc();
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
// 下位層のコンストラクタに随時Blocを渡していく
WidgetA(_hogeBloc),
WidgetB(_hogeBloc),
],
);
}
@override
dispose() {
_hogeBloc.dispose();
super.dispose();
}
}
肝心のBLoCはこちら。
class _HogeBloc {
_HogeBloc() {
// 初期値は最初に流しておく
_counterController.add(_counter);
// incrementにnullが投げ入れられたら、_counterを1増やしてStreamに流す
_incrementController.stream.listen((_) {
_counter++;
_counterController.add(_counter);
});
}
int _counter = 0;
// 各種Controllerと、Stream・Sinkのgetterを定義
final StreamController<void> _incrementController = StreamController<void>();
final BehaviorSubject<int> _counterController = BehaviorSubject<int>();
Stream<int> get counter => _counterController.stream;
StreamSink<void> get increment => _incrementController.sink;
// Controllerをcloseするためdisposeメソッドも必要
void dispose() {
_incrementController.close();
_counterController.close();
}
}
入力側となるWidgetAはこんな感じ
class WidgetA extends StatelessWidget {
WidgetA(this._hogeBloc);
final _HogeBloc _hogeBloc;
@override
Widget build(BuildContext context) {
return RaisedButton.icon(
// 受け取っているHogeBlocのincrementというSinkにnullを投げ入れる
onPressed: () => _hogeBloc.increment.add(null),
);
}
}
出力を受け取るWidgetCはこんな感じ。
BLoCの出力であるStreamに応答してStreamBuilderが中身をリビルドします。
class WidgetC extends StatelessWidget {
WidgetC(this._hogeBloc);
final _HogeBloc _hogeBloc;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
// このStreamBuilderの傘下だけがリビルドされる
StreamBuilder(
// 受け取っているHogeBlocのStreamに応答してリビルド
stream: _hogeBloc.counter,
builder: (_, AsyncSnapshot snapshot) =>
Text(snapshot.data.toString()),
),
// (略)
}
メリット
- 出力側のことを気にせず好きなタイミングで
Sinkにデータを入力できる - リビルド範囲を最小限に抑えられる
- Flutter以外のDartプロジェクトとコードが共有できる
デメリット
- BLoCの受け渡し方法を別途考える必要がある
- StreamとSinkの理解が必要
- BLoCをdisposeするためだけにStatefulWidgetが必要
- IDEで定義にジャンプするとStreamに飛んでしまい、処理自体に飛べない(処理自体はStreamのリスナーを登録した所に書いてある)
8. BLoCをProviderで下層に渡す(本命)
ということで、やっぱり状態管理クラスを下層に渡すならProviderです。
やはりproviderパッケージを使用します。
今回はChangeNotifierProviderではなく単なるProviderを使います。
BLoCクラス自体は先ほどと同じです。
HogeWidgetはまたStatelessWidgetになります。
disposeはどうすんの? と思ったかもしれませんが、それもProviderがうまいことやってくれます。
class HogeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ここのProviderからBLoCを下層に渡します
return Provider<_HogeBloc>(
builder: (_) => _HogeBloc(),
dispose: (_, _HogeBloc bloc) => bloc.dispose(),
child: Column(//(略)
}
disposeを登録したので、このProviderがWidgetツリーから除去される際にきちんとdisposeを呼んでくれます。
入力側のWidgetAはこんな感じ
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton.icon(
// Provider経由で、HogeBlocのincrementというSinkにnullを投げ入れる
onPressed: () => Provider.of<_HogeBloc>(context).increment.add(null),
);
}
}
出力を受け取るWidgetCはこんな感じ。
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
StreamBuilder(
// Provider経由で受け取っているStreamに応答してリビルド
stream: Provider.of<_HogeBloc>(context).counter,
builder: (_, AsyncSnapshot snapshot) =>
Text(snapshot.data.toString()),
),
WidgetD(),
],
);
}
}
StreamBuilderに渡すStreamの入手方法が変わっただけですね。
メリット
- 出力側のことを気にせず好きなタイミングで
Sinkにデータを入力できる - リビルド範囲を最小限に抑えられる
- Flutter以外のDartプロジェクトとコードが共有できる
- 状態管理クラスに下層のどこからでもアクセスできる
デメリット
- StreamとSinkの理解が必要
- コード量増えがち
- IDEで定義にジャンプするとStreamに飛んでしまい、処理自体に飛べない(処理自体はStreamのリスナーを登録した所に書いてある)
9. (追記)ValueNotifierとValueListenableProvider
ValueNotifierは、その中にvalueという変数を一つ持ち、それが変更された場合に自動検知してそれを周囲に伝えるという機能があります。
Providerを使わない場合は、ValueListenableBuilderというWidgetでValueNotifierを(コンストラクタ経由やInheritedWidget経由など、なんらかの方法で)受け取り、変更に応答してリビルドされるようにできますが、
代わりに、ValueListenableProviderというWidgetを使うと、ValueNotifierの中身であるvalueを直接子孫に渡すことかできます。valueが変更された場合は子孫Widgetもリビルドされます。
class HogeWidget extends StatelessWidget {
final _HogeValueNotifier _hogeValueNotifier = _HogeValueNotifier(0);
@override
Widget build(BuildContext context) {
// ここからintを下層に渡す
return ValueListenableProvider<int>(
builder: (_) => _hogeValueNotifier,
child: Column(
children: <Widget>[
WidgetA(_hogeValueNotifier.increment),
WidgetB(),
],
),
);
}
}
class _HogeValueNotifier extends ValueNotifier<int> {
_HogeValueNotifier(value) : super(value);
void increment() {
value++;
// この場合 notifyListeners(); は不要。valueの変更は自動で検知される。
}
}
値の受け取り手であるWidgetCはこんな感じ
class WidgetC extends StatelessWidget {
WidgetC(this.child);
final Widget child;
@override
Widget build(BuildContext context) {
print('WidgetC is built.');
return Column(
children: <Widget>[
// ここで、リビルド対象として登録される。
Text(Provider.of<int>(context).toString()),
child,
],
);
}
}
MyApp4でやったInheritedWidgetの時と同じように、WidgetCがまるごとリビルドされます。
なので、WidgetDはあらかじめ作って外から渡すことでリビルドを防いでいます。
つづいて更新ボタンのWidgetAについてですが、
状態管理クラスのインスタンスそのものを下層に渡しているわけではないので、状態を変更するメソッドに直接アクセスできません。
なので、MyApp3でやったように、更新用メソッドを直接WidgetAに渡しています。
class WidgetA extends StatelessWidget {
WidgetA(this.incrementer);
final void Function() incrementer;
// 略
// 既に受け取っているincrementerを呼ぶだけ
onPressed: () => incrementer(),
// 略
}
メリット
- 値だけが渡るので、受け取り手の書き方がシンプル
デメリット
- 更新メソッドへのアクセス方法は別途考える必要がある
- intやStringといったシンプルな値を流したい場合、同じ型の値が複数あると交錯して混ざる
リビルド範囲を最小限にするにはConsumerを使えばいいかもしれませんが、それをやると「受け取り手をシンプルに書ける」というメリットが死にますね。
今の所の僕の理解では、基本的にChangeNotifierで良い気がします。
「一度オンラインやストレージから値を取得したら、基本的に変更しない」という場合には、ValueListenableProviderが良さそう。
同じ型の値が交錯する問題は、以下が実装されれば解決しますね。
typedef for simple type aliases
その他
Redux ・ MobX
どちらもReact界隈で生まれた手法ですかね?
私自身がReactの経験がまったくないため、解説できません!興味ある方はお調べください!
まとめ
ということで、2019年9月現在の本命としては、ChangeNotifierとChangeNotifierProviderを使った方法が良いと思います。
すでにJavaやJavaScriptなどでRxやObservableに馴染みのある人は、BLoCパターンも使ってみると良いと思います。BLoCにデータをポンポン放り込む感じが楽しいです。
Streamに興味のある方は下のリンク先をお読みください。いまだにちょくちょく「いいね」が付くObservableの記事です。JavasScriptの記事ですが、ObservableをStreamと読み替えればそのままDartでも通用します。
また、一部の状態管理手法に「本命」なんて書いてしまいましたが、別にそれ以外の管理手法がまったく悪いわけではありません。
「非推奨」と書いたものですら、状況によっては問題なく使えます。使う場合が想定されてるから用意されているわけですしね。
それから、一つのアプリの中で一つの状態管理手法しか使っていけないということもないですね。状況に応じて使い分けると良いと思います。
でも一つのパターンに固定するのも管理コストが抑えられて良いですけどね。
状態管理手法に「唯一の正解」はないので、「これでいいのかな…?」と不安な人も、決めないと始まりませんので、決断して実装に取り掛かりましょう。


