LoginSignup
604
467

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-05-23

はじめに

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

使用例を探してみたのですが、scoped_model の代わりに ChangeNotifierProvider を使う例ばかりで、他の多数のプロバイダの情報はほとんど見当たりません。
各プロバイダの使用例について 要望 が上がっているので、そのうち公式に用意される可能性がありますが(結局なし)、まず自分で探り探りサンプルを作りながら調べてみました。
文字どおり「探り探り」ですので、誤りがあった場合にはご容赦ください。

2019/12/25 追記
投稿から一年が経った今では非常に有用なパッケージとしてメジャーになっています。

2020/7/31 追記
Flutter の 状態管理方法一覧ページ が半月ほど前に更新され、provider について「A recommended approach.」と記されました。

バージョン

この記事は 2021/3/6 更新時点で provider v5.0.0 に対応しています。

最近の大きな変化:

  • v3.2.0
    • ProxyProviderinitialBuilder/builder がそれぞれ create/update、他の各 Provider の buildercreate に変わり、元の引数名は deprecated となりました。
  • v4.0.0
    • v3.2.0 で deprecated となった引数名が完全廃止されました。
    • Provider.oflisten を指定しなくても使い分けてくれるようになりました。
      • デグレードが起こったため、すぐに v4.0.0-hotfix.1 で無くなりました。
    • StreamProvider.controller が廃止されました。
    • Selector でコレクションの深い比較が行われるようになりました。
    • SelectorshouldRebuild が追加され、リビルドの条件を指定できるようになりました。
    • 各 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 のデフォルトコンストラクタが廃止されました。
    • FutureProviderStreamProvider とその各々の .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 はあらかじめビルドされてから builderchild に渡されます。
これは 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 が交互にリビルドされました。
このように変更に応じる対象を絞れば、余計なリビルドを防いで無駄をなくせます。
flutter_provider_selector.gif

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つあります(Selector2Selector6)。
基底クラスである 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: ...,
)

同類として ChangeNotifierProxyProviderListenableProxyProvider も 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 等のハンドラで使うときにも注意が必要です。

read()の場合
Column(
  children: <Widget>[
    RaisedButton(
      child: ...,
      onPressed: () => context.read<CnCounter>().increment(),  // OK
    ),
    RaisedButton(
      child: ...,
      onPressed: context.read<CnCounter>().increment,  // ダメ
    ),
  ],
)
watch()の場合
Column(
  children: <Widget>[
    RaisedButton(
      child: ...,
      onPressed: () => context.watch<CnCounter>().increment(),  // ダメ
    ),
    RaisedButton(
      child: ...,
      onPressed: context.watch<CnCounter>().increment,  // OK
    ),
  ],
)

read() をビルド中に使えないのは、(確信はないですが)値の変化に応じたリビルドが必要な箇所で誤って read() を使ってしまうミスを防ぐためだと思われます。
ビルド中に使いたければ、これまで通り Provider.of(context, listen: false) を使いましょう。11

こちらの 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 で渡そうとした値を使うことはできず、BuilderConsumerSelector といった 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(),
)

サンプルと解説

よくあるカウンターのサンプルです。
右下のボタンをタップすると画面中央の値がインクリメントされます。

flutter_provider_examples.gif

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

Provider

何らかの型の値をツリーの下方に渡したいときに使える一番シンプルなプロバイダです。
BLoC パターンのためのプロバイダとしても使えます。
ドキュメント によれば、BLoC 等のために StatefulWidget を使う手間を省くのに使えます。14

そこで、このサンプルでも BLoC パターンを使ってみました。
BLoC パターンの学習までまだ進んでいない方にはわかりにくいかもしれませんが、

  • (a) BLoC をインスタンス化するタイミングに迷う
  • (b) BLoC が不要になったときに StatelessWidget だと自動で破棄されない
  • (c) BLoC パターンを実現しやすくするプロバイダのパッケージが乱立していた

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

  • (a) Providercreate にインスタンス生成処理を渡す
    • Provider の State 生成時(Widget ツリーに追加されるとき)に一度だけ実行してくれる
      • 指定例: create: (context) => CounterBloc()
  • (b) Providerdispose に破棄用メソッドを指定する
    • Provider が Widget ツリーから外されるタイミングで実行してくれる
      • 指定例: dispose: (context, bloc) => bloc.dispose()
  • (c) provider パッケージが無難な選択肢になった 15

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

blocs/counter_bloc.dart
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();
  }
}
pages/provider.dart
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 で値を得る方法 でご紹介したような単純な値の伝播に向いているようです。

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, 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 で廃止されました。
折り畳んで残しておきます。

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

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

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

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

  • StreamController<int>create に渡すからと言って StreamProvider<StreamController<int>>.controller とするわけではない(StreamProvider<int>.controller にする)
  • ツリーの下位にある Widget で値を得るときにも Provider.of<int>(context) とする

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


しかし、使い方はわかってもあまりメリットが見えません。
次の二つくらいでしょうか…。:thinking: 16

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

また、StreamController のインスタンスを create で生成せずに、先に生成して変数に入れなければならなかったのも少し腑に落ちません。
create で生成したインスタンスを取得する方法がないと .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(
        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()),
    );
  }
}
  • initialData
    • Stream にまだ値が来ていない間の初期値(省略時は null)。

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

  • catchError
    • エラーが来たときのフォールバック用の値をここで返します。
      下位 Widget ではエラー時にそのフォールバック値が得られます。
      エラーデータが扱われる Stream であれば必須です。
  • updateShouldNotify
    • Provider.valueupdateShouldNotify と同様です。

StreamProvider

create で生成する対象が、v3.0.0 で StreamController から Stream に変わりました。
代わりに先ほどの StreamProvider.controllerStreamController 版として追加されています。
StreamProvider.controller は provider v4.0.0 で廃止されました。

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

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>(
        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.valueupdateShouldNotify と同様です。

StreamProvider.value

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

  • create はなく 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 と同じです。

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>(
      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 を使いましたが、その派生クラス(ValueNotifierScrollController など)にも使えます。

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

v5.0.0 でこのデフォルトコンストラクタのほうだけ廃止されました。
廃止されていない .value のほうの理解に必要であれば下記を開いてご覧ください。

クリックで開閉

ValueListenable というと、そのインタフェースを実装した ValueNotifierValueListenableBuilder と組み合わせて値の変更を Widget に随時反映するのがよくある使い方です。
次の動画を観るほうがイメージしやすいと思います(2分弱の短い動画です)。

ValueListenableProvider を使うと、ValueListenableBuilder を使わずに同じことが簡単にできます。
動画では InheritedWidget も組み合わせることが提案されていますが、provider パッケージは InheritedWidget のシンタックスシュガーなのでそれも不要です。

なお、Provider.of() で得られるのは create で生成されたインスタンスではない点に注意が必要です。
ValueNotifier.value(本サンプルでは VnCounter.value)の値です。
その値を変更しても Notify されません(そもそも変更もできません)。

create で生成したものは 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>(
        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()),
    );
  }
}

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

  • 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 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

このうちの ChangeNotifierValueListenable は既に見た ChangeNotifierProviderValueListenableProvider で扱えますね。
その二つにはこの 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>.valueT の部分に 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 という名前付きのコンストラクタに関する説明はこの先省略します。


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>(
        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進数の二つの表示になっています。

flutter_proxy_provider.gif

  1. 10進数と16進数のカウンターがある
    • 10進数は ValueListenableProvider のサンプルで使ったものを流用。
    • 16進数のほうは10進数の値を受け取って保持する immutable なクラスであり、ゲッターで返すときに16進数に変換する。
  2. ChangeNotifierProvider で10進数カウンターを生成
  3. ProxyProvider で10進数の値を渡して16進数カウンターを生成
  4. 10進数カウンターの値が変わるたびに ProxyProviderupdate が呼ばれる
    • そのたびに16進数カウンターのインスタンスが生成される。
  5. 両カウンターのインスタンスが Widget ツリーの下位に伝播される
    • 10進数のカウントアップに連動して16進数の値も更新される。
models/hex_counter.dart
class HexCounter {
  int _decimal;

  HexCounter();

  set newValue(int newValue) => _decimal = newValue;

  String get hex => _decimal.toRadixString(16);
}
pages/proxy_provider.dart
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引数で受け取って再利用することができ、再利用して更新した値を下位に伝えたいときに役立ちます。

なお、ChangeNotifierProxyProviderListenableProxyProvider では、provider v3.1.0 において create が必須となりました。

※v3.1.0 時点では create ではなく initialBuilderupdate ではなく 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 に使うことができます。

早速ですが用例を見てみましょう。

read()のドキュメントに書かれているLocatorの使用例
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 のライフサイクル(各メソッドが呼ばれるタイミング)については次のページがわかりやすいです。

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() 内で ChangeNotifierValueNotifier を使って値を更新すると起こる現象です。
例をご覧ください。

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),
    );
  }
}
  1. FooModelvalueChangeNotifierProviderListen している
  2. 値が変わると、関連箇所をリビルドさせるために内部的に dirty という印が付けられる 23
  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 を付けないことです。
そう考えると、代わりのタイミングは次の二つになります。

  • ビルドより前(祖先がビルドされている間)
    FooModelvalue の変更によって dirty が付く Widget がビルドされる前という意味です。
    つまり ChangeNotifierProvider より前である必要があります。
    ChangeNotifierProvidercreate でも可能ですが、そこが 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.barfoo.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をネストした場合
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の場合
MultiProvider(
  Provider<Foo>(
    create: (_) => Foo(),
  ),
  Provider<Bar>(
    create: (context) {
      final foo = Provider.of<Foo>(context, listen: false);
      return Bar(foo);
    },
  ),
  child: ...,
)
  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. read() の制約が厳しい理由を理解した上であれば、あえて制約のない context.value() (名前は適当)のような extension を自作して使うのは良いかもしれません。

  12. リビルドは再描画ではないので、多少の無駄なリビルドが体感やバッテリ消費に大きく影響するものでもありません。

  13. 更新履歴 には Builder と同じ振る舞いだと書かれていますが、少しだけ異なると思います。BuilderSelector 等では Baz はその孫なので Foo の値が更新されるたびにリビルドされますが、builder を使う場合は Baz は ChangeNotifierProvider の子であり、あらかじめビルドしてから使用されるのでリビルドされないはずです。

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

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

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

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

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

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

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

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

  22. 個人的には、Model がどのようなオブジェクトを使うのかが渡す側から見てわかりにくいと感じました。また、単体テスト等で T Function<T>() 型にして渡す手間も生じます。一方で、依存が増えても渡す側を変更する手間をなくせます。そのようなメリットとデメリットのトレードオフになります。

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

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

604
467
13

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
604
467