【Flutter】ProviderにおけるConsumerをサンプル付きで解説してみたよ

Dart

追記情報

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

概要

前回の記事で、Providerに関して詳しく説明しました。

今回の記事はこちらの記事での知識を前提に書かれているので、
Providerの基礎についてまだ理解していない方は、先にお読みいただけると嬉しいです。

【Flutter】Providerについてサンプル付きで解説してみた
追記情報 lintに対応した書式に変更しました。 (2020.09.29) 概要 「FlutterのProvider?」 「ChangeNotifier?Consumer?何それ美味しいの?」 あなたは、こ...

さて、Providerを勉強しているとConsumerって単語も出て来ます。

…消費者?

この記事を読めば、Consumerの使い所や使い方などを理解していただけると思います♪

また、サンプルに関しては今回の解説内容分を更新しました。

GitHub - daiki1003/bookshelf_sample: Bookshelf sample. See https://blog.dalt.me/1741
Bookshelf sample. See Contribute to daiki1003/bookshelf_sample development by creating an account on GitHub.

それでは、解説していきましょー!

前回の完成形から問題点を考える

Providerを用いた前回の完成形

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

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

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

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

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) {
    final booksData = Provider.of<Books>(context);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Book List'),
      ),
      body: Center(
        child: SizedBox(
          height: 200,
          child: Column(
            children: <Widget>[
              Expanded(
                child: Bookshelf(booksData.books),
              ),
              Center(
                child: Text('totalFavoriteCount: ${booksData.favoriteCount}'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Bookshelf extends StatelessWidget {
  const Bookshelf(this.books);

  final List<Book> books;

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: books.length,
      itemBuilder: (ctx, index) => BookItem(
        books[index].id,
      ),
    );
  }
}

class BookItem extends StatelessWidget {
  const BookItem(this.bookId);

  final String bookId;

  @override
  Widget build(BuildContext context) {
    final booksData = Provider.of<Books>(context);
    final book = booksData.findById(bookId);
    return ListTile(
      leading: Text(book.id),
      title: Text(book.title),
      trailing: IconButton(
        icon: Icon(book.isFavorite ? Icons.star : Icons.star_border),
        onPressed: () => booksData.toggleFavorite(book.id),
      ),
    );
  }
}

問題点を考える

前回、これで完成!としたのは良いのですがまだ実は問題点があります。
試しに、Bookshelfのbuildメソッドの先頭に

print("build");

を追加してみます。
この状態で、お気に入りボタンを押すと

flutter: build
flutter: build

と押すたびに出力されます。
つまり、

お気に入りボタンを押すたびにBookshelfが再ビルドされてしまっている

と言うことです。

ですが、お気に入りボタンを押したとしてもBookshelfウィジェット自体は何も変わっていないのでビルドされる必要はありません。

ここで、登場するのがお気づきの通りConsumerとなります。

ProviderのConsumerを使って書き換えてみよう

Consumerって何?

Consumerの特徴は

notifyされた際、再ビルドの対象を絞るためのWidget
※notifyされる必要があるので、Provider.ofと同じく
ChangeNotifierProviderが管理しているWidget内でないともちろんダメです。

です。

今回の例で言えば、再ビルドしたい対象とはどれでしょうか?

そうです、favoriteCountの表示をしているCenterだけです。

Consumerでラップする

なので、まずCenterをConsumerでラップします。

Consumer(
  child: Center(
    child: Text('totalFavoriteCount: ${booksData.favoriteCount}'),
  ),
),

Consumerはジェネリック型なのでBooksを指定します。

Consumer<Books>(
  child: Center(
    child: Text('totalFavoriteCount: ${booksData.favoriteCount}'),
  ),
),

引数builderに再ビルド関数を渡す

Consumerの引数として、再ビルドするための関数をbuilder引数に渡します。

Consumer<Books>(
  builder: (ctx, booksData, _) {
    return Center(
      child: Text('totalFavoriteCount: ${booksData.favoriteCount}'),
    );
  },
),

builder関数は以下の様な定義となっています。

Widget Function(BuildContext, dynamic, Widget)

返り値

返り値のWidgetにはBooksが更新されるたびに再ビルドしたいWidgetを返します。
今回であればBooksが更新されるたびにお気に入りの数が更新されているはずなのでCenterウィジェットを返しています。

引数

次は、引数について考えてみましょう。

BuildContextは良いとして、dynamicとWidgetには何が渡ってくるのでしょうか?
(上記のサンプルでは第三引数は使わないので_として使わないことを明示しています。)

第二引数
 -> Booksインスタンス

第三引数
 -> Consumerコンストラクタにchildとして渡したインスタンス

がそれぞれ返って来ます。

Consumer<Books>でラップするとBooksが更新されるたびにbuilder関数が実行されます。

その際に一部、Booksの更新に関係ないWidgetを含んでいたとします。
そんな時はConsumerの引数childとして、
Booksの更新に関係ないWidgetを渡すとリビルドすることなく、builder関数実行時に使えると言う流れです。

この様にして少しでも無駄なリビルドを無くしてパフォーマンスを改善しているのです。

ここに関しては、同じサンプルを用いて明日解説記事を書きたいと思います。

BookshelfScreenのProvider.ofにlisten: falseを設定しよう

さて、Consumerでラップしたことで、Booksの中身が変更されたらCenterウィジェットがリビルドされる様になりました。

現在、BookshelfScreenのbuildメソッドで

final booksData = Provider.of<Books>(context);

を呼んでいるため、Booksの更新時にBookshelfScreen全体がリビルドされる様になっています。

が、今回の例で言えばお気に入りが変わってもBookshelfはリビルドする必要はありません。

なので、BookshelfScreen全体としては通知の変更を受け取る必要はないわけです。

これを実現するために、Provider.ofメソッドの第二引数に listen: false を指定します。

class BookshelfScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final booksData = Provider.of<Books>(context, listen: false);
...

これで、ホットリスタートをすればお気に入りをオン/オフしてもbuildが表示されることはないですね。

つまり、Booksの更新で再ビルドされるのは

・Centerウィジェット
・BookItemウィジェット

の二つだけになります。

Consumerはパフォーマンス改善のために積極的に使おう

この様に、Consumerはパフォーマンス改善のために積極的に使っていきましょう。

そして、実はまだまだ改善点があります。
それを考えてから次の記事に進んでみてください♪

【Flutter】ProviderにおけるConsumerについて解説してみたよ vol.2
追記情報 lintに対応した形式に修正しました。 2020.09.29 概要 「Consumerってbuilderだけコンストラクタの引数に渡せば良いんじゃないの?」 「第三引数のWidgetって何?」 「Cons...
誰かのお役に立てば。

Twitterフォローお願いします

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

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

Twitterコミュニティ参加お願いします

Twitterコミュニティ「Flutter lovers」を開設しました!
参加お待ちしております😁

☕️ Buy me a coffee

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

コメント

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