Edited at

[Flutter] package:provider の各プロバイダの詳細


はじめに

Google I/O 2019 にて provider というパッケージについて言及があり、公に Google 推奨になりました。

早めに理解するべく使用例を探してみたのですが、scoped_model の代わりに ChangeNotifierProvider を使う例ばかりで、他の複数種類あるプロバイダの情報はほとんど見当たりません。

v2.0.0 で仕様が大きく変わり、そこからまだ日が浅いせいかもしれません。

各プロバイダの使用例に関する要望 が Issues に上がっているので、そのうち公式に出てくる可能性がありますが、まず自分で探り探りサンプルを作りながら調べてみました。

文字どおり「探り探り」ですので、誤りがあった場合にはご容赦ください。


バージョン

この記事は 2019/09/03 更新時点で provider 3.1.0 に対応しています。

活発に開発されていて、破壊的な変更もたびたび加えられています。

今後も大きく変わる場合がありますが、この記事を読んでおけば変更を理解しやすくなるはずです。


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


下位 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 にすることで無駄を避けられます。


ChangeNotifierProvider のような、値の変更を listen するタイプのプロバイダでなければリビルドは起こらないため、Provider 等のプロバイダでは明示的に false にする必要はないと思われます。


Consumer()

Provider.of() ではプロバイダの子孫にあたる Widget の BuildContext が必要ですが、Consumer() はそれが得られない場合にも使えて便利です。

ただし、Provider.of() のようなリビルドの調整はできません。

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);
},
),
);
}
}

他に Consumer2() から Consumer6() まであり、得たい値の種類数によって使い分けることができます。

個人的にそういう仕様はイマイチに思えましたが、Dart の型システムの制限によるものだそうです。9

provider 3.1.0 で MultiProvider() と組み合わせることもできるようになりました。


Selector()

Consumer() をより便利にしたものです。

provider 3.1.0 で追加されました。

class DualCounter with ChangeNotifier {

int count1 = 0;
int count2 = 0;

DualCounter() {
Timer.periodic(Duration(seconds: 1), (t) {
t.tick % 2 > 0 ? count1++ : count2++;
notifyListeners();
});
}
}

count1 と count2 の値を1秒ごとに交互にインクリメントして変更を通知するクラスです。

その値を Consumer() で受け取ると、count1 の変更時に count2 用の Text までリビルドされてしまいます。

そこで役立つのが Selector() です。

selector で指定した値が変更されたときだけ builder が呼ばれるように制限できます。

下記では、count1 の値が変わると a の builder、count2 の値が変わると b の builder が呼ばれます。

ChangeNotifierProvider<DualCounter>(

builder: (_) => DualCounter(),
child: Column(
children: <Widget>[
Selector<DualCounter, int>(
selector: (_, model) => model.count1,
builder: (_, count, __) => Text(count.toString()), // a
),
Selector<DualCounter, int>(
selector: (_, model) => model.count2,
builder: (_, count, __) => Text(count.toString()), // b
),
],
),
)

実際に試してみると、Text が交互にリビルドされました。

このように変更に応じる対象を絞れば、余計なリビルドを防いで無駄をなくせます。

flutter_provider_selector.gif

Consumer と同様に他に5つあります(Selector2()Selector6())。

基底クラスである Selector0() も存在しますが、基本的には自分で使う必要はないと思います。


ProxyProvider()

v3.0.0 で追加されました。10

Consumer() に似ていて、他のプロバイダの値を得て利用することができます。

MultiProvider(

providers: [
Provider<Foo>(
builder: (_) => Foo(),
),
ProxyProvider<Foo, Bar>(
builder: (context, foo, previousBar) => Bar(foo),
),
],
child: ...,
)

同類として ChangeNotifierProxyProvider()ListenableProxyProvider() も v3.0.0 で追加されています。

これらも全て Consumer と同様に6つずつあります。


サンプルと解説

よくあるカウンターのサンプルです。

右下のボタンをタップすると画面中央の値がインクリメントされます。

flutter_provider_examples.gif

大半のプロバイダでは同じ動作のカウンターにしました。

一部(v3.0.0 で追加されたプロバイダ等)は同じ動作にするのが適していないため、異なるものにしています。


Provider()

ドキュメント によれば、BLoC パターンなどでわざわざ StatefulWidget を使うのを避けるのに使えます。11

そこで、このサンプルでも BLoC パターンを使ってみました。

BLoC パターンの学習までまだ進んでいない方にはわかりにくいかもしれませんが、


  • (a) BLoC をインスタンス化するタイミングに迷う

  • (b) BLoC が不要になったときに StatelessWidget だと自動で破棄されない

  • (c) BLoC パターンを実現しやすくするプロバイダのパッケージが乱立していた

というあたりの問題をそれぞれ次のように解決してくれています。



  • (a) Provider()builder にインスタンス生成処理を渡す


    • Provider の State 生成時(Widget ツリーに追加されるとき)に一度だけ実行してくれる

      指定例: builder: (context) => CounterBloc()




  • (b) Provider()dispose に破棄用メソッドを指定する


    • Provider が Widget ツリーから外されるタイミングで実行してくれる

      指定例: dispose: (context, bloc) => bloc.dispose()



  • (c) provider パッケージが無難な選択肢になった 12


BLoC パターンでなくても、インスタンスの生成と破棄を自動的にやってもらいつつ下位の Widget で値を利用したいケースでは役立つはずです。


blocs/counter_bloc.dart

class CounterBloc {

final _valueController = StreamController<String>();
Stream<String> get value => _valueController.stream;

int _number = 0;

void increment() {
_number++;
_valueController.sink.add(_number.toString());
}

void dispose() {
_valueController.close();
}
}



pages/provider.dart

class ProviderPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Provider<CounterBloc>(
builder: (_) => CounterBloc(),
dispose: (_, bloc) => bloc.dispose(),
child: Scaffold(
body: _CounterText(),
floatingActionButton: _floatingButton(),
),
);
}

Widget _floatingButton() {
return Consumer<CounterBloc>(
builder: (_, bloc, __) {
return FloatingActionButton(
onPressed: bloc.increment,
child: const Icon(Icons.add),
);
},
);
}
}

class _CounterText extends StatelessWidget {
@override
Widget build(BuildContext context) {
final bloc = Provider.of<CounterBloc>(context);

return StreamBuilder<String>(
stream: bloc.value,
builder: (context, snapshot) {
return Center(
child: Text(snapshot.data ?? '0'),
);
},
);
}
}


BLoC パターンについては、以前に関連記事を書いています。

一読されると理解しやすくなるかもしれません。


注意

builder の処理は一度しか呼ばれません。

値が変化するたびにプロバイダにセットし直したいケースでは .value という名前の付いたコンストラクタのほうを使いましょう。

これはほとんどのプロバイダで共通で、3種の ProxyProvider だけは例外です。

ProxyProviderbuilder は何度も呼ばれます。

代わりに、一度だけ呼ばれる初期生成用の initialBuilder が用意されています。


Provider.value()

Provider() の名前付きコンストラクタ版です。

先ほどと同じ CounterBloc を使ったサンプルにしてみました。

名前付きのほうは BLoC の dispose() が自動的には呼ばれません。

そこで代わりに State.dispose() を使って呼べるように StatefulWidget にしています。

このように Provider() を使った場合より少し手間が多くなります。

Provider.value() は先ほど 下位 Widget で値を得る方法 でご紹介したような単純な値の伝播に向いているようです。


pages/provider_value.dart

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);

return StreamBuilder<String>(
stream: bloc.value,
builder: (context, snapshot) {
return Center(
child: Text(snapshot.data ?? '0'),
);
},
);
}
}


このサンプルでは使いませんでしたが、次の引数もあります。



  • updateShouldNotify

    値に変更がなくても不要なリビルドが起こってしまうのを防ぐものです。

    必要に応じて指定しましょう。


StreamProvider.controller()

StreamProvider() からご紹介すべきかもしれませんが、v2 系で StreamProvider() だったものが v3.0.0 で StreamProvider.controller() に変わったため、先にこちらを説明します。


Provider()Stream 版のようなものです。

これもインスタンスの生成と破棄をうまく扱ってくれます。



  • builder には StreamController を渡す

  • Widget ツリーから外されるときに StreamController.close() が自動的に呼ばれる

注意が必要なのは次の点です。



  • StreamController<int>()builder に渡すからと言って StreamProvider<StreamController<int>>.controller とするわけではない(StreamProvider<int>.controller にする)

  • ツリーの下位にある Widget で値を得るときにも Provider.of<int>(context) とする

つまり、下位 Widget で得られるのは単なる int 型の値だということです。


しかし、使い方はわかってもあまりメリットが見えません。

次の二つくらいでしょうか…。:thinking: 13



  • StreamBuilder を使わないで済む

  • BLoC パターン等以外で Sink / Stream によって変更を通知するのに使える

また、StreamController のインスタンスを builder で生成せずに、先に生成して変数に入れなければならなかったのも少し腑に落ちません。

builder で生成したインスタンスを取得する方法がないと .sink.add() できないので、やむを得ずそうしました。


pages/stream_provider_controller.dart

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(
builder: (_) => 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()),
);
}
}




  • initialData

    Stream にまだ値が来ていない間の初期値(省略時は null)。

このサンプルでは使いませんでしたが、次の引数もあります。


  • catchError

    エラーが来たときのフォールバック用の値をここで返します。

    下位 Widget ではエラー時にそのフォールバック値が得られます。

    エラーデータが扱われる Stream であれば必須です。


  • updateShouldNotify

    Provider.value()updateShouldNotify と同様です。



StreamProvider()

builder で生成する対象が、v3.0.0 で StreamController から Stream に変わりました。

代わりに先ほどの StreamProvider.controller()StreamController 版として追加されています。

他のプロバイダではデフォルトコンストラクタに破棄の機能が備わっていますが、StreamProvider()ドキュメント にはそのことが書かれていません。

自分で StreamController.close() する必要があると思われます。14


pages/stream_provider.dart

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>(
builder: (_) => _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()),
);
}
}


builder に指定する対象が異なる点を除いて、引数は StreamProvider.controller() と同じです。


StreamProvider.value()

StreamProvider() とほとんど同じで、異なるのは次の点のみです。



  • builder はなく valueStream を指定する


StreamController.close() は自分で行う必要があります。


pages/stream_provider_value.dart

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()StreamProvider.controller() と同じです。


ChangeNotifierProvider()

scoped_model パッケージに似ていて、ほとんど同じ感覚で使えそうです。

モデルの状態を変更したときに notifyListeners() で通知するのも同じです。

次のようにして置き換えることができます。

モデルのインスタンスの生成と破棄はよしなに取り計らってくれます。

破棄のほうは Provider() と違って自分で dispose の引数がありません。

ChangeNotifierProvider が Widget ツリーから外されたときに自動的に破棄されます。


models/change_notifier_counter.dart

class CnCounter with ChangeNotifier {

int _number = 0;
int get number => _number;

void increment() {
_number++;
notifyListeners();
}
}



pages/change_notifier_provider.dart

class CnProviderPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<CnCounter>(
builder: (_) => CnCounter(),
child: Scaffold(
body: _CounterText(),
floatingActionButton: _FloatingButton(),
),
);
}
}

class _FloatingButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ボタンはカウンターの値の変更に反応する必要がないためlistenしないようにしておく
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 にも使えます。


ChangeNotifierProvider.value()

ChangeNotifierProvider() と異なるのは次の一点のみです。


  • モデルのインスタンスの生成・破棄は自己責任


change_notifier_provider_value.dart

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()

ValueListenable というと、そのインタフェースを実装した ValueNotifierValueListenableBuilder と組み合わせて値の変更を Widget に随時反映するのがよくある使い方です。

次の動画を観るほうがイメージしやすいと思います(2分弱の短い動画です)。

ValueListenableProvider() を使うと、ValueListenableBuilder を使わずに同じことが簡単にできます。

動画では InheritedWidget も組み合わせることが提案されていますが、provider パッケージは InheritedWidget のシンタックスシュガーなのでそれも不要です。

なお、Provider.of() で得られるのは builder で生成されたインスタンスではない点に注意が必要です。

ValueNotifier.value(本サンプルでは VnCounter.value)の値です。

その値を変更しても Notify されません(そもそも変更もできません)。

builder で生成したものは ChangeNotifierProvider() と同様に自動的に破棄してくれます。


models/value_notifier_counter.dart

class VnCounter extends ValueNotifier<int> {

VnCounter() : super(0);

void increment() => value++;
}



value_listenable_provider.dart

class VlProviderPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
final counter = VnCounter();

return Scaffold(
body: ValueListenableProvider<int>(
builder: (_) => 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()),
);
}
}


このサンプルでは使いませんでしたが、次の引数もあります。



  • updateShouldNotify

    Provider.value() 等の updateShouldNotify と同様です。


おさらい(ChangeNotifierProvider vs ValueListenableProvider)



  • ChangeNotifierProvider



    • ChangeNotifier にも ValueNotifier にも使える

    • 下位の Widget に伝わるのはインスタンス




  • ValueListenableProvider



    • ChangeNotifier には使えない

    • 下位の Widget に伝わるのは ValueNotifier.value の中身




ValueListenableProvider.value()

ValueListenableProvider() / ValueListenableProvider.value() の違いは、Provider() / Provider.value() の違いと同じです。


value_listenable_provider_value.dart

class VlProviderValuePage extends StatefulWidget {

@override
_VlProviderValueState createState() => _VlProviderValueState();
}

class _VlProviderValueState extends State<VlProviderValuePage> {
final _counter = VnCounter();

@override
Widget build(BuildContext context) {
return Scaffold(
body: ValueListenableProvider<int>.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 number = Provider.of<int>(context);

return Center(
child: Text(number.toString()),
);
}
}


引数は、v2 系で valueListenable だったのが v3.0.0 で value に変わりました。ご注意ください。


ListenableProvider()

このプロバイダだけはよくわかりませんでした。

ChangeNotifierProvider がこれを継承していて、たいていはそちらで済むように思えます。

もっと明らかになった場合には追記しようと思います。

2019/7/6

ChangeNotifierProviderValueListenableProvider に似たものです。

builder では Listenable を生成します。

Listenable を実装しているものとして、ドキュメント には次のように4つが挙げられています。


Implementers

Animation, ChangeNotifier, CustomPainter, ValueListenable


このうちの ChangeNotifierValueListenable は既に見た ChangeNotifierProviderValueListenableProvider で扱えますね。

その二つは ListenableProvider も使えるようです 16 が、基本的に使う必要はなさそうです。17

このプロバイダは特別な理由がなければ考えなくて良いと思います。

使う場合、破棄の処理を任せたいなら dispose を指定する必要があります。

この点は ChangeNotifierProvider 等よりわずかに手間です。


お詫び(2019/7/6)

サンプルを載せていましたが、このプロバイダを使う明確な理由がなかったため削除しました。

説明文も更新しました。


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> を取得し、その値を返す 18


という仕様によるものです。19

MultiProvider() に限ったことではありません。

ご注意ください。


FutureProvider()

ここからは v3.0.0 で追加された種類になります。

.value という名前付きのコンストラクタに関する説明はこの先省略します。


InheritedWidgetFutureBuilder を組み合わせたような便利なものです。

ここまでのサンプルと同じ動作にしたかったのですが、無理でしたので異なるものにしました。

flutter_future_provider.gif


  1. ページを表示すると「Wait for 3 seconds...」と表示される


  2. Future の処理が3秒後に完了する

  3. 自動的に表示が「3 seconds elasped.」に変わる


pages/future_provider.dart

class FutureProviderPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureProvider<String>(
builder: (_) =>
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 の処理が完了していない間の初期値(省略時は null)。

このサンプルでは使いませんでしたが、次の引数もあります。


  • catchError

    他のプロバイダの同名の引数と同様です。


  • updateShouldNotify

    他のプロバイダの同名の引数と同様です。


FutureProvider.value もありますが、省略します。


ProxyProvider()

下位 Widget で値を得る方法 のところで既に触れたものです。

これも同じ動作にするのが難しかったため、異なるサンプルにしました。

カウンターという点は同じですが、10進数と16進数の二つの表示になっています。

flutter_proxy_provider.gif


  1. 10進数と16進数のカウンターがある

    10進数は ValueListenableProvider() のサンプルで使ったものを流用。

    16進数のほうは10進数の値を受け取って保持する immutable なクラスであり、ゲッターで返すときに16進数に変換する。


  2. ChangeNotifierProvider() で10進数カウンターを生成


  3. ProxyProvider() で10進数の値を渡して16進数カウンターを生成

  4. 10進数カウンターの値が変わるたびに ProxyProviderbuilder が呼ばれる

    そのたびに16進数カウンターのインスタンスが生成される。

  5. 両カウンターのインスタンスが Widget ツリーの下位に伝播される

    10進数のカウントアップに連動して16進数の値も更新される。


models/hex_counter.dart

class HexCounter {

final int _decimal;

HexCounter(this._decimal);

String get valueString => _decimal.toRadixString(16);
}



pages/proxy_provider.dart

class ProxyProviderPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<VnCounter>(
builder: (_) => VnCounter(),
),
ProxyProvider<VnCounter, HexCounter>(
builder: (_, decCounter, __) => HexCounter(decCounter.value),
),
],
child: Scaffold(
body: _CounterResults(),
floatingActionButton: _FloatingButton(),
),
);
}
}

class _FloatingButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ボタンはカウンターの値の変更に反応する必要がないためlistenしないようにしておく
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.valueString),
],
),
);
}
}


他のプロバイダのインスタンスと異なり、builder は何度も呼ばれます。

代わりに、一度しか呼ばれない initialBuilder が用意されています。

前回に生成されたインスタンスを ProxyProvider の第3引数で受け取って再利用できるのですが、前回生成分がない初回のために initialBuilder で先に生成しておくことができます。

なお、ChangeNotifierProxyProvider()ListenableProxyProvider() では、provider 3.1.0 において initialBuilder が必須となりました。

ChangeNotifierProxyProvider<Foo, MyChangeNotifier>(

initialBuilder: (_) => MyChangeNotifier(),
builder: (_, foo, myNotifier) => myNotifier
..foo = foo,
child: ...
);

このコードは ChangeNotifierProxyProvider()ドキュメント からの引用したものです。

MyChangeNotifier のインスタンスを initialBuilder で一度だけ生成しています。

builder が呼ばれるたびに生成済みのインスタンスを第3引数で受け取って使い回すことになります。

ProxyProvider() では initialBuilder は必須ではありません。


InheritedProvider()

クラスのドキュメント に次のように書かれています。


Do not use this class directly unless you are creating a custom "Provider".

Instead use Provider class, which wraps InheritedProvider.


訳: カスタムなプロバイダを作る以外では InheritedProvider クラスを直接使わず、

   それをラップした Provider クラスを代わりに使うこと。

基本的に直接触る必要がないものです。


DI(依存オブジェクトの注入)

2019/7/1

provider の作者自身が言われているように DI にも使えるものです。5 6

この記事では省いてブログに書きました。


トラブルシューティング


遷移先のページで値を得られない

// A

Navigator.of(context).push(
MaterialPageRoute(builder: (context) {
// B
return NextPage();
}),
)

A 以上と B 以下ではコンテキストが異なっています。

A 以上の位置でプロバイダを使って値を渡そうとしても、遷移先ページで受け取れません。

次のように B の位置でプロバイダを使えば渡せます。

Navigator.of(context).push(

MaterialPageRoute(builder: (context) {
return Provider<Foo>.value(
value: foo,
child: NextPage(),
);
}),
)

ちょっと冗長な書き方になりますね。

プロバイダを使わずにコンストラクタに渡すほうがシンプルに書けます。

Navigator.of(context).push(

MaterialPageRoute(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() の中で行いましょう。

State のライフサイクル(各メソッドが呼ばれるタイミング)については次のページがわかりやすいです。


build()内で値を変えるとエラー

Widget の build() 内で ChangeNotifierValueNotifier を使って値を更新すると起こる現象です。

例をご覧ください。

class FooModel extends ValueNotifier<String> {

FooModel() : super('');
}

ChangeNotifierProvider<FooModel>(

builder: (_) => 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),
);
}
}



  1. FooModelvalueChangeNotifierProviderListen している

  2. 値が変わると、関連箇所をリビルドさせるために内部的に dirty という印が付けられる 20

  3. そのタイミングは 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()builder でも可能ですが、そこが OK なのは、その時点でまだビルドが始まっていないからだと思います。


  • ビルドが終わった後

    ChangeNotifierProvider() の子孫のビルド中に値を変えても例外が発生します。

    全子孫のビルドが終わったタイミングに処理をするには、下記のようなハックが必要です。21

    5つのうちどれを使っても、実行をビルド完了後に遅延させることができました。


// (1)

Future(() { /* 処理 */ });

// (2)

Future.delayed(Duration.zero, () { /* 処理 */ });

// (3)

WidgetsBinding.instance.addPostFrameCallback((_) { /* 処理 */ });

// (4)

// package:flutter/scheduler.dartのインポートが必要
SchedulerBinding.instance.addPostFrameCallback((_) { /* 処理 */ });

// (5)

// dart:asyncのインポートが必要
scheduleMicrotask((_) { /* 処理 */ });

initState()didChangeDependencies()build() のどこに書いてもビルド後になります。

全部使った場合、(5) が一番早く実行され、次に (1) か (2)、最後に (3) か (4) が来るようです。22

注意


  • 使う前によく検証しましょう。


  • initState() 以外はリビルドのたびに呼ばれ、値更新→リビルド→値更新… と繰り返してしまいます。

    フラグなどで制御する必要があると思います。


  • こちらのページ では WidgetsBinding を使えた人と使えなかった人がいます。



Selector() で複数の値を対象にしたい

これは 公式ドキュメント を見れば一目瞭然です。

Selector<Foo, Tuple2<Bar, Baz>>(

selector: (_, foo) => Tuple2(foo.bar, foo.baz),
builder: (_, data, __) {
return Text('${data.item1} ${data.item2}');
}
)

tuple を使うのが最も簡単な方法だと紹介されています。

上の例では foo.barfoo.baz の値が変わったときのみ builder が呼ばれます。





  1. 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, and package:scoped_model. There is a possibility that (some) of these efforts will merge. Learn more in issue #3." 



  2. 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 using provider instead. So don't use provide. Use provider." 



  3. https://pub.dev/packages/provider "provider is mostly syntax sugar for InheritedWidget, to make common use-cases straightforward." 



  4. 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." 



  5. https://pub.dev/packages/provider "A dependency injection system built with widgets for widgets." 



  6. 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" 



  7. https://www.reddit.com/r/FlutterDev/comments/bmrvey/so_is_provider_recommended_over_bloc_just_watched/emze9z6/ "explicit creation/ dispose" 



  8. 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: ..." 



  9. https://github.com/rrousselGit/provider/issues/127#issuecomment-508954636 



  10. ProxyProvider 導入が検討されていたときの「[RFC] Simplifying providers that depends on each others #46」という Issue を見るとよりわかりやすいです。 



  11. "It is usually used to avoid making a StatefulWidget for something trivial, such as instantiating a BLoC." 



  12. BLoC に特化した他の軽量なパッケージをあえて用いるのも良い選択だと思います。 



  13. .controller() ではなく .value() のほうについては、Firebase のユーザ認証に使う例が こちらの記事 に書かれています。プラグイン等によって用意される Stream をそのまま用いるようなケースに向いていそうです。 



  14. ソースコードをみると、builder の処理が BuilderStateDelegate というものに移譲され、そこの dispose() では builder によって生成された Stream の破棄が行われているようです。このときに StreamController が破棄されるわけではないはずです。 



  15. ScopedModelDescendant() に近い書き方になるのは Consumer() ですが、もちろん Provider.of() も使えます。 



  16. ややこしいですが、ChangeNotifierValueListenableListenable の実装であり、ValueNotifierChangeNotifier を継承 & ValueListenable を実装したものです。また、ChangeNotifierProviderListenableProvider を継承しています。 



  17. https://github.com/rrousselGit/provider/issues/39#issuecomment-483842355 



  18. "Obtains the nearest Provider<T> up its widget tree and returns its value." 



  19. 延いては、Provider.of() で使われている BuildContext.inheritFromWidgetOfExactType()BuildContext.ancestorInheritedElementForWidgetOfExactType() の仕様です。 



  20. Flutter in Focus の How Stateful Widgets Are Used Best - Flutter Widgets 101 Ep. 2 という動画が非常にわかりやすいです。 



  21. StackOverflow の ここここ が助けになりました。書かれている質問は別件だったりするのですが、回答のほうはビルド中の dirty 化の問題にも当てはまります。  



  22. 仕組みは難しくて私も理解が不十分ですが、公式の非同期処理のドキュメントこちらの動画 を見ると理解が進むと思います。この二つのリンク先は StackOverflow の ここ で知ったものです。