Providerってなんですか?
Providerは色々機能(各種Providerの種類)はありつつも、主に以下の用途で使え、状態管理をこれだけに頼ってアプリを組むことも可能です。
- (不変な)インスタンスを受け渡す(DI・サービスロケーター的な用途)
- 状態の変更を伝える
InheritedWidget を使いやすくしてミスを防ぐためのシンタックスシュガー DI の仕組みを提供 インスタンスの生成と破棄を助ける(StatelessWidget でも dispose() が可能になる等) その他何でも(Scoped Model、BLoC 等による状態管理、ValueNotifier 等による Widget 更新など) (https://qiita.com/kabochapo/items/a90d8438243c27e2f6d9)
https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple https://github.com/flutter/samples/blob/c6f6b5b757/provider_shopper/lib/main.dart
class CatalogModel {
static const _itemNames = [
'Code Smell',
...
...
CatalogModelはChangeNotifierを実装していない。
class CartModel extends ChangeNotifier { //★★★
/// The private field backing [catalog].
CatalogModel _catalog;
/// Internal, private state of the cart. Stores the ids of each item.
final List<int> _itemIds = [];
/// The current catalog. Used to construct items from numeric ids.
CatalogModel get catalog => _catalog;
set catalog(CatalogModel newCatalog) {
assert(newCatalog != null);
assert(_itemIds.every((id) => newCatalog.getById(id) != null),
'The catalog $newCatalog does not have one of $_itemIds in it.');
_catalog = newCatalog;
// Notify listeners, in case the new catalog provides information
// different from the previous one. For example, availability of an item
// might have changed.
notifyListeners();
}
/// List of items in the cart.
List<Item> get items => _itemIds.map((id) => _catalog.getById(id)).toList();
/// The current total price of all items.
int get totalPrice =>
items.fold(0, (total, current) => total + current.price);
/// Adds [item] to cart. This is the only way to modify the cart from outside.
void add(Item item) {
_itemIds.add(item.id);
// This line tells [Model] that it should rebuild the widgets that
// depend on it.
notifyListeners(); //★★★
}
}
CartModelはChangeNotifierを実装している。 内容が変わったときにnotifyListenersを呼ぶことになっている(おやくそく)。
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Using MultiProvider is convenient when providing multiple objects.
return MultiProvider(
providers: [
// In this sample app, CatalogModel never changes, so a simple Provider
// is sufficient.
Provider(create: (context) => CatalogModel()), //★★★
// CartModel is implemented as a ChangeNotifier, which calls for the use
// of ChangeNotifierProvider. Moreover, CartModel depends
// on CatalogModel, so a ProxyProvider is needed.
ChangeNotifierProxyProvider<CatalogModel, CartModel>( //★★★
create: (context) => CartModel(),
update: (context, catalog, cart) {
cart.catalog = catalog;
return cart;
},
),
],
child: MaterialApp(
title: 'Provider Demo',
theme: appTheme,
initialRoute: '/',
routes: {
'/': (context) => MyLogin(),
'/catalog': (context) => MyCatalog(),
'/cart': (context) => MyCart(),
},
),
);
}
}
Widgetツリーの一番上あたりでCatalogModelとCartModelのインスタンスを生成している。
CartModelはCatalogModelに依存しているので、上で生成したCatalogModelを取ってきて(内部でProvider.of
Provider.of
Provider.of<ほしいクラス>(context)で、contextを上に登って見つかった『ほしいクラス』のインタンスを取れる。
class _CartList extends StatelessWidget {
@override
Widget build(BuildContext context) {
var itemNameStyle = Theme.of(context).textTheme.title;
var cart = Provider.of<CartModel>(context); //★★★
return ListView.builder(
itemCount: cart.items.length,
itemBuilder: (context, index) => ListTile(
leading: Icon(Icons.done),
title: Text(
cart.items[index].name,
style: itemNameStyle,
),
),
);
}
}
cart.itemsを使ってリスト表示を作っている。
class _AddButton extends StatelessWidget {
final Item item;
const _AddButton({Key key, @required this.item}) : super(key: key);
@override
Widget build(BuildContext context) {
var cart = Provider.of<CartModel>(context); //★★★
return FlatButton(
onPressed: cart.items.contains(item) ? null : () => cart.add(item),
splashColor: Theme.of(context).primaryColor,
child: cart.items.contains(item)
? Icon(Icons.check, semanticLabel: 'ADDED')
: Text('ADD'),
);
}
}
- cart.itemsに含まれているか(contains)を見てボタンの切り替えをしている。
- cart.addでボタンが押されたときの「カートに追加」の処理を行っている。
class _MyListItem extends StatelessWidget {
final int index;
_MyListItem(this.index, {Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
var catalog = Provider.of<CatalogModel>(context); //★★★
var item = catalog.getByPosition(index);
var textTheme = Theme.of(context).textTheme.title;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: LimitedBox(
maxHeight: 48,
child: Row(
children: [
AspectRatio(
aspectRatio: 1,
child: Container(
color: item.color,
),
),
SizedBox(width: 24),
Expanded(
child: Text(item.name, style: textTheme),
),
SizedBox(width: 24),
_AddButton(item: item),
],
),
),
);
}
}
「model=Provider.ofを呼んだbuildが属するWidgetはmodelが更新されたらリビルドされます」 むずかしい。。。
具体的には。。。
-
CartList,AddButtonのbuildの中で『var cart = Provider.of
(context);』としている。 - cartの内容を使って画面表示を作っている。(カート内リストの内容や数,ボタンのラベルの切り替えなど)
- このWidget(CartList,AddButton)はcartが更新されたらリビルド(=表示を最新に更新)してほしい。
- → Providerさん「Provider.of(listen=true)したのでデータが更新されたらリビルドしますね!!」
-
CartModel(extends ChangeNotifier)のaddでnotifyListenersが呼ばれる。
- ChangeNotifierProviderが変更を受け取る。
- → 「データが更新されたそうなのでさっきのProvider.ofしたところのWidget、リビルドします!」
-
TODO
- Provider.ofしたリビルド対象のWidgetを覚えている仕組みが謎。
- ProviderがWidget更新する仕組みが謎。特にStatelessWidget。
Consumer
class _CartTotal extends StatelessWidget {
@override
Widget build(BuildContext context) {
var hugeStyle = Theme.of(context).textTheme.display4.copyWith(fontSize: 48);
return SizedBox(
height: 200,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Consumer<CartModel>(
builder: (context, cart, child) =>
Text('\$${cart.totalPrice}', style: hugeStyle)),
SizedBox(width: 24),
FlatButton(
onPressed: () {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('Buying not supported yet.')));
},
color: Colors.white,
child: Text('BUY'),
),
],
),
),
);
}
}
contextが無い場合にConsumer<ほしいクラス>でProvider.ofのようにインスタンスを取れるWidget。 Builder Widget使うかWidgetを切り出すかすれば同じこと、かな? 複数取りたいときはどうするのかな?
と思ってたら、
- childを使うとリビルドを最適化できる
- Consumer2() から Consumer6()まである
- Consumer をより便利にした Selector がある (https://qiita.com/kabochapo/items/a90d8438243c27e2f6d9)
read,watchという新しいのがある
- context.read() ≒ Provider.of(context, listen: false)
- context.watch() ≒ Provider.of(context, listen: true)
-
context.select()
final value = context.select((Foo foo) => foo.value); Text(value.toString());
大きなクラスのある一部だけの変化だけを検知→リビルドできる。これは良さそう。 ChangeNotifierProviderに大きなモデルを乗っけてselectで指定したところが変わったときだけrebuildされる。 「モデルの『どこ』が変わったら〜」の『どこ』をWidget側に寄せることができる。
まとめ
いろんななんとかProviderがあるけど、データソースによっていろいろ使えるよってことみたい。 基本はChangeNotifierってことでいいのかな。
Widgetと変更検知範囲の粒度をうまくやって、コードの可読性と変更検知&リビルドのパフォーマンスのバランスを取るってことかな。 はじめは荒く、問題になったら最適化、で。