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()パターン」のような言い方でたまに言及されるのがこのやり方です。
Hoge
Widgetは引き続き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
と同様に、状態管理クラスをコンストラクタで渡していく方法を見てみましょう。
Hoge
Widgetがまた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クラス自体は先ほどと同じです。
Hoge
Widgetはまた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
でも通用します。
また、一部の状態管理手法に「本命」なんて書いてしまいましたが、別にそれ以外の管理手法がまったく悪いわけではありません。
「非推奨」と書いたものですら、状況によっては問題なく使えます。使う場合が想定されてるから用意されているわけですしね。
それから、一つのアプリの中で一つの状態管理手法しか使っていけないということもないですね。状況に応じて使い分けると良いと思います。
でも一つのパターンに固定するのも管理コストが抑えられて良いですけどね。
状態管理手法に「唯一の正解」はないので、「これでいいのかな…?」と不安な人も、決めないと始まりませんので、決断して実装に取り掛かりましょう。