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

Dart

概要


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

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

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

【Flutter】Providerについてサンプル付きで解説してみた
Flutter、Providerでつまづいてませんか? この記事ではFlutterにおけるProviderに関してsample,example付きで解説しています。 是非ご覧いただけると嬉しいです。
【Flutter】ProviderにおけるConsumerをサンプル付きで解説してみたよ
Providerについて勉強してるけど、Consumerって何?ってなってないですか? この記事では、サンプルを使ってConsumerの使い方や特徴を解説しています。 是非ご覧いただけると嬉しいです。

※今回の解説分も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);
    print("build");
    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: Text('Book List'),
      ),
      body: Center(
        child: Container(
          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);
    print("build");
    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: Text('Book List'),
      ),
      body: Center(
        child: Container(
          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 {
  final String id;
  final String title;
  bool isFavorite = false;

  Book(this.id, this.title);

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

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

  Book findById(String id) {
    for (Book book in books) {
      if (book.id == id) return book;
    }
    return null;
  }

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

    book.toggleFavorite();
    notifyListeners();
  }

  int get favoriteCount {
    int count = 0;
    for (Book book in books) {
      if (book.isFavorite) count++;
    }
    return count;
  }
}

class BookshelfScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Book List'),
      ),
      body: Center(
        child: Container(
          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);
    print("build");
    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: Icon(
          Icons.book,
        ),
        onPressed: () {},
      ),
    );
  }
}

終わりに

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

また、その次からは要望の多いFirebase連携についても書く予定です。
よろしければTwitterのフォローもお願いします。

誰かのお役に立てば。

Twitterフォローお願いします

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

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

コメント

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