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

Dart

概要

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

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

【Flutter】Providerについてサンプル付きで解説してみた
Flutter、Providerでつまづいてませんか? この記事ではFlutterにおけるProviderに関してsample,example付きで解説しています。 是非ご覧いただけると嬉しいです。

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

…消費者?

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

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

daiki1003/bookshelf_sample
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 {
  final String id;
  final String title;
  bool isFavorite = false;

  Book(this.id, this.title);

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

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

class Bookshelf extends StatelessWidget {
  final List<Book> books;

  const Bookshelf(this.books);

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

class BookItem extends StatelessWidget {
  final String bookId;

  const BookItem(this.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
ProviderのConsumerについて良く分からない? この記事では、Providerの基礎から始まり、Consumerについても詳しく解説しています。 是非ご覧ください♪
誰かのお役に立てば。

Twitterフォローお願いします

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

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

コメント

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