【Flutter】ProviderにおけるConsumerについて解説してみたよ vol.2

Dart

追記情報

lintに対応した形式に修正しました。 2020.09.29

概要


「Consumerってbuilderだけコンストラクタの引数に渡せば良いんじゃないの?」
「第三引数のWidgetって何?」
「Consumerのコンストラクタの引数のchildって何を渡せば良いの?」

この記事は、主にこういった疑問にお答えする事を目的に書いています。

※この記事はProvider解説の第三弾の記事となっています。
まだお読みになっていない方はそちらからお読みいただけると嬉しいです。

【Flutter】Providerについてサンプル付きで解説してみた
追記情報 lintに対応した書式に変更しました。 (2020.09.29) 概要 「FlutterのProvider?」 「ChangeNotifier?Consumer?何それ美味しいの?」 あなたは、こ...
【Flutter】ProviderにおけるConsumerをサンプル付きで解説してみたよ
追記情報 lintに対応した形式に修正しました。 2020.09.29 概要 前回の記事で、Providerに関して詳しく説明しました。 今回の記事はこちらの記事での知識を前提に書かれているので、 Provider...

※今回の解説分もGitHubにコミットしてあります。

daiki1003/bookshelf_sample
Bookshelf sample. See Contribute to daiki1003/bookshelf_sample development by creating an account on GitHub.

それでは始めましょー!

…とその前に追加要素のご説明

今回の記事のために少し修正しました。

ListTileのデザインを少し変更しました。

それでは気を取り直して行ってみましょう!

どんな改善点があるのか?

どんな改善点があるでしょうか?
少し考えてみましょう。

わかりましたか?

BookItemはBook単体を扱っているのでBooks全体をnotifyされる必要はない

というのが一つ僕の考えです。

これを元に前回のコードを書き直していきましょう。

(仮に今回のブログ内で変更された点以外の問題を思いついた天才な方は
コメントか@daiki1003に教えていただけると嬉しいです!)

BookをChangeNotifierに適用させる

class Book with ChangeNotifier {
  final String id;
  final String title;
  bool isFavorite = false;

  Book(this.id, this.title);

  void toggleFavorite() {
    isFavorite = !isFavorite;
    notifyListeners();
  }
}

BookクラスをChangeNotifierに適用させます。
これで、単体のBookの変更を通知する事が出来る様になりました。

ChangeNotifierProvider.valueを使ってみる

class Bookshelf extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final booksData = Provider.of<Books>(context);
    return ListView.builder(
      itemCount: booksData.books.length,
      itemBuilder: (ctx, index) => ChangeNotifierProvider.value(
        value: booksData.books[index],
        child: BookItem(),
      ),
    );
  }
}

前回、 ChangeNotifierProvider(create:child:)コンストラクタを使って
ChangeNotifierProviderウィジェットを生成しました。
今回はChangeNotifier.value(value:child:)を使います。

もちろん生成されるのはChangeNotifierProviderインスタンスです。

では、何が違うのか?
引数に与えるのはvalueとchildです。
valueには既に生成されているChangeNotifierが適用されたインスタンスを渡します。

ChangeNotifierProvider(create:child:)の時は、Booksインスタンスを生成して渡しましたよね?
createして渡しているのです。
だから、引数がcreateなのですね。

ChangeNotifierProvider.valueは既に存在しているインスタンスをvalueとして渡します。

他に違いはありません。
これで、BookItemにBookの変更が通知可能となりました。

ChangeNotofierProvider.valueで渡されたBookインスタンスをBookItemで受け取る

class BookItem extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final book = Provider.of<Book>(context);
    return ListTile(
      leading: Text(book.id),
      title: Text(book.title),
      trailing: IconButton(
        icon: Icon(book.isFavorite ? Icons.star : Icons.star_border),
        onPressed: () => Provider.of<Books>(context, listen: false).toggleFavorite(book.id),
      ),
    );
  }
}

同じ様にProvider.ofでbookインスタンスが取得できるので取得します。
BookItemがbookIdなしに取得できるって凄くないですか?笑

さて、Provider.ofでも取得出来るのですが今回はConsumerで
ラップする形で書き換えたいと思います。

BookItemのListTileをConsumerでラップする

class BookItem extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<Book>(
      builder: (ctx, book, _) => ListTile(
        leading: IconButton(
          icon: Icon(
            Icons.book,
          ),
          onPressed: () {},
        ),
        title: Text(book.id),
        subtitle: Text(book.title),
        trailing: IconButton(
          icon: Icon(book.isFavorite ? Icons.star : Icons.star_border),
          onPressed: () => Provider.of<Books>(context, listen: false)
              .toggleFavorite(book.id),
        ),
      ),
    );
  }
}

さて、ここで復習がてら考えていきたい事があります。

Consumer<Book>としているので、Bookインスタンスが変更されると
builderに渡された関数がビルドされます。

ここまでは良いですね?

ですが、

IconButtonにとってbookインスタンスの変更なんてどうでもいい!!

ですよね?

こういう時にchild引数を使います。

IconButtonをchildに渡してみよう

class BookItem extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<Book>(
      builder: (ctx, book, child) => ListTile(
        leading: child,
        title: Text(book.id),
        subtitle: Text(book.title),
        trailing: IconButton(
          icon: Icon(book.isFavorite ? Icons.star : Icons.star_border),
          onPressed: () => Provider.of<Books>(context, listen: false)
              .toggleFavorite(book.id),
        ),
      ),
      child: IconButton(
        icon: Icon(
          Icons.book,
        ),
        onPressed: () {},
      ),
    );
  }
}

childにIconButtonを渡しました。
こうする事で、bookの変更のたびにIconButtonがリビルドされるのを防げるのですね。
このビルドされたIconButtonウィジェットはbuilderに渡した関数の第三引数でに渡って来るのでleadingにそのまま渡しています。

Bookshelfが自分でBooksを取得する

class BookshelfScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Book List'),
      ),
      body: Center(
        child: Sized(
          height: 200,
          child: Column(
            children: <Widget>[
              Expanded(
                child: Bookshelf(),
              ),
              Consumer<Books>(
                builder: (ctx, booksData, _) => Center(
                  child: Text('totalFavoriteCount: ${booksData.favoriteCount}'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Bookshelf extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final booksData = Provider.of<Books>(context, listen: false);
    return ListView.builder(
      itemCount: booksData.books.length,
      itemBuilder: (ctx, index) => BookItem(
        booksData.books[index].id,
      ),
    );
  }
}

さて、ここからは今回の趣旨とは少し逸れるのですが。
Bookshelfは引数として渡される事なく、Provider.ofでBooksを取得できますね。
なので、自ら取得する様に変更しました。

そうすると…。

ChangeNotifierProviderの適用範囲を狭める

>
class BookshelfScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Book List'),
      ),
      body: Center(
        child: SizedBox(
          height: 200,
          child: ChangeNotifierProvider(
            create: (ctx) => Books(),
            child: Column(
              children: <Widget>[
                Expanded(
                  child: Bookshelf(),
                ),
                Consumer<Books>(
                  builder: (ctx, booksData, _) => Center(
                    child:
                        Text('totalFavoriteCount: ${booksData.favoriteCount}'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

ChangeNotifierProviderの管理するウィジェットの範囲を狭める事ができる
のでMaterialAppをchildとしていたのをBookshelfScreenのColumnまで絞りました。

この様にする事でも、パフォーマンス改善につながるので積極的に変更しましょう。

完成!

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Book with ChangeNotifier {
  Book(this.id, this.title);

  final String id;
  final String title;
  bool isFavorite = false;

  void toggleFavorite() {
    isFavorite = !isFavorite;
    notifyListeners();
  }
}

class Books with ChangeNotifier {
  List<Book> books = [
    Book('1', 'Harry Potter'),
    Book('2', 'FACTFULNESS'),
  ];

  Book findById(String id) {
    return books.firstWhere((book) => book.id == id);
  }

  void toggleFavorite(String id) {
    final book = findById(id);
    if (book == null) {
      return;
    }

    book.toggleFavorite();
    notifyListeners();
  }

  int get favoriteCount {
    return books.where((book) => book.isFavorite).length;
  }
}

class BookshelfScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Book List'),
      ),
      body: Center(
        child: SizedBox(
          height: 200,
          child: ChangeNotifierProvider(
            create: (ctx) => Books(),
            child: Column(
              children: <Widget>[
                Expanded(
                  child: Bookshelf(),
                ),
                Consumer<Books>(
                  builder: (ctx, booksData, _) => Center(
                    child:
                        Text('totalFavoriteCount: ${booksData.favoriteCount}'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class Bookshelf extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final booksData = Provider.of<Books>(context, listen: false);
    return ListView.builder(
      itemCount: booksData.books.length,
      itemBuilder: (ctx, index) => ChangeNotifierProvider.value(
        value: booksData.books[index],
        child: BookItem(),
      ),
    );
  }
}

class BookItem extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<Book>(
      builder: (ctx, book, child) => ListTile(
        leading: child,
        title: Text(book.id),
        subtitle: Text(book.title),
        trailing: IconButton(
          icon: Icon(book.isFavorite ? Icons.star : Icons.star_border),
          onPressed: () => Provider.of<Books>(context, listen: false)
              .toggleFavorite(book.id),
        ),
      ),
      child: IconButton(
        icon: const Icon(
          Icons.book,
        ),
        onPressed: () {},
      ),
    );
  }
}

終わりに

3回に渡ってBookshelfScreenの改修記事にお付き合いくださりありがとうございます。
次回からは少し短めにしてProviderについて補足説明記事を書いていきます。

また、その次からは要望の多いFirebase連携についても書く予定です。

誰かのお役に立てば。

Twitterフォローお願いします

「次回以降も記事を読んでみたい!」
「この辺分からなかったから質問したい!」

そんな時は、是非@daiki1003のフォローお願いします♪

また、記事がとても役に立ったと思う人は
コーヒーを奢っていただけると非常に嬉しいです!

コメント

タイトルとURLをコピーしました