追記情報
lintに対応した書式に変更しました。 (2020.09.29)
概要
「FlutterのProvider?」
「ChangeNotifier?Consumer?何それ美味しいの?」
あなたは、こんな状態じゃありませんか?
でも、大丈夫!
この記事ではそんなFlutter初心者が最初に大きくつまづきそうなProviderについて解説した記事になります。
読み終わった頃には、自信を持って使える様になっているのではないでしょうか。
ちなみに、Consumerに関しては下記記事に書いていますので合わせてお楽しみください。
また、解説しているサンプルコードは以下に置いています。
では、解説していきたいと思います。
FlutterにおけるProviderとは?
難しく言うと、framework.dartで定義されているInheritedWidgetと言うWidgetのラッパークラスです。
簡単に言うと、Flutterの仕組みに最適化されたObserverパターンの枠組みと思ってもらって大丈夫です。
そして、個人的な意見ですが、InheritedWidgetについてはひとまず知らなくて良いかと思います。
それではサンプルとして、本一覧を表示しつつその下にお気に入り数を表示するスクリーン、BookshelfScreenを作ります。
まずは、このサンプルをProviderなしで作ってみます。
その後、Providerありで書くとどうなるのか、と言う順番で見ていきましょう。
Providerを使わない場合
Widgetの構成図はこんなイメージでしょうか。
表示する本全体はBookschelfScreenで管理しています。
なので、お気に入りしているかどうかをオン/オフする関数toggleFavoriteも
BookschelfScreenで管理します。
これらを適宜、BookshelfやBookItemにコンストラクタで引き渡していく感じですね。
では、コードにしてみたいと思います。
import 'package:flutter/material.dart'; class Book { Book(this.id, this.title); final String id; final String title; bool isFavorite = false; void toggleFavorite() { isFavorite = !isFavorite; } } class BookshelfScreen extends StatefulWidget { @override _BookshelfScreenState createState() => _BookshelfScreenState(); } class _BookshelfScreenState extends State { List<Book> books = [ Book('1', 'Harry Potter'), Book('2', 'FACTFULNESS'), ]; void toggleFavorite(String id) { setState(() { books.firstWhere((book) => book.id == id).toggleFavorite(); }); } int get favoriteCount { return books.where((book) => book.isFavorite).length; } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Book List'), ), body: Center( child: SizedBox( height: 200, child: Column( children: [ Expanded( child: Bookshelf(books, toggleFavorite), ), Center( child: Text('totalFavoriteCount: $favoriteCount'), ), ], ), ), ), ); } } class Bookshelf extends StatelessWidget { const Bookshelf(this.books, this.toggleFavorite); final List<Book> books; final Function(String) toggleFavorite; @override Widget build(BuildContext context) { return ListView.builder( itemCount: books.length, itemBuilder: (ctx, index) => BookItem( books[index], toggleFavorite, ), ); } } class BookItem extends StatelessWidget { const BookItem(this.book, this.toggleFavorite); final Book book; final Function(String) toggleFavorite; @override Widget build(BuildContext context) { return ListTile( leading: Text(book.id), title: Text(book.title), trailing: IconButton( icon: Icon(book.isFavorite ? Icons.star : Icons.star_border), onPressed: () => toggleFavorite(book.id), ), ); } }
何が問題なのか?
この場合、問題点として
・BookItemに渡すためだけにtoggleFavoriteをBookshelfListに渡している
があります。
ここで、BookItemにTrashボタンを追加する事を考えてみましょう。
1. BookshelfScreenに removeBook(String) メソッドの追加
2. BookshelfListのメンバ変数でremoveBookを保持できる様にし、コンストラクタの変更
3. BookListのメンバ変数でremoveBookを保持出来る様にし、コンストラクタの変更
4. BookshelfScreenからBookshelfListにremoveBookメソッドポインタを渡す
5. BookshelfListからBookItemにremoveBookメソッドポインタを渡す
と言った作業が必要になります。
二個目のオレンジの線が!
問題点が分かって来ましたか?
さて、ではProviderを使うとこれがどう解決できるのでしょうか?
上記サンプルをProviderを使って書き換えてみます。
Providerを使って書き換えてみよう
手順としては、以下の通りです。
1. providerパッケージのインストール
2. Booksクラスの実装
3. BookshelfScreenから本一覧に関する処理の削除
4. BookshelfScreenをStatefulWidgetからStatelessWidgetへ
5. ChangeNotifierProviderを使ってBooksの登録
6. Providerを使ってBooksを取得する
7. toggleFavoriteを渡すのをやめる
8. Books経由でtoggleFavoriteを呼ぶ
9. 完成!
よろしければ、下記プロジェクトをcloneしてbeforeブランチをcheckoutして
自分でも手を動かしてみてください。
そのほうが理解しやすいかもしれません!
それでは、長いですが張り切って行ってみましょー!
完成形が見たい人は、一気に9まで飛んじゃってください。
providerパッケージのインストール
パッケージのインストール方法に関しては、こちらのブログを参照ください。
Booksクラスの実装
それでは、今回の肝となるBooksクラスを実装します。
import 'package:provider/provider.dart'; 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; } }
まずは、provider.dartをimportします。
ここで、ChangeNotifierと言うClassが出て来ました。
こいつはつまるところObservableみたいな意味合いです。
toggleFavorite内にある様にnotifyObservers()メソッドが使える様になります。
BookshelfScreenから本一覧に関する処理の削除
本を管理する処理がBooksに移動したのでBookshelfScreenから削除してしまいます。
(ここから、完成までコンパイルは通らなくなります。)
class BookshelfScreen extends StatefulWidget { @override _BookshelfScreenState createState() => _BookshelfScreenState(); } class _BookshelfScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Book List'), ), body: Center( child: SizedBox( height: 200, child: Column( children: [ Expanded( child: Bookshelf(books, toggleFavorite), ), Center( child: Text('totalFavoriteCount: $favoriteCount'), ), ], ), ), ), ); } }
booksもtoggleFavoriteもfavoriteCountもコンパイルエラーになっていますが
とりあえず次に進みます。
BookshelfScreenをStatefulWidgetからStatelessWidgetへ
内部での状態管理がなくなってStatefulWidgetである必要がなくなったので、StatelessWidgetに変更します。
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: Column( children: [ Expanded( child: Bookshelf(books, toggleFavorite), ), Center( child: Text('totalFavoriteCount: $favoriteCount'), ), ], ), ), ), ); } }
ChangeNotifierProviderを使ってBooksの登録
ここだけ、main.dartに話が移ります。
コードはこんな感じ。
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return ChangeNotifierProvider( create: (ctx) => Books(), child: MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: BookshelfScreen(), ), ); } }
新キャラが登場しましたね。
ChangeNotifierProviderは見た目には何も影響を与えませんがWidgetです。
以下の役割と思っていただいて大丈夫です。
1. Flutterの内部システムにChangeNotifierであるBooksを管理してもらう
2. このWidgetのchild以降にnotifyしてくれる
アプリがどんどん大きくなってくると、あるデータの変更によって変えたいUIと変えたくないUIがあるはずです。
例えば、ショップアプリがあるとしてカートに追加ボタンを押した時。
カートの中身はもちろん再描画したいですが、お気に入り一覧なんかは再描画する必要はないですよね?
こう言ったときにnotifyする対象を制限する事が出来ます。
今回で言えば、MaterialApp以降のWidgetにnotify可能となっています。
Providerを使ってBooksを取得する
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: [ Expanded( child: Bookshelf(booksData.books, toggleFavorite), ), Center( child: Text('totalFavoriteCount: ${booksData.favoriteCount}'), ), ], ), ), ), ); } }
Providerが管理してくれているインスタンスは
Provider.of<T>(BuildContext)
で取得する事ができます。
また、ここで一つ大事な事があります。
この処理を呼んだWidgetがnotifyの対象になる(=再ビルドされる)と言う事です。
通知の対象になりたくない場合は、
Provider.of<T>(context, listen: false)
と言う形で呼び出すことによりnotifyの対象とならずにTインスタンスを取得する事ができます。
toggleFavoriteを渡すのをやめる
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: [ Expanded( child: Bookshelf(booksData.books), ), Center( child: Text('totalFavoriteCount: $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], ), ); } } class BookItem extends StatelessWidget { const BookItem(this.book); final Book book; @override Widget build(BuildContext context) { return ListTile( leading: Text(book.id), title: Text(book.title), trailing: IconButton( icon: Icon(book.isFavorite ? Icons.star : Icons.star_border), onPressed: () => toggleFavorite(book.id), ), ); } }
toggleFavoriteを渡す必要がなくなったのでコンストラクタで渡し続けるのをやめました。
ここでは、BookItemのtoggleFavorite(book.id)だけエラーになっていると思います。
Books経由でtoggleFavoriteを呼ぶ
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 { 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(bookId), ), ); } }
まず、Bookインスタンスを渡す必要はなく
BookItemのみで探し出す(findById)事ができる様になったので最低限の情報、bookIdだけを渡す様に変更しました。
また、BookItem内でもBooksインスタンスを取得し、このインスタンスに対してtoggleFavoriteを呼び出しています。
完成!
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), ), ); } }
と言うことで完成しました。
ここで、完成図のイメージ図を最後に見てみましょう。
この様に、不必要に途中経由するWidgetを変更する必要なく実装する事ができました。
次に読む
実は、↑では無駄なWidgetのリビルドが走ってしまっています。
それを解決するConsumerについて解説しています。誰かのお役に立てば。
Twitterフォローお願いします
「次回以降も記事を読んでみたい!」「この辺分からなかったから質問したい!」
そんな時は、是非Twitter (@daiki1003)やInstagram (@ashdik_flutter)のフォローお願いします♪
Twitterコミュニティ参加お願いします
Twitterコミュニティ「Flutter lovers」を開設しました!参加お待ちしております😁
☕️ Buy me a coffee
また、記事がとても役に立ったと思う人はコーヒーを奢っていただけると非常に嬉しいです!
コメント