[Flutter] Providerの使い方を理解する


ProviderパッケージはInheritedWidgetのラッパーライブラリです。下位ツリーのWidgetから上位ツリーのWidgetが管理する状態にアクセスする手段を提供します。Google I/O'19でも紹介されており、非常にメジャーなパッケージとなっています。

https://youtu.be/d_m5csmrf7I

実装

サンプルソースは以下に置いています。

https://github.com/NAOYA-MAEDA-DEV/flutter_provider_sample

「Increment Count A/B/C」ボタンをタップすると"Counter A"、"Counter C"の表示が更新されるようになっています。動作検証の為"Counter B"の表示は更新しないようにしています。

サンプルソースの解説

Providerをインストールする

pubspec.yamlを編集してProviderをインストールします。dependenciesprovider: ^6.0.2を追記してPub Getを実行してください。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.2

ChangeNotifierを継承した状態クラスを作る

counter.dart
class Counter extends ChangeNotifier {
  var countA = 0;
  var countB = 0;
  var countC = 0;

  void incrementCounterA() {
    countA++;
    notifyListeners();
  }

  void incrementCounterB() {
    countB++;
    notifyListeners();
  }

  void incrementCounterC() {
    countC++;
    notifyListeners();
  }
}
  • ChangeNotifier
    ChangeNotifierはリスナーに変更通知を行う機能を提供しているクラスです。CounterクラスはChangeNotifierを継承することでnotifyListenersを使用することができるようになっています。
  • notifyListeners
    リスナーに変更通知を行います。

countA/B/Cが下位ツリーのウィジェットからアクセスする状態変数となります。incrementCounterA/B/CメソッドはcountA/B/Cそれぞれをインクリメントするメソッドです。

状態クラスを上位ツリーに配置する

main.dart
ChangeNotifierProvider(
  create: (context) => Counter(),
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: const [
      TextWidgetA(),
      TextWidgetB(),
      TextWidgetC(),
      ButtonWidgetA(),
      ButtonWidgetB(),
      ButtonWidgetC()
    ],
  ),
),
  • ChangeNotifierProvider
    上位ツリーにChangeNotifierProviderを配置します。createにはChangeNotifierを継承したオブジェクトを返す関数を指定します。childに下位ツリーとなるWidgetを配置していきます。

下位ツリーのWidgetから上位ツリーの状態クラスにアクセスする

context.watchcontext.readcontext.selectのいずれかを使用して上位ツリーの状態クラスにアクセスします。

  • context.watch
main.dart
class WidgetA extends StatelessWidget {
  const WidgetA({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    print('Built WidgetA');

    return Center(
        child: Text(
            'Counter A: ${context.watch<Counter>().countA}',
            style: const TextStyle(
                fontSize: 20
            )
        )
    );
  }
}

context.watchは状態クラスの変化を監視します。状態クラスのnotifyListenersが実行されると、context.watchを使用しているウィジェットに対して変更が通知され、ウィジェットがリビルドされます。

  • context.read
main.dart
class TextWidgetB extends StatelessWidget {
  const TextWidgetB({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    print('Built TextWidgetB');

    return Center(
        child: Text(
            'Counter B: ${context.read<Counter>().countB}',
            style: const TextStyle(
                fontSize: 20
            )
        )
    );
  }
}

context.readは状態クラスの変化を監視しません。状態クラスのnotifyListenersが実行されても、context.readを使用しているウィジェットに対して変更通知が行われず、ウィジェットがリビルドされません。

main.dart
context.read<Counter>().incrementCounterA()

ButtonWidgetA/B/C内でcontext.readを通してincrementCounterA/B/Cを呼び出しています。状態クラスのメソッドを呼び出す時はcontext.readを使用します。

  • context.select
main.dart
class TextWidgetC extends StatelessWidget {
  const TextWidgetC({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    print('Built TextWidgetC');

    return Center(
        child: Text(
            'Counter C: ${context.select((Counter counter) => counter.countC)}',
            style: const TextStyle(
                fontSize: 20
            )
        )
    );
  }
}

context.selectは状態クラスが持つ特定のオブジェクトの変化を監視します。状態クラスのnotifyListenersが実行されると、context.selectで指定したcountCの状態が変化した時のみウィジェットに対して変更が通知され、ウィジェットがリビルドされます。

実行結果

「Increment Count A」ボタンをタップしてみます。CounterオブジェクトのincrementAが実行され、countAがインクリメントされます。TextWidgetAではcontext.watchを使用してCounterオブジェクトにアクセスしているため、Counterオブジェクトに変更が入るとTextWidgetAがリビルドされ、「Increment Count A」ボタンをタップした回数の表示が更新されます。

次に「Increment Count B」ボタンをタップしてみます。CounterオブジェクトのincrementBが実行され、countBがインクリメントされます。TextWidgetBではcontext.readを使用してCounterオブジェクトにアクセスしているため、Counterオブジェクトに変更が入ってもTextWidgetBはリビルドされず、「Increment Count B」ボタンをタップした回数の表示が更新されません。

最後に「Increment Count C」ボタンをタップしてみます。CounterオブジェクトのincrementCが実行され、countCがインクリメントされます。TextWidgetCではcontext.selectを使用してCounterオブジェクトにアクセスしています。また、countCの変更を監視するようにしているため、countCに変更が入るとTextWidgetCがリビルドされ、「Increment Count C」ボタンをタップした回数の表示が更新されます。

もう少し踏み込んで、TextWidgetA/TextWidgetB/TextWidgetCがリビルドされるタイミングも確認します。「Increment Count A」ボタンをタップしてみます。TextWidgetAがリビルドされたことにより、コンソールに"flutter: Built TextWidgetA"が表示されました。

次に「Increment Count B」ボタンをタップしてみます。TextWidgetBがリビルドされず、コンソールに"flutter: Built TextWidgetA"のみが表示されました。

TextWidgetBではcontext.readを使用してCounterオブジェクトにアクセスしたため、Counterオブジェクトに変更が入ってもTextWidgetBには変更が通知されずTextWidgetBはリビルドされません。しかしTextWidgetAではcontext.watchを使用してCounterオブジェクトにアクセスしたため、counterBがインクリメントされたことでCounterオブジェクトに変更が入ったことが検知されます。その結果、「Increment Count B」ボタンをタップした時はTextWidgetAのみ変更が通知され、TextWidgetAはリビルドされています。

最後に「Increment Count C」ボタンをタップしてみます。countCがインクリメントされることによりCounterオブジェクトが変更され、TextWidgetAがリビルドされます。また、countCがインクリメントされたことにより、countCを監視していたTextWidgetCがリビルドされます。その結果、コンソールに"flutter: Built TextWidgetA"、"flutter: Built TextWidgetC"と表示されました。