Providerってなんですか?

October 27, 2020

Providerってなんですか?

Providerは色々機能(各種Providerの種類)はありつつも、主に以下の用途で使え、状態管理をこれだけに頼ってアプリを組むことも可能です。

  • (不変な)インスタンスを受け渡す(DI・サービスロケーター的な用途)
  • 状態の変更を伝える

(https://medium.com/flutter-jp/state-1daa7fd66b94)

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

https://github.com/flutter/samples/blob/c6f6b5b757752da94daee570ce28a8724c94b92c/provider_shopper/lib/models/catalog.dart#L14

class CatalogModel {
  static const _itemNames = [
    'Code Smell',
    ...
    ...

CatalogModelはChangeNotifierを実装していない。

https://github.com/flutter/samples/blob/c6f6b5b757752da94daee570ce28a8724c94b92c/provider_shopper/lib/models/cart.dart#L8

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を呼ぶことになっている(おやくそく)。

https://github.com/flutter/samples/blob/c6f6b5b757752da94daee570ce28a8724c94b92c/provider_shopper/lib/main.dart

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(context))「cart.catalog = catalog;」として注入ってことだろうたぶん。

Provider.of

Provider.of<ほしいクラス>(context)で、contextを上に登って見つかった『ほしいクラス』のインタンスを取れる。

https://github.com/flutter/samples/blob/c6f6b5b757752da94daee570ce28a8724c94b92c/provider_shopper/lib/screens/cart.dart#L40

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を使ってリスト表示を作っている。

https://github.com/flutter/samples/blob/c6f6b5b757752da94daee570ce28a8724c94b92c/provider_shopper/lib/screens/catalog.dart#L35

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でボタンが押されたときの「カートに追加」の処理を行っている。

https://github.com/flutter/samples/blob/c6f6b5b757752da94daee570ce28a8724c94b92c/provider_shopper/lib/screens/catalog.dart#L70

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を切り出すかすれば同じこと、かな? 複数取りたいときはどうするのかな?

と思ってたら、

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と変更検知範囲の粒度をうまくやって、コードの可読性と変更検知&リビルドのパフォーマンスのバランスを取るってことかな。 はじめは荒く、問題になったら最適化、で。


Profile picture

Written by takeru You should follow them on Twitter