はじめに
Google I/O 2019 にて provider
というパッケージについて言及があり、公に Google 推奨になりました。
- provider | Flutter Package
使用例を探してみたのですが、scoped_model の代わりに ChangeNotifierProvider
を使う例ばかりで、他の多数のプロバイダの情報はほとんど見当たりません。
各プロバイダの使用例について 要望 が上がっているので、そのうち公式に用意される可能性がありますが(結局なし)、まず自分で探り探りサンプルを作りながら調べてみました。
文字どおり「探り探り」ですので、誤りがあった場合にはご容赦ください。
2019/12/25 追記
投稿から一年が経った今では非常に有用なパッケージとしてメジャーになっています。
2020/7/31 追記
Flutter の 状態管理方法一覧ページ が半月ほど前に更新され、provider について「A recommended approach.」と記されました。
バージョン
この記事は 2021/3/6 更新時点で provider v5.0.0 に対応しています。
最近の大きな変化:
-
v3.2.0
-
ProxyProvider
のinitialBuilder
/builder
がそれぞれcreate
/update
、他の各 Provider のbuilder
がcreate
に変わり、元の引数名は deprecated となりました。
-
-
v4.0.0
- v3.2.0 で deprecated となった引数名が完全廃止されました。
-
Provider.of
のlisten
を指定しなくても使い分けてくれるようになりました。- デグレードが起こったため、すぐに v4.0.0-hotfix.1 で無くなりました。
-
StreamProvider.controller()
が廃止されました。 -
Selector
でコレクションの深い比較が行われるようになりました。 -
Selector
にshouldRebuild
が追加され、リビルドの条件を指定できるようになりました。 - 各 Provider によるインスタンス生成と listen 開始が lazy になりました。
- 必要なタイミングに遅延実行されます。
- アプリの挙動が過去バージョンと違ってくる場合があるのでご注意ください。
-
v4.1.0
-
BuildContext
の三つの extension メソッドが追加されました。context.read()
context.watch()
context.select()
- 各 Provider に
builder
の引数が追加されました。 -
Locator
という型定義が追加されました。
-
-
v4.2.0
-
MultiProvider
にもbuilder
の引数が追加されました。
-
-
v4.3.0
-
ReassembleHandler
が追加されました。
-
-
v4.3.2+4
-
ValueListenableProvider
のデフォルトコンストラクタが deprecated になりました。
-
-
v4.3.3
-
context.read()
とcontext.watch()
の使用箇所の assertion がなくなりました。
-
-
v5.0.0
- Null safety に対応しました。
-
ValueListenableProvider
のデフォルトコンストラクタが廃止されました。 -
FutureProvider
、StreamProvider
とその各々の.value
コンストラクタでinitialData
が必須となりました。
活発に開発されていて、破壊的な変更もたびたび加えられています。
今後も大きく変わる場合がありますが、この記事を読んでおけば変更を理解しやすくなるはずです。
provide と provider
provider
の他に provide という類似パッケージがあります。
名前までよく似ていて一字違いなのでちょっと紛らわしいですね。
provide
はもともと Google 公式で「Scoped Model バージョン2」と言える位置付けだったようですが、数ヶ月前から議論 1 があり、結局 provider
のほうが優れているという判断に至ったそうです。2
今後は provider
を使いましょう。
何のためのパッケージ?
Google I/O のプレゼンでは Scoped Model の新しい書き方という印象でした。
しかし、作者自身による様々な説明を見ると、それに限定した用途ではないことがわかります。
- InheritedWidget を使いやすくしてミスを防ぐためのシンタックスシュガー 3 4
- DI の仕組みを提供 5 6
- インスタンスの生成と破棄を助ける(
StatelessWidget
でもdispose()
が可能になる等) 7 - その他何でも(Scoped Model、BLoC 等による状態管理、ValueNotifier 等による Widget 更新など) 8
本記事について
provider を使った状態管理方法に重点を置いて解説するものではありません。
その用途でよく使われる Provider や ChangeNotifier をはじめとして使用頻度の低いプロバイダまで各種類を把握しておきたい方や、個々のプロバイダを使うときに辞書/参考書のように見直したい方に向いています。
下位 Widget で値を得る方法
InheritedWidget
を使うと、ツリーの下位にある Widget に情報を効率良く渡すことができます。
provider は先述のとおり InheritedWidget
のシンタックスシュガーなので使い方が似ていて、プロバイダで設定した値を下位 Widget で得ることができます。
その方法をまず見ておきましょう。複数あります。
Provider.of()
class FooWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<String>.value(
value: 'この値を下位Widgetで使いたい',
child: _BarWidget(),
);
}
}
class _BarWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final value = Provider.of<String>(context); // value == 'この値を下位Widgetで使いたい'
return Text(value);
}
}
このメソッドには listen
という引数もあります。
true
こちらがデフォルトです。
inheritFromWidgetOfExactType が使われ、変更が notify されるとリビルドが起こります。false
ancestorInheritedElementForWidgetOfExactType が使われ、リビルドを防げます。
値の変更に応じた処理(表示更新など)が起こらなくなりますが、そういった処理が不要な箇所(受け取ったモデルのメソッドを使うだけ等)ではfalse
にすることで無駄を避けられます。
provider 4.0.0 では自動的に判断して使い分けてくれるようになりました。
次の場合を除き、基本的に明示的な指定は不要です。
State.initState() 内で使うときProvider の create で使うとき
listen の自動使い分けにはバグが見つかり、4.0.0-hotfix.1 で廃止されました。
v4.1.0 では Provider.of()
の代わりに使える extension メソッドが追加されましたので後述します。
Consumer()
Provider.of()
ではプロバイダの子孫にあたる Widget の BuildContext
が必要ですが、Consumer()
はそれが得られない場合にも使えて便利です。
class FooWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<String>.value(
value: 'この値を下位Widgetで使いたい',
child: Consumer<String>(
builder: (context, value, child) { // value == 'この値を下位Widgetで使いたい'
return Text(value);
},
),
);
}
}
また FAQ にあるとおり、次のように使うと Foo や Baz はリビルドせずに Bar だけをリビルドさせることができます。
Foo(
child: Consumer<A>(
builder: (_, a, child) {
return Bar(a: a, child: child);
},
child: Baz(),
),
)
Baz はあらかじめビルドされてから builder
の child
に渡されます。
これは AnimatedBuilder() 等の child と同じ仕組みです。
例えば下記コードでは、四角形の高さが変わっていっても Text('ほげ')
はリビルドされません。
そうやってパフォーマンスを改善するのに使えるものだと理解しておきましょう。
Consumer<SampleModel>(
builder: (context, model, child) {
return Container(
width: 250.0,
height: 250.0 + model.value,
color: Colors.red,
child: child,
);
},
child: Text('ほげ'),
)
Consumer() の他に Consumer2() から Consumer6() まであり、得たい値の種類数によって使い分けることができます。
個人的にそういう仕様はイマイチに思えましたが、Dart の型システムの制限によるものだそうです。9
provider v3.1.0 で MultiProvider() と組み合わせることもできるようになりました。
Selector()
Consumer()
をより便利にしたものです。
provider v3.1.0 で追加され、v4.0.0 で更に機能が増しました。
class DualCounter with ChangeNotifier {
int count1 = 0;
int count2 = 0;
DualCounter() {
Timer.periodic(Duration(seconds: 1), (t) {
t.tick.isOdd ? count1++ : count2++;
notifyListeners();
});
}
...
}
count1 と count2 の値を1秒ごとに交互にインクリメントして変更を通知するクラスです。
その値を Consumer()
で受け取ると、count1 の変更時に count2 用の Text までリビルドされてしまいます。
そこで役立つのが Selector()
です。
selector
で指定した値が変更されたときだけ builder
が呼ばれるように制限できます。
下記では、count1 の値が変わると a の builder
、count2 の値が変わると b の builder
が呼ばれます。
ChangeNotifierProvider<DualCounter>(
create: (context) => DualCounter(),
child: Column(
children: <Widget>[
Selector<DualCounter, int>(
selector: (context, model) => model.count1,
builder: (context, count, child) => Text(count.toString()), // a
),
Selector<DualCounter, int>(
selector: (context, model) => model.count2,
builder: (context, count, child) => Text(count.toString()), // b
),
],
),
)
実際に試してみると、Text が交互にリビルドされました。
このように変更に応じる対象を絞れば、余計なリビルドを防いで無駄をなくせます。
provider v4.0.0 では selector
で指定するものがコレクションの場合に深い比較が行われるようになりました。
更に、リビルドの条件を shouldRebuild
という引数で上書きできるようになりました。
その例として、GitHub に置いているサンプルに 偶数のみ表示するカウンターの例 を追加しました。
Selector<CnCounter, int>(
selector: (context, counter) => counter.number,
shouldRebuild: (prev, next) => next.isEven, // 偶数になるときのみ
builder: (context, number, child) => Text(number.toString()),
),
Consumer
と同様に他に5つあります(Selector2()
~ Selector6()
)。
基底クラスである Selector0()
も存在しますが、基本的には自分で使う必要はないと思います。
v4.1.0 では Selector
の代わりに使える extension メソッドが追加されましたので後述します。
ProxyProvider()
v3.0.0 で追加されました。10
Consumer()
に似ていて、他のプロバイダの値を得て利用することができます。
MultiProvider(
providers: [
Provider<Foo>(
create: (context) => Foo(),
),
ProxyProvider<Foo, Bar>(
create: (context) => Bar(),
update: (context, foo, prevBar) => prevBar..foo = foo,
),
],
child: ...,
)
同類として ChangeNotifierProxyProvider()
と ListenableProxyProvider()
も v3.0.0 で追加されています。
これらも全て Consumer
と同様に6つずつあります。
上の例では、別の Provider で渡された Foo が変化したときに Bar をアップデートするものですが、State
等で管理されている値の変化に伴わせるには ProxyProvider0
が使えます。
class _FooState extends State<Foo> {
int _value = 0;
void updateValue(int value) {
setState(() => _value = value);
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProxyProvider0<Bar>(
create: (context) => Bar(_value),
update: (context, bar) => bar..value = _value,
child: ...,
);
}
}
context.read() / watch()
v4.1.0 で追加されました。
BuildContext
の extension であり、Provider.of()
に相当します。
ただし、下表のような使い分けが必要で、Provider.of()
より少しややこしいです。
以前は誤って用いるとエラーが出ていましたが、v4.3.3 で出なくなりました。
自分で気を付けましょう。
相当するもの | 制約 | |
---|---|---|
context.read() | Provider.of(context, listen: false) | ビルド中は不可 各 Provider の create の関数内では使用可 |
context.watch() | Provider.of(context, listen: true) | ビルド中のみ可 各 Provider の update の関数内でも使用可 |
「ビルド中」というのは StatelessWidget や State の build() メソッドが始まってから終わるまでの間のことです。
例外として、read()
は各 Provider の create
の関数内では使用できます。
また、watch()
は update
の関数内で使用することもできます。
Widget build() {
final foo1 = context.read<Foo>(); // ダメ
final foo2 = context.watch<Foo>(); // OK
final fooValue = context.select((Foo foo) => foo.value); // OK(後述します)
return Provider<Foo>(
create: (context) => Foo(),
child: ProxyProvider0<Bar>(
create: (context) {
return Bar(foo: context.read<Foo>()), // OK
//return Bar(foo: context.watch<Foo>()), // ダメ
},
update: (context, bar) {
//return bar..foo = context.read<Foo>(); // ダメ
return bar..foo = context.watch<Foo>(); // OK
},
child: ...,
),
);
}
さらに、ボタンの onPressed
等のハンドラで使うときにも注意が必要です。
Column(
children: <Widget>[
RaisedButton(
child: ...,
onPressed: () => context.read<CnCounter>().increment(), // OK
),
RaisedButton(
child: ...,
onPressed: context.read<CnCounter>().increment, // ダメ
),
],
)
Column(
children: <Widget>[
RaisedButton(
child: ...,
onPressed: () => context.watch<CnCounter>().increment(), // ダメ
),
RaisedButton(
child: ...,
onPressed: context.watch<CnCounter>().increment, // OK
),
],
)
read()
をビルド中に使えないのは、(確信はないですが)値の変化に応じたリビルドが必要な箇所で誤って read()
を使ってしまうミスを防ぐためだと思われます。
ビルド中に使いたければ、これまで通り 11Provider.of(context, listen: false)
を使いましょう。
こちらの Issue に書かれている作者の説明によると、read()
だけでなく Provider.of(context, listen: false)
もエッジケース以外では避けたほうが良いそうです。
- 状態が変わったら表示に反映するのが適当なので
watch()
かselect()
が良い - それ以外の用途は主にタップ等によるメソッドのコールであって、そこ(
onPressed
等)ではread()
が使える
こう考えると、ビルド中に使えないのはあまり困ることでもないですね。
ただし、ケースによっては無駄なリビルドが生じることもあります。
パフォーマンスを確認しながら判断するのが良いでしょう。12
context.select()
v4.1.0 で追加されました。
BuildContext
の extension であり、Selector
に近い機能を簡潔な書き方で使えるものです。
使いどころは Selector の説明 を参照ください。
Selector<Foo, int>(
selector: (context, foo) => foo.value,
builder: (context, value, child) => Text(value.toString()),
)
これを select()
を使って書くと次のようにシンプルに書けます。
final value = context.select((Foo foo) => foo.value);
Text(value.toString());
実装は R select<T, R>(R selector(T value))
となっているので、context.select<Foo, int>((foo) => foo.value)
のように書くこともできます。
builder
v4.1.0 で各 Provider に追加された引数です(MultiProvider
には v4.2.0 で追加)。
ややこしいですが、v4.0.0 より前に存在していた builder(現 create)とは異なるものです。
では例を見ましょう。
ChangeNotifierProvider<Foo>(
create: (context) => Foo(),
child: Selector<Foo, int>(
selector: (context, foo) => foo.value,
builder: (context, value, baz) {
return Bar(value, child: baz);
}
child: Baz(),
),
)
これまで、Provider の子ですぐにその Provider で渡そうとした値を使うことはできず、Builder
・Consumer
・Selector
といった Widget を挟む必要がありました。
新しい builder
の引数は、そうしなくても済むようにしてくれるものです。13
ChangeNotifierProvider<Foo>(
create: (context) => Foo(),
builder: (context, baz) {
final value = context.select((Foo foo) => foo.value));
return Bar(value, child: baz);
},
child: Baz(),
)
サンプルと解説
よくあるカウンターのサンプルです。
右下のボタンをタップすると画面中央の値がインクリメントされます。
大半のプロバイダでは同じ動作のカウンターにしました。
一部(v3.0.0 で追加されたプロバイダ等)は同じ動作にするのが適していないため、異なるものにしています。
Provider()
何らかの型の値をツリーの下方に渡したいときに使える一番シンプルなプロバイダです。
BLoC パターンのためのプロバイダとしても使えます。
ドキュメント によれば、BLoC 等のために StatefulWidget
を使う手間を省くのに使えます。14
そこで、このサンプルでも BLoC パターンを使ってみました。
BLoC パターンの学習までまだ進んでいない方にはわかりにくいかもしれませんが、
- (a) BLoC をインスタンス化するタイミングに迷う
- (b) BLoC が不要になったときに
StatelessWidget
だと自動で破棄されない - (c) BLoC パターンを実現しやすくするプロバイダのパッケージが乱立していた
というあたりの問題をそれぞれ次のように解決してくれています。
- (a)
Provider()
のcreate
にインスタンス生成処理を渡す- Provider の
State
生成時(Widget ツリーに追加されるとき)に一度だけ実行してくれる- 指定例:
create: (context) => CounterBloc()
- 指定例:
- Provider の
- (b)
Provider()
のdispose
に破棄用メソッドを指定する- Provider が Widget ツリーから外されるタイミングで実行してくれる
- 指定例:
dispose: (context, bloc) => bloc.dispose()
- 指定例:
- Provider が Widget ツリーから外されるタイミングで実行してくれる
- (c) provider パッケージが無難な選択肢になった 15
BLoC パターンでなくても、インスタンスの生成と破棄を自動的にやってもらいつつ下位の Widget で値を利用したいケースでは役立つはずです。
class CounterBloc {
final _valueController = StreamController<int>();
Stream<int> get value => _valueController.stream;
int _number = 0;
void increment() {
_number++;
_valueController.sink.add(_number);
}
void dispose() {
_valueController.close();
}
}
class ProviderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<CounterBloc>(
create: (_) => CounterBloc(),
dispose: (_, bloc) => bloc.dispose(),
child: Scaffold(
body: _CounterText(),
floatingActionButton: _floatingButton(),
),
);
}
Widget _floatingButton() {
return Consumer<CounterBloc>(
builder: (_, bloc, child) {
return FloatingActionButton(
onPressed: bloc.increment,
child: child,
);
},
child: const Icon(Icons.add),
);
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = Provider.of<CounterBloc>(context, listen: false);
return StreamBuilder<int>(
stream: bloc.value,
initialData: 0,
builder: (_, snapshot) {
return Center(
child: Text(snapshot.data.toString()),
);
},
);
}
}
BLoC パターンについては、以前に関連記事を書いています。
一読されると理解しやすくなるかもしれません。
lazy
という引数については各プロバイダの後に解説していますので そちら をご覧ください。
注意
-
create
の処理は一度しか呼ばれません。- 値が変化するたびにプロバイダにセットし直したいケースでは
.value
という名前の付いたコンストラクタのほうを使いましょう。
- 値が変化するたびにプロバイダにセットし直したいケースでは
- 既に存在するインスタンスをデフォルトコンストラクタで扱うのは安全ではありません。
- まだ使用が終わっていないうちに dispose される危険性があります。
- 代わりに
Provider.value()
のほうを使いましょう。
-
lazy
はデフォルトでtrue
です。- 思った挙動と違ったらそれが原因の場合かもしれません。
Provider.value()
Provider()
の名前付きコンストラクタ版です。
先ほどと同じ CounterBloc を使ったサンプルにしてみました。
名前付きのほうは BLoC の dispose()
が自動的には呼ばれません。
そこで代わりに State.dispose()
を使って呼べるように StatefulWidget
にしています。
このように Provider()
を使った場合より少し手間が多くなります。
Provider.value()
は先ほど 下位 Widget で値を得る方法 でご紹介したような単純な値の伝播に向いているようです。
class ProviderValuePage extends StatefulWidget {
@override
_ProviderValueState createState() => _ProviderValueState();
}
class _ProviderValueState extends State<ProviderValuePage> {
final _bloc = CounterBloc();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Provider<CounterBloc>.value(
value: _bloc,
child: _CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: bloc.increment,
child: const Icon(Icons.add),
),
);
}
@override
void dispose() {
_bloc.dispose();
super.dispose();
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = Provider.of<CounterBloc>(context, listen: false);
return StreamBuilder<int>(
stream: bloc.value,
initialData: 0,
builder: (_, snapshot) {
return Center(
child: Text(snapshot.data.toString()),
);
},
);
}
}
このサンプルでは使いませんでしたが、次の引数もあります。
-
updateShouldNotify
- 値に変更がなくても不要なリビルドが起こってしまうのを防ぐものです。 必要に応じて指定しましょう。
StreamProvider.controller()
provider v4.0.0 で廃止されました。
折り畳んで残しておきます。
注意が必要なのは次の点です。 つまり、下位 Widget で得られるのは単なる int 型の値だということです。 しかし、使い方はわかってもあまりメリットが見えません。 また、 このサンプルでは使いませんでしたが、次の引数もあります。クリックで開閉
StreamProvider()
からご紹介すべきかもしれませんが、v2 系で StreamProvider()
だったものが v3.0.0 で StreamProvider.controller()
に変わったため、先にこちらを説明します。
Provider()
の Stream
版のようなものです。
これもインスタンスの生成と破棄をうまく扱ってくれます。
create
には StreamController
を渡すStreamController.close()
が自動的に呼ばれる
StreamController<int>()
を create
に渡すからと言って StreamProvider<StreamController<int>>.controller
とするわけではない(StreamProvider<int>.controller
にする)Provider.of<int>(context)
とする
次の二つくらいでしょうか…。 16
StreamBuilder
を使わないで済むSink
/ Stream
によって変更を通知するのに使えるStreamController
のインスタンスを create
で生成せずに、先に生成して変数に入れなければならなかったのも少し腑に落ちません。
create
で生成したインスタンスを取得する方法がないと .sink.add()
できないので、やむを得ずそうしました。class StreamProviderCtrlPage extends StatefulWidget {
@override
_StreamProviderCtrlState createState() => _StreamProviderCtrlState();
}
class _StreamProviderCtrlState extends State<StreamProviderCtrlPage> {
int _number = 0;
@override
Widget build(BuildContext context) {
final streamController = StreamController<int>();
return Scaffold(
body: StreamProvider<int>.controller(
create: (_) => streamController,
initialData: 0,
child: _CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => streamController.sink.add(++_number),
child: const Icon(Icons.add),
),
);
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final number = Provider.of<int>(context);
return Center(
child: Text(number.toString()),
);
}
}
Stream
にまだ値が来ていない間の初期値(省略時は null
)。
下位 Widget ではエラー時にそのフォールバック値が得られます。
エラーデータが扱われる Stream
であれば必須です。
Provider.value()
の updateShouldNotify
と同様です。
StreamProvider()
create
で生成する対象が、v3.0.0 で StreamController
から Stream
に変わりました。
代わりに先ほどの StreamProvider.controller()
が StreamController
版として追加されています。
StreamProvider.controller() は provider v4.0.0 で廃止されました。
他のプロバイダではデフォルトコンストラクタに破棄の機能が備わっていますが、StreamProvider()
の ドキュメント にはそのことが書かれていません。
自分で StreamController.close()
する必要があると思われます。17
class StreamProviderPage extends StatefulWidget {
@override
_StreamProviderState createState() => _StreamProviderState();
}
class _StreamProviderState extends State<StreamProviderPage> {
final _streamController = StreamController<int>();
int _number = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: StreamProvider<int>(
create: (_) => _streamController.stream,
initialData: 0,
child: _CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _streamController.sink.add(++_number),
child: const Icon(Icons.add),
),
);
}
@override
void dispose() {
_streamController.close();
super.dispose();
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final number = Provider.of<int>(context);
return Center(
child: Text(number.toString()),
);
}
}
-
initialData
-
Stream
にまだ値が来ていない間の初期値。 - 以前は任意でしたが、v5.0.0 で必須となりました。
-
このサンプルでは使いませんでしたが、次の引数もあります。
-
catchError
- エラーが来たときのフォールバック用の値をここで返します。
下位 Widget ではエラー時にそのフォールバック値が得られます。
エラーデータが扱われるStream
であれば必須です。
- エラーが来たときのフォールバック用の値をここで返します。
-
updateShouldNotify
-
Provider.value()
のupdateShouldNotify
と同様です。
-
StreamProvider.value()
StreamProvider()
とほとんど同じで、異なるのは次の点のみです。
-
create
はなくvalue
にStream
を指定する
StreamController.close()
は自分で行う必要があります。
class StreamProviderValuePage extends StatefulWidget {
@override
_StreamProviderValueState createState() => _StreamProviderValueState();
}
class _StreamProviderValueState extends State<StreamProviderValuePage> {
final _streamController = StreamController<int>();
int _number = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: StreamProvider<int>.value(
value: _streamController.stream,
initialData: 0,
child: _CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _streamController.sink.add(++_number),
child: const Icon(Icons.add),
),
);
}
@override
void dispose() {
_streamController.close();
super.dispose();
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final number = Provider.of<int>(context);
return Center(
child: Text(number.toString()),
);
}
}
引数は、v2 系で stream
だったのが v3.0.0 で value
に変わりました。ご注意ください。
value
以外の引数は StreamProvider()
と同じです。
ChangeNotifierProvider()
scoped_model
パッケージに似ていて、ほとんど同じ感覚で使えそうです。
モデルの状態を変更したときに notifyListeners()
で通知するのも同じです。
次のようにして置き換えることができます。
- モデルは
Model
ではなく ChangeNotifier を継承/Mixin適用する -
ScopedModel() を
ChangeNotifierProvider<T>()
にする -
ScopedModelDescendant() を
Consumer<T>()
にする 18
モデルのインスタンスの生成と破棄はよしなに取り計らってくれます。
破棄のほうは Provider()
と違って dispose
の引数がありません。
ChangeNotifierProvider
が Widget ツリーから外されたときに自動的に破棄されます。
class CnCounter with ChangeNotifier {
int _number = 0;
int get number => _number;
void increment() {
_number++;
notifyListeners();
}
}
class CnProviderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<CnCounter>(
create: (_) => CnCounter(),
child: Scaffold(
body: _CounterText(),
floatingActionButton: _FloatingButton(),
),
);
}
}
class _FloatingButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<CnCounter>(context, listen: false);
return FloatingActionButton(
onPressed: counter.increment,
child: const Icon(Icons.add),
);
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<CnCounter>(context);
return Center(
child: Text(counter.number.toString()),
);
}
}
このサンプルでは ChangeNotifier
を使いましたが、その派生クラス(ValueNotifier
、ScrollController
など)にも使えます。
ChangeNotifierProvider.value()
ChangeNotifierProvider()
と異なるのは次の一点のみです。
- モデルのインスタンスの生成・破棄は自己責任
class CnProviderValuePage extends StatefulWidget {
@override
_CnProviderValueState createState() => _CnProviderValueState();
}
class _CnProviderValueState extends State<CnProviderValuePage> {
final _counter = CnCounter();
@override
Widget build(BuildContext context) {
return Scaffold(
body: ChangeNotifierProvider<CnCounter>.value(
value: _counter,
child: _CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: _counter.increment,
child: const Icon(Icons.add),
),
);
}
@override
void dispose() {
_counter.dispose();
super.dispose();
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<CnCounter>(context);
return Center(
child: Text(counter.number.toString()),
);
}
}
引数は、v2 系で notifier
だったのが v3.0.0 で value
に変わりました。ご注意ください。
ValueListenableProvider()
v5.0.0 でこのデフォルトコンストラクタのほうだけ廃止されました。
廃止されていない .value
のほうの理解に必要であれば下記を開いてご覧ください。
なお、 このサンプルでは使いませんでしたが、次の引数もあります。クリックで開閉
ValueListenable
というと、そのインタフェースを実装した ValueNotifier
を ValueListenableBuilder
と組み合わせて値の変更を Widget に随時反映するのがよくある使い方です。
次の動画を観るほうがイメージしやすいと思います(2分弱の短い動画です)。ValueListenableProvider()
を使うと、ValueListenableBuilder
を使わずに同じことが簡単にできます。
動画では InheritedWidget
も組み合わせることが提案されていますが、provider パッケージは InheritedWidget
のシンタックスシュガーなのでそれも不要です。Provider.of()
で得られるのは create
で生成されたインスタンスではない点に注意が必要です。
ValueNotifier.value
(本サンプルでは VnCounter.value
)の値です。
その値を変更しても Notify されません(そもそも変更もできません)。create
で生成したものは ChangeNotifierProvider()
と同様に自動的に破棄してくれます。class VnCounter extends ValueNotifier<int> {
VnCounter() : super(0);
void increment() => value++;
}
class VlProviderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = VnCounter();
return Scaffold(
body: ValueListenableProvider<int>(
create: (_) => counter,
child: _CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
child: const Icon(Icons.add),
),
);
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final number = Provider.of<int>(context);
return Center(
child: Text(number.toString()),
);
}
}
Provider.value()
等の updateShouldNotify
と同様です。
おさらい(ChangeNotifierProvider vs ValueListenableProvider)
-
ChangeNotifierProvider
-
ChangeNotifier
にもValueNotifier
にも使える - 下位の Widget に伝わるのはインスタンス
-
-
ValueListenableProvider
-
ChangeNotifier
には使えない - 下位の Widget に伝わるのは
ValueNotifier.value
の中身
-
ValueListenableProvider.value()
ValueListenableProvider()
/ ValueListenableProvider.value()
の違いは、Provider()
/ Provider.value()
の違いと同じです。
class VlProviderValuePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = VnCounter();
return Scaffold(
body: ValueListenableProvider<int>.value(
value: counter,
child: _CounterText(),
),
floatingActionButton: FloatingActionButton(
onPressed: counter.increment,
child: const Icon(Icons.add),
),
);
}
}
class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final number = Provider.of<int>(context);
return Center(
child: Text(number.toString()),
);
}
}
引数は、v2 系で valueListenable
だったのが v3.0.0 で value
に変わりました。ご注意ください。
ListenableProvider()
ChangeNotifierProvider
はこれを継承したものであり、似ています。
create
では Listenable
を生成します。
Listenable
を実装しているものとして、ドキュメント には次のように4つが挙げられています。
Implementers
Animation, ChangeNotifier, CustomPainter, ValueListenable
このうちの ChangeNotifier
と ValueListenable
は既に見た ChangeNotifierProvider
や ValueListenableProvider
で扱えますね。
その二つにはこの ListenableProvider
も使えます 19 が、特別な理由がなければあえて使う必要はないと思います。
使う場合、破棄の処理を任せたいなら dispose
を指定する必要があります。
この点は ChangeNotifierProvider
等よりわずかに手間です。
2019/7/6
サンプルを載せていましたが、このプロバイダを使う明確な理由がなかったため削除しました。
説明文も更新しました。
(GitHub に置いているサンプル では参考としてアニメーションに使っています。)
MultiProvider()
複数のプロバイダを使うときに
Provider<Foo>.value(
value: foo,
child: Provider<Bar>.value(
value: bar,
child: Provider<Baz>.value(
value: baz,
child: someWidget,
)
)
)
という深いネストにする代わりに
MultiProvider(
providers: [
Provider<Foo>.value(value: foo),
Provider<Bar>.value(value: bar),
Provider<Baz>.value(value: baz),
],
child: someWidget,
)
のように簡潔に書けるようにするプロバイダです。
コードの見た目は異なりますが、Widget ツリー上は同じものになります。
※このコードは クラスのドキュメント から借用しました。
注意
上記の例では Provider<T>.value()
の T
の部分に Foo
Bar
Baz
という異なる型が指定されています。
もし同じ型にしてしまうと、次のように最後のプロバイダで指定した値になってしまいます。
MultiProvider(
providers: [
Provider<int>.value(value: 1),
Provider<int>.value(value: 2),
Provider<int>.value(value: 3),
],
child: someWidget(context),
)
Widget someWidget(BuildContext context) {
final val1 = Provider.of<int>(context);
final val2 = Provider.of<int>(context);
final val3 = Provider.of<int>(context);
print(val1); // 3
print(val2); // 3
print(val3); // 3
}
これは Provider.of() のドキュメント に書かれているとおり
Widget ツリーをさかのぼって最寄りの
Provider<T>
を取得し、その値を返す 20
という仕様によるものです。21
MultiProvider()
に限ったことではありません。
ご注意ください。
FutureProvider()
ここからは v3.0.0 で追加された種類になります。
.value
という名前付きのコンストラクタに関する説明はこの先省略します。
InheritedWidget
と FutureBuilder を組み合わせたような便利なものです。
ここまでのサンプルと同じ動作にしたかったのですが、無理でしたので異なるものにしました。
- ページを表示すると「Wait for 3 seconds...」と表示される
-
Future
の処理が3秒後に完了する - 自動的に表示が「3 seconds elasped.」に変わる
class FutureProviderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureProvider<String>(
create: (_) =>
Future.delayed(Duration(seconds: 3), () => '3 seconds elapsed.'),
initialData: 'Wait for 3 seconds...',
child: _FutureText(),
),
);
}
}
class _FutureText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final description = Provider.of<String>(context);
return Center(
child: Text(description),
);
}
}
-
initialData
-
Future
の処理が完了していない間の初期値。 - 以前は任意でしたが、v5.0.0 で必須となりました。
-
このサンプルでは使いませんでしたが、次の引数もあります。
-
catchError
- 他のプロバイダの同名の引数と同様です。
-
updateShouldNotify
- 他のプロバイダの同名の引数と同様です。
FutureProvider.value() もありますが、省略します。
ProxyProvider()
下位 Widget で値を得る方法 のところで既に触れたものです。
これも同じ動作にするのが難しかったため、異なるサンプルにしました。
カウンターという点は同じですが、10進数と16進数の二つの表示になっています。
- 10進数と16進数のカウンターがある
- 10進数は
ValueListenableProvider()
のサンプルで使ったものを流用。 - 16進数のほうは10進数の値を受け取って保持する immutable なクラスであり、ゲッターで返すときに16進数に変換する。
- 10進数は
-
ChangeNotifierProvider()
で10進数カウンターを生成 -
ProxyProvider()
で10進数の値を渡して16進数カウンターを生成 - 10進数カウンターの値が変わるたびに
ProxyProvider
のupdate
が呼ばれる- そのたびに16進数カウンターのインスタンスが生成される。
- 両カウンターのインスタンスが Widget ツリーの下位に伝播される
- 10進数のカウントアップに連動して16進数の値も更新される。
class HexCounter {
int _decimal;
HexCounter();
set newValue(int newValue) => _decimal = newValue;
String get hex => _decimal.toRadixString(16);
}
class ProxyProviderPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<VnCounter>(
create: (_) => VnCounter(),
),
ProxyProvider<VnCounter, HexCounter>(
create: (_) => HexCounter(),
update: (_, decCounter, prevHexCounter) =>
prevHexCounter..newValue = decCounter.value,
),
],
child: Scaffold(
body: _CounterResults(),
floatingActionButton: _FloatingButton(),
),
);
}
}
class _FloatingButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
final counter = Provider.of<VnCounter>(context, listen: false);
return FloatingActionButton(
onPressed: counter.increment,
child: const Icon(Icons.add),
);
}
}
class _CounterResults extends StatelessWidget {
@override
Widget build(BuildContext context) {
final decCounter = Provider.of<VnCounter>(context);
final hexCounter = Provider.of<HexCounter>(context);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Decimal'),
Text(decCounter.value.toString()),
const Text('Hexadecimal'),
Text(hexCounter.hex),
],
),
);
}
}
create
は一度しか呼ばれず、update
は何度も呼ばれます。
create
で生成されたインスタンスを update
のコールバック関数の第3引数で受け取って再利用することができ、再利用して更新した値を下位に伝えたいときに役立ちます。
なお、ChangeNotifierProxyProvider()
と ListenableProxyProvider()
では、provider v3.1.0 において create
が必須となりました。
※v3.1.0 時点では create
ではなく initialBuilder
、update
ではなく builder
という引数名でした。
builder の呼ばれるタイミングが ProxyProvider だけ異なるというややこしさがありましたが、その後 v3.2.0 のリネームにより解消しました。
ChangeNotifierProxyProvider<Foo, MyChangeNotifier>(
create: (_) => MyChangeNotifier(),
update: (_, foo, myNotifier) => myNotifier
..foo = foo,
child: ...
);
このコードは ChangeNotifierProxyProvider()
の ドキュメント からの引用したものです。
MyChangeNotifier のインスタンスを create
で一度だけ生成しています。
update
が呼ばれるたびに生成済みのインスタンスを第3引数で受け取って使い回すことになります。
InheritedProvider()
クラスのドキュメント に次のように書かれています。
Do not use this class directly unless you are creating a custom "Provider".
Instead use Provider class, which wraps InheritedProvider.
訳: カスタムなプロバイダを作る以外では InheritedProvider
クラスを直接使わず、
それをラップした Provider
クラスを代わりに使うこと。
基本的に直接触る必要がないものです。
ReassembleHandler
Provider ではなくインタフェースです。
このインタフェースを実装(reassemble()
というメソッドを用意)してそこに何らかの処理を書いておくと、Hot reload 等でリビルドされたときにその処理が実行されます。
実際の用途としては Hot reload 時にそのクラスのオブジェクトの値だけリセットするといったところかと思うのですが、ここでのサンプルとしては良いものが浮かばず、ボタン押下時だけでなく Hot reload 時にもカウンターの値が増えるというサンプルにしてみました。
class ReassemblingCounterNotifier extends ValueNotifier<int> with ReassembleHandler {
ReassemblingCounterNotifier() : super(0);
@override
void reassemble() {
increment();
}
void increment() {
value++;
}
}
コード全体は GitHub にあるプロジェクト の該当箇所をご覧ください。
(実際のコードではリビルド時に SnackBar の表示もしています。)
lazy
v4.0.0 で各プロバイダのコンストラクタ(MultiProvider や名前付きコンストラクタを除く)に導入された引数です。
省略するか true
を指定すると、create
に渡したコールバック関数で生成されるオブジェクトが最初に使用されるときまでその生成が遅延されます。
class Foo {
Foo() {
print('Foo was created.');
}
}
class Bar {
Bar(this.foo) {
print('Bar was created.');
}
final Foo foo;
void method() {
print("Bar's method was called.");
}
}
MultiProvider(
providers: [
Provider<Foo>(
create: (context) { // 3
print('Creating Foo...');
return Foo(); // 4
},
),
Provider<Bar>(
create: (context) { // 2
print('Creating Bar...');
final foo = context.read<Foo>(); // 5
return Bar(foo); // 6
},
),
],
builder: (context, child) {
return RaisedButton(
child: child,
onPressed: () { // 1
print('Button was pressed.');
context.read<Bar>(); // 7
},
);
},
child: const Text('Push'),
)
この例では lazy
を省略していて true
になるため、create
に渡したコールバック関数はすぐに呼ばれません。
ボタンを押すと初めて Foo や Bar が生成されます。
そのときの出力は次のようになります。
I/flutter ( 4276): Button was pressed.
I/flutter ( 4276): Creating Bar...
I/flutter ( 4276): Creating Foo...
I/flutter ( 4276): Foo was created.
I/flutter ( 4276): Bar was created.
I/flutter ( 4276): Bar's method was called.
試しに Provider<Bar>(
の次の行に lazy: false,
を加えてみてください。
そうするとボタンを押さなくてもコールバックが実行されて次の出力になります。
I/flutter ( 4276): Creating Bar...
I/flutter ( 4276): Creating Foo...
I/flutter ( 4276): Foo was created.
I/flutter ( 4276): Bar was created.
I/flutter ( 4276): Button was pressed.
I/flutter ( 4276): Bar's method was called.
Locator
v4.1.0 で追加されました。
DI に使うことができます。
早速ですが用例を見てみましょう。
class Model {
Model(this.locator);
final Locator locator;
void method() {
print(locator<Whatever>());
}
}
// ...
Provider(
create: (context) => Model(context.read),
child: ...,
)
これまでは Whatever のオブジェクトを Model 内で使うには、そのオブジェクト自体を Model に渡すか、BuildContext
を渡して Model の中で Provider.of()
を使って取り出す必要がありました(後者は Provider や Flutter に依存してしまうアンチパターン)。
一方 Locator
を使うと、上記のように context.read
を Model で受け取っておけば locator<Whatever>()
で値にアクセスすることができます。
この Locator
は次のように定義されていて、Flutter への依存がありません。
依存させたくない Model の中でも安心して使えることになります。
typedef Locator = T Function<T>();
ただし、これが便利かどうかは使用者の考え次第でしょう。22
トラブルシューティング
遷移先のページで値を得られない
解決方法は主に三つあります。
MaterialApp より上で渡す
MaterialApp
より下で Provider によって渡そうとしても次のページからは値にアクセスできません。
これは、遷移前後のページ間に親子関係がないからです。
遷移先から見た親は遷移前のページではなく、最寄りの Navigator
(自分で新たな Navigator
を追加しない限りは MaterialApp
)です。
そこより上で渡していなければ受け取れません。
逆に言うと、MaterialApp
より上で渡していれば問題に遭遇せずに済むということです。
これで解決できるわけですが、上の位置になるほどツリーから外される機会が減って寿命が長くなりますので、そのことが好ましくない場合には残りの方法を検討しましょう。
Navigator を追加する
今述べた構造から想像できたかもしれませんが、遷移前に Navigator
を追加すれば遷移先ページの親がその Navigator
になり、それより上(MaterialApp
より下であっても)で渡している値にアクセスできるようになります。
ただし、Navigator
が多重になることで何らかの悪影響が出る可能性があります。
この方法を使う場合にはご注意ください。
ページ遷移時に渡し直す
// A
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (context2) {
// B
return NextPage();
}),
)
A 以上と B 以下の BuildContext
(context と context2)は大きく異なるものです。
名前に 2 をつけて区別していますが、名前だけでなく中身も違っていて、ツリーの同じ枝上にすらありません。
これも先ほど説明した構造によるもので、ツリーは MaterialApp
で分岐しているので、context2 を用いて遡っても他の枝にある元ページのほうで渡された値にアクセスできないのです。
どうしても無理ですから、受け取るには A の BuildContext
を使うしかありません。
そうやって受け取った値を B 側で渡し直せば解決します。
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context2) => Provider<Foo>.value(
value: Provider.of<Foo>(context, listen: false),
child: NextPage(),
),
),
)
ちょっと冗長な書き方になりますね。
ツリーの深いところまで渡す必要がなければ、プロバイダを使わずにコンストラクタの引数で渡すほうがシンプルで良い場合もあります。
Navigator.of(context).push(
MaterialPageRoute<void>(builder: (context) => NextPage(foo)),
)
※コンストラクタ経由以外の渡し方 もあります。
initState() で値を得られない
class _FooPageState extends State<FooPage> {
Bar _bar;
@override
void initState() {
super.initState();
_bar = Provider.of<Bar>(context);
}
...
}
このように initState()
にてプロバイダ経由で値を得ようとすると例外が発生します。
I/flutter ( 4953): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter ( 4953): The following assertion was thrown building InheritedProvider<Bar>:
I/flutter ( 4953): inheritFromWidgetOfExactType(InheritedProvider<Bar>) or inheritFromElement() was called before
I/flutter ( 4953): _FooPageState.initState() completed.
I/flutter ( 4953): When an inherited widget changes, for example if the value of Theme.of() changes, its dependent
I/flutter ( 4953): widgets are rebuilt. If the dependent widget's reference to the inherited widget is in a constructor
I/flutter ( 4953): or an initState() method, then the rebuilt dependent widget will not reflect the changes in the
I/flutter ( 4953): inherited widget.
I/flutter ( 4953): Typically references to inherited widgets should occur in widget build() methods. Alternatively,
I/flutter ( 4953): initialization based on inherited widgets can be placed in the didChangeDependencies method, which
I/flutter ( 4953): is called after initState and whenever the dependencies change thereafter.
このエラーメッセージには答えがちゃんと書かれています。
特に最後の3行です。
build()
内か、initState()
の後に呼ばれる didChangeDependencies()
の中で行いましょう。
なお、didChangeDependencies() は initState() と違って何度も呼ばれるのでご注意ください。
State のライフサイクル(各メソッドが呼ばれるタイミング)については次のページがわかりやすいです。
-
Stateful Widget Lifecycle | Flutter By Exampleページがなくなったようです… - https://stackoverflow.com/a/53379776/12086560
provider 4.0.0 以降では listen: false
を指定すれば initState() 内で使用できます。
ただし、リビルドが起こるようなメソッドをそこで使うことはできません。
@override
void initState() {
super.initState();
_bar = Provider.of<Bar>(context, listen: false);
//_bar.someMethodToCauseRebuild(0);
}
build()内で値を変えるとエラー
Widget の build()
内で ChangeNotifier
や ValueNotifier
を使って値を更新すると起こる現象です。
例をご覧ください。
class FooModel extends ValueNotifier<String> {
FooModel() : super('');
}
ChangeNotifierProvider<FooModel>(
create: (_) => FooModel(),
child: MyWidget(),
)
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
@override
Widget build(BuildContext context) {
final model = Provider.of<FooModel>(context);
// build()内ですぐに値を変える
model.value = 'Hello!';
return Center(
child: Text(model.value),
);
}
}
-
FooModel
のvalue
をChangeNotifierProvider
がListen
している - 値が変わると、関連箇所をリビルドさせるために内部的に
dirty
という印が付けられる 23 - そのタイミングは Widget のビルド中であってはならず、違反すると次の例外が発生する
I/flutter (19781): ══╡ EXCEPTION CAUGHT BY FOUNDATION LIBRARY ╞════════════════════════════════════════════════════════
I/flutter (19781): The following assertion was thrown while dispatching notifications for FooModel:
I/flutter (19781): setState() or markNeedsBuild() called during build.
I/flutter (19781): This ChangeNotifierProvider<FooModel> widget cannot be marked as needing to build because the
I/flutter (19781): framework is already in the process of building widgets. A widget can be marked as needing to be
I/flutter (19781): built during the build phase only if one of its ancestors is currently building. This exception is
I/flutter (19781): allowed because the framework builds parent widgets before children, which means a dirty descendant
I/flutter (19781): will always be built. Otherwise, the framework might not visit this widget during this build phase.
I/flutter (19781): The widget on which setState() or markNeedsBuild() was called was:
I/flutter (19781): ChangeNotifierProvider<FooModel>
I/flutter (19781): The widget which was currently being built when the offending call was made was:
I/flutter (19781): MyWidget
これを避ける方法は、Widget のビルド中に dirty
を付けないことです。
そう考えると、代わりのタイミングは次の二つになります。
ビルドより前(祖先がビルドされている間)
FooModel()
のvalue
の変更によってdirty
が付く Widget がビルドされる前という意味です。
つまりChangeNotifierProvider()
より前である必要があります。
ChangeNotifierProvider()
のcreate
でも可能ですが、そこが OK なのは、その時点でまだビルドが始まっていないからだと思います。ビルドが終わった後
ChangeNotifierProvider()
の子孫のビルド中に値を変えても例外が発生します。
全子孫のビルドが終わったタイミングに処理をするには、下記のようなハックが必要です。24
下記6つのうちどれを使っても、実行をビルド完了後に遅延させることができました。
Future(() { /* 処理 */ });
Future.delayed(Duration.zero, () { /* 処理 */ });
WidgetsBinding.instance.addPostFrameCallback((_) { /* 処理 */ });
// package:flutter/scheduler.dartのインポートが必要
// 機能はWidgetsBindingのほうと全く同じ
SchedulerBinding.instance.addPostFrameCallback((_) { /* 処理 */ });
// dart:asyncのインポートが必要
scheduleMicrotask(() { /* 処理 */ });
Future.microtask(() { /* 処理 */ });
initState()
、didChangeDependencies()
、build()
のどこに書いてもビルド後になります。
注意
使う前によく検証しましょう。
initState()
以外はリビルドのたびに呼ばれ、値更新→リビルド→値更新… と繰り返してしまいます。
フラグなどで制御する必要があると思います。こちらの SO の情報 では
addPostFrameCallback()
でできた人とできなかった人がいます。個人的には問題なく使えています。
Selector() で複数の値を対象にしたい
公式ドキュメント では tuple を使うのが最も簡単な方法だと紹介されています。
次の例では foo.bar
か foo.baz
の値が変わったときのみ builder
が呼ばれます。
Selector<Foo, Tuple2<Bar, Baz>>(
selector: (_, foo) => Tuple2(foo.bar, foo.baz),
builder: (_, data, __) {
return Text('${data.item1} ${data.item2}');
}
)
しかし、FAQ に書かれているように複数の値にそれぞれの型を持たせるのも良いでしょう。
FAQ の例では国名と市名を String ではなく Country と City という個別の型にすることで解決しています。
Provider<Country>(
create: (_) => Country('England'),
child: Provider<City>(
create: (_) => City('London'),
child: ...,
),
),
ダイアログでエラーが出る
ダイアログに限りませんが、Provider と異なるツリーで Provider.of()
を使おうとすると例外が発生し、次のようなエラーメッセージが出力されます。
Tried to listen to a value exposed with provider, from outside of the widget tree.
もしそうなったら、Provider.of() で listen: false
を指定してみましょう。
これで解決しない場合、似て非なる原因かもしれません。
メッセージの続きに対処法が書かれていないか確認してみてください。
Provider で生成したオブジェクトを直後の Provider で使いたい
create
の関数に渡される BuildContext
を使うと直前の Provider で生成されたオブジェクトにアクセスできます。
Provider<Foo>(
create: (_) => Foo(),
child: Provider<Bar>(
create: (context) { // このBuildContextを使う
final foo = Provider.of<Foo>(context, listen: false),
// または final foo = context.read<Foo>();
return Bar(foo);
},
child: ...,
),
)
MultiProvider(
Provider<Foo>(
create: (_) => Foo(),
),
Provider<Bar>(
create: (context) {
final foo = Provider.of<Foo>(context, listen: false);
return Bar(foo);
},
),
child: ...,
)
-
https://pub.dev/packages/provide "NOTE 2019-02-21: There's a discussion in the Flutter community over the difference between this package,
package:provider
, andpackage:scoped_model
. There is a possibility that (some) of these efforts will merge. Learn more in issue #3." ↩ -
https://www.youtube.com/watch?time_continue=1162&v=d_m5csmrf7I 19:23~19:33辺り "Fun fact: we actually had our own package, kind of like Scoped Model version 2. And then we released it earlier this year, open-sourced it everything, and then we realized
provider
is actually much better. So we're usingprovider
instead. So don't useprovide
. Useprovider
." ↩ -
https://pub.dev/packages/provider "
provider
is mostly syntax sugar forInheritedWidget
, to make common use-cases straightforward." ↩ -
https://github.com/rrousselGit/provider/issues/45#issuecomment-488241885 "This library is not opinionated on state management. It aims mostly at fixing the errors that the community make when working with inherited widgets." ↩
-
https://pub.dev/packages/provider "A dependency injection system built with widgets for widgets." ↩
-
https://www.reddit.com/r/FlutterDev/comments/bmrvey/so_is_provider_recommended_over_bloc_just_watched/emze9z6/ "What
provider
is all about is: having a really robust dependency injection system. It "forces" you to have a architecturally better app. No global, no messy object graph" ↩ -
https://www.reddit.com/r/FlutterDev/comments/bmrvey/so_is_provider_recommended_over_bloc_just_watched/emze9z6/ "explicit creation/ dispose" ↩
-
https://www.reddit.com/r/FlutterDev/comments/bmrvey/so_is_provider_recommended_over_bloc_just_watched/emze9z6/ "you can fit it pretty much anything. For example BLoC may use
provider
this way: ..." ↩ -
https://github.com/rrousselGit/provider/issues/127#issuecomment-508954636 ↩
-
ProxyProvider
導入が検討されていたときの「[RFC] Simplifying providers that depends on each others #46」という Issue を見るとよりわかりやすいです。 ↩ -
read()
の制約が厳しい理由を理解した上であれば、あえて制約のないcontext.value()
(名前は適当)のような extension を自作して使うのは良いかもしれません。 ↩ -
リビルドは再描画ではないので、多少の無駄なリビルドが体感やバッテリ消費に大きく影響するものでもありません。 ↩
-
更新履歴 には
Builder
と同じ振る舞いだと書かれていますが、少しだけ異なると思います。Builder
・Selector
等では Baz はその孫なので Foo の値が更新されるたびにリビルドされますが、builder
を使う場合は Baz は ChangeNotifierProvider の子であり、あらかじめビルドしてから使用されるのでリビルドされないはずです。 ↩ -
"It is usually used to avoid making a StatefulWidget for something trivial, such as instantiating a BLoC." ↩
-
BLoC に特化した他の軽量なパッケージをあえて用いるのも良い選択だと思います。 ↩
-
.controller()
ではなく.value()
のほうについては、Firebase のユーザ認証に使う例が こちらの記事 に書かれています。プラグイン等によって用意されるStream
をそのまま用いるようなケースに向いていそうです。 ↩ -
ソースコードをみると、
create
の処理がBuilderStateDelegate
というものに移譲され、そこのdispose()
でStream
の破棄が行われているようです。このときにStreamController
が破棄されるわけではないはずです。 ↩ -
ScopedModelDescendant()
に近い書き方になるのはConsumer()
ですが、もちろんProvider.of()
も使えます。 ↩ -
ややこしいですが、
ChangeNotifier
とValueListenable
はListenable
の実装であり、ValueNotifier
はChangeNotifier
を継承 &ValueListenable
を実装したものです。また、ChangeNotifierProvider
はListenableProvider
を継承しています。 ↩ -
"Obtains the nearest
Provider<T>
up its widget tree and returns its value." ↩ -
延いては、
Provider.of()
で使われているBuildContext.inheritFromWidgetOfExactType()
やBuildContext.ancestorInheritedElementForWidgetOfExactType()
の仕様です。 ↩ -
個人的には、Model がどのようなオブジェクトを使うのかが渡す側から見てわかりにくいと感じました。また、単体テスト等で
TFunction<T>()
型にして渡す手間も生じます。一方で、依存が増えても渡す側を変更する手間をなくせます。そのようなメリットとデメリットのトレードオフになります。 ↩ -
Flutter in Focus の How Stateful Widgets Are Used Best - Flutter Widgets 101 Ep. 2 という動画が非常にわかりやすいです。 ↩
-
StackOverflow の ここ や ここ が助けになりました。書かれている質問は別件だったりするのですが、回答のほうはビルド中の
dirty
化の問題にも当てはまります。 ↩