こんにちは。最近Flutterを勉強中です。その中でもとても便利に思えるProviderですが、理解にだいぶ時間がかかりました。今回は頭を整理する意味もこめて、2021年9月現在のProviderの使い方について、書いていきます。
今回は、以下のアプリを元に話を進めていきます。

全体のコードは最後に記しますので、まずは順を追って説明していきます。
Providerとは
Flutterにおける、状態監視のためのパッケージです。他で図解されているサイトなどもありますので、合わせてご参照ください。
ConsumerとSelector
状態監視は、データを保持したProviderが好きなタイミングでウィジェットに通知を送信することにより行われます。ところが、データが更新されればいいやと何でもかんでも通知を送信していては、負荷がかかりアプリの動作が重くなる原因になってしまいます。そこでConsumerやSelectorを使い、選択的に通知を送るウィジェットを限定するわけです。
通知を受けるための判定式 | 受け取れる変数 | |
---|---|---|
Consumer | ❌ | Providerのインスタンス |
Selector | 🔵 | 一つの変数 |
Consumerは判定式がなく細かいことは出来ない反面、Providerのインスタンス自体を受け取れます。そのため、自由の効いた動作を実行出来ます。
一方Selectorは判定式を用いることでより細かい通知制御が出来る反面、受け取れる変数が一つだけと限定されています。ただしやり方によっては、Providerのインスタンスを使用することも可能です。またTuple2やTuple3などを用いて、複数の変数を取得することもできます。Tuple2などを用いることで、Selectorでもかなり自由の効いた設計ができます。
Providerを作る
まずはProviderを設計します。ChangeNotifierを継承したクラスで、状態監視のための変数を保持しています。
class MyProvider extends ChangeNotifier {
int test1 = 0;
int test2 = 10;
int test3 = 100;
static late MyProvider instance;
MyProvider() {
//自分自身をクラス変数に格納。
//どこからでもインスタンスにアクセスできるようになる
instance = this;
}
void increment(int i) {
switch (i) {
case 1:
test1 += 1;
break;
case 2:
test2 += 1;
break;
case 3:
test3 += 1;
break;
}
//更新を通知する
notifyListeners();
}
}
ここでは初期化時にインスタンスを中に格納しています。格納先はクラス変数ですので、どこからでも参照可能になっています。テスト用に作成したincrement関数では、最後にnotifyListeners()によって下位のウィジェットに更新通知を送っています。
プロバイダーをウィジェットに追加
続いてプロバイダーのインスタンスを、ウィジェットツリーに追加します。出来るだけ上位に入れたいため、RunAppのすぐで設定しています。細かい調整はConsumerやSelectorで行えるため、おそらく問題ありません。
void main() {
//MyProviderインスタンスを生成
MyProvider provider = MyProvider();
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<MyProvider>(create: (context) {
//普通はここで次のようにインスタンスを生成する
//return MyProvider();
//だが、あらかじめ外でインスタンスを生成した方が安全
return provider;
}),
],
child: MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("test"),
),
body: HomeWidget(),
),
),
));
}
追記(2021年9月14日)
ドキュメントに以下の記述がありました。今回作ったコードは、やってはいけない書き方のようです。
DO use ChangeNotifierProvider.value to provide an existing ChangeNotifier.
MyChangeNotifier variable;
ChangeNotifierProvider.value(
value: variable,
child: ...
)
DON'T reuse an existing ChangeNotifier using the default constructor
MyChangeNotifier variable;
ChangeNotifierProvider(
create: (_) => variable,
child: ...
)
MultiProviderのprovidersリストの中に入れています。その際は、ChangeNotifierProvider<(作ったプロバイダの型)>を使用しています。今回は一つだけですが、リストになっていますので複数のプロバイダを入れることもできます。
ChangeNotifierProviderの中でインスタンスを生成して返すのが一般的かと思われますが、そうするとインスタンス生成のタイミングが読めません。実際、今回のコードでそれを行うと、インスタンス生成前に参照することとなってしまい、エラーでアプリが動きませんでした。ですので、あらかじめインスタンスを生成しています。今回main関数の中で生成を行いましたが、関数の外側で「_muProviderInstance=MyProvider」のようにしてから、ChangeNotifierProviderに入れた方がより安全かもしれません。
ConsumerとSelectorで受ける
最後にProviderが送信する通知を、ConsumerやSelectorの中で受け取ります。
以下は、テキストウィジェットとボタンウィジェットを作るための便宜上使用する関数です。
Widget myText(String str) {
return Text(
"$str\n",
style: TextStyle(fontSize: 18),
);
}
Widget myButton(int i) {
return ElevatedButton(
child: Text("ボタン test$i++"),
onPressed: () {
MyProvider.instance.increment(i);
},
);
}
使い方の基本
続いて、SelectorとConsumerです。Selectorは次のように使用します。説明の都合上、動かないソースになっています。
Selector<(作ったプロバイダー型), (監視する変数の型)>(
selector: (context, provider) => provider.(監視する変数),
builder: (BuildContext context, (監視する変数の型) value, Widget? child) {
(下位のウィジェット)
}
Consumerは次のようになっています。こちらも、動かないソースとなっています。
Consumer<(作ったプロバイダー型)>(
builder: (BuildContext context, (作ったプロバイダー型) provider, Widget? child) {
(下位のウィジェット)
}
実際
お待たせしました。以上を踏まえた、動くソースの一部です。今回のアプリの全体は、この記事の最後に載せます。
class HomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(children: [
myText("初期値。SelectorやConsumerで囲ってないため更新されない\n"
"test1=${MyProvider.instance.test1}\n"
"test2=${MyProvider.instance.test2}\n"
"test3=${MyProvider.instance.test3}"),
//ここからSelector
//Selector<(プロバイダ型),(受け取る変数型)>
Selector<MyProvider, int>(
//test1を監視
selector: (context, provider) => provider.test1,
builder: (BuildContext context, int i, Widget? child) {
//test1が渡される
return myText("Selectorでtest1を監視。\n"
"test1=$i\n"
"クラス変数instanceからtest2も取れる。\n"
"MyProvider.instanceからのtest2=${MyProvider.instance.test2}");
},
),
Selector<MyProvider, int>(
//test2を監視
selector: (context, provider) => provider.test2,
builder: (BuildContext context, int i, Widget? child) {
//test2が渡される
//MyProviderインスタンスからtest1にもアクセスできる
return myText("Selectorでtes2を監視。\n"
"test2=$i\n"
"クラス変数instanceからtest1も取れる。\n"
"MyProvider.instanceからのtest1=${MyProvider.instance.test1}");
},
),
Consumer<MyProvider>(
builder: (BuildContext context, MyProvider provider, Widget? child) {
int i1 = provider.test1;
int i2 = provider.test2;
int i3 = provider.test3;
return myText("ConsumerでMyProviderインスタンスを監視\n"
"test1=$i1, test2=$i2, test3=$i3");
},
),
myButton(1),
myButton(2),
myButton(3)
]);
}
}
まとめ
いかがでしたでしょうか。Selectorは条件に変数を指定できますので、かなり詳細な作りこみが出来るかと思います。今回の方法で行えば、しっかりインスタンスもとれますから、自由度も高いです。条件に指定する変数も、Mapなどにしてしまえば複数変数も使えますからね。
最後に、今回のアプリの全体を載せて終わらせていただきます。ありがとうございました。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MyProvider extends ChangeNotifier {
int test1 = 0;
int test2 = 10;
int test3 = 100;
static late MyProvider instance;
MyProvider() {
//自分自身をクラス変数に格納。
//どこからでもインスタンスにアクセスできるようになる
instance = this;
}
void increment(int i) {
switch (i) {
case 1:
test1 += 1;
break;
case 2:
test2 += 1;
break;
case 3:
test3 += 1;
break;
}
//更新を通知する
notifyListeners();
}
}
void main() {
//MyProviderインスタンスを生成
MyProvider provider = MyProvider();
runApp(MultiProvider(
providers: [
ChangeNotifierProvider<MyProvider>(create: (context) {
//普通はここで次のようにインスタンスを生成する
//return MyProvider();
//だが、あらかじめ外でインスタンスを生成した方が安全
return provider;
}),
],
child: MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("test"),
),
body: HomeWidget(),
),
),
));
}
Widget myText(String str) {
return Text(
"$str\n",
style: TextStyle(fontSize: 18),
);
}
Widget myButton(int i) {
return ElevatedButton(
child: Text("ボタン test$i++"),
onPressed: () {
MyProvider.instance.increment(i);
},
);
}
class HomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(children: [
myText("# 初期値。SelectorやConsumerで囲ってないため更新されない\n"
"test1=${MyProvider.instance.test1}\n"
"test2=${MyProvider.instance.test2}\n"
"test3=${MyProvider.instance.test3}"),
//ここからSelector
//Selector<(プロバイダ型),(受け取る変数型)>
Selector<MyProvider, int>(
//test1を監視
selector: (context, provider) => provider.test1,
builder: (BuildContext context, int i, Widget? child) {
//test1が渡される
return myText("# Selectorでtest1を監視。\n"
"受け取った変数 test1=$i\n"
"## クラス変数のinstanceからも取れる。\n"
"インスタンスのtest1=${MyProvider.instance.test1}\n"
"インスタンスのtest2=${MyProvider.instance.test2}");
},
),
Selector<MyProvider, int>(
//test2を監視
selector: (context, provider) => provider.test2,
builder: (BuildContext context, int i, Widget? child) {
//test2が渡される
//MyProviderインスタンスからtest1にもアクセスできる
return myText("# Selectorでtes2を監視。\n"
"受け取った変数 test2=$i\n"
"## クラス変数のinstanceからも取れる。\n"
"インスタンスのtest1=${MyProvider.instance.test1}\n"
"インスタンスのtest2=${MyProvider.instance.test2}");
},
),
Consumer<MyProvider>(
builder: (BuildContext context, MyProvider provider, Widget? child) {
int i1 = provider.test1;
int i2 = provider.test2;
int i3 = provider.test3;
return myText("# ConsumerでMyProviderインスタンスを監視\n"
"test1=$i1, test2=$i2, test3=$i3");
},
),
myButton(1),
myButton(2),
myButton(3)
]);
}
}