【Flutter】ChangeNotifier, ChangeNotifierProvider, Consumer, Provider.Ofメモ
状態を管理するときに便利なProvider
アプリの状態をさまざまな場所から変更する必要がある場合、大量のコールバックを渡さなければなりません。
provider
を使えば便利に解決できる。
3つのコンセプトを理解する必要があります。
- ChangeNotifier
- ChangeNotifierProvider
- Consumer
ChangeNotifier
ChangeNotifierの役割
ChangeNotifierは、Flutter SDKに含まれるシンプルなクラスで、リスナーに変更通知を提供する。
監視可能にさせるクラス。ChangeNotifierをextendsしているクラスは、インスタンスの中のメソッドが実行されるとchangeNotifierで知らせることができるようになる。
ChangeNotifier
に固有のコードは、notifyListeners()
の呼び出しだけです。アプリのUIを変更するようなモデルの変更があった場合は、このメソッドを呼び出す。
ChangeNotifierでカプセル化できる
provider
では、ChangeNotifier
は、アプリケーションの状態をカプセル化する一つの方法でもある。
(provider
でChangeNotifier
を使う必要はありませんが、簡単に使えるクラスです)
class CartModel extends ChangeNotifier {
/// カートの内部、プライベートな状態
final List<Item> _items = [];
/// カートの中のアイテムを変更できないように表示します。
// privateな変数_itemを含むUnmodifiableListView取得するためのgetter
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
/// すべてのアイテムの現在の合計金額(すべてのアイテムが42ドルであると仮定した場合)。
int get totalPrice => _items.length * 42;
/// [item]をカートに追加します。これと[removeAll]は、
/// 外部からカートを変更する唯一の方法です。
void add(Item item) {
_items.add(item);
// この呼び出しは、このモデルを聴いているウィジェットに再構築を指示します。
notifyListeners();
}
/// Removes all items from the cart.
void removeAll() {
_items.clear();
// This call tells the widgets that are listening to this model to rebuild.
notifyListeners();
}
}
ChangeNotifierProvider
役割
ChangeNotifierProvider
は、ChangeNotifier
のインスタンス(モデル)をその子孫に提供するウィジェット。
モデルを使いたいWidgetの上に置く。ただし、必要以上上には置かない。
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(),
),
);
}
Consumer
目的:
ChangeNotifierProvider
で提供されたモデルを使うためのWidget
これは、Consumerウィジェットを通じて行われます。
return Consumer<CartModel>(
builder: (context, cart, child) {
return Text("Total price: ${cart.totalPrice}");
},
);
使い方
- Consumerではアクセスしたいモデルのタイプを指定する必要があります。
-
Consumer
ウィジェットの唯一の必須引数はbuilderです。Builderは、ChangeNotifier
が変更されるたびに呼び出される関数です。(言い換えれば、モデルでnotifyListeners()
を呼び出すと、対応するすべてのConsumer
ウィジェットのbuilderメソッドが呼び出されます)
builderは3つの引数で呼び出されます。
- 最初の引数は
context
で、これはすべてのbuildメソッドでも取得できます。 - ビルダー関数の第2引数は、
ChangeNotifier
のインスタンスです。これは最初に求めていたものです。モデルのデータを使って、任意の時点でのUIがどうあるべきかを定義することができます。 - 3番目の引数は
child
ですが、これは最適化のためにあります。Consumer
の下に、モデルが変更されても変更されない大きなwidgetサブツリーがある場合、それを一度構築して、ビルダーを介して取得することができます。
return Consumer<CartModel>(
builder: (context, cart, child) => Stack(
children: [
// ここでSomeExpensiveWidgetを使うと、毎回再構築する必要がありません。
if (child != null) child,
Text("Total price: ${cart.totalPrice}"),
],
),
// Build the expensive widget here.
child: const SomeExpensiveWidget(),
);
-
Consumer
ウィジェットは、できるだけツリーの深いところに配置するのがよいでしょう。どこかの詳細が変更されたからといって、UIの大部分を再構築することは避けたいものです。
// DON'T DO THIS
return Consumer<CartModel>(
builder: (context, cart, child) {
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Text('Total price: ${cart.totalPrice}'),
),
);
},
);
Instead:
// DO THIS
return HumongousWidget(
// ...
child: AnotherMonstrousWidget(
// ...
child: Consumer<CartModel>(
builder: (context, cart, child) {
return Text('Total price: ${cart.totalPrice}');
},
),
),
);
必要なところのみconsumerを使う。
Provider.of
モデルの使うためのWidget。
Provider.of<CartModel>(context, listen: false).removeAll();
Consumerとの違い。
UIの変更などで、モデルのデータが必要ない場合(例えばカートの内容削除とか)に
listenパラメータをfalseにして、メソッドのみ呼び出すといい。
無駄なwidget の再構築がなくて済む。
上記の行をビルドメソッドで使用すると、notifyListeners
が呼ばれたときに、このウィジェットが再構築されません。
参考
状態とは
SPAやモバイルアプリなど昨今のフロントエンド開発において、フロントエンド側は多くの「状態」を保持し、「状態」に応じてUIを変化させます。
状態とは一言で言えば「アプリケーションが保持するデータ」のことですが、
・APIを通じて取得したサーバのデータ
・フォームに入力した文字列
・モーダルが開いている・閉じている
などサーバから取得した状態やUIに閉じた状態など様々なデータが含まれます。