【Flutter】Providerについてサンプル付きで解説してみた

Dart

概要


「FlutterのProvider?」
「ChangeNotifier?Consumer?何それ美味しいの?」

あなたは、こんな状態じゃありませんか?
でも、大丈夫!

この記事ではそんなFlutter初心者が最初に大きくつまづきそうなProviderについて解説した記事になります。
読み終わった頃には、自信を持って使える様になっているのではないでしょうか。

ちなみに、Consumerに関しては下記記事に書いていますので合わせてお楽しみください。

【Flutter】ProviderにおけるConsumerをサンプル付きで解説してみたよ
Providerについて勉強してるけど、Consumerって何?ってなってないですか? この記事では、サンプルを使ってConsumerの使い方や特徴を解説しています。 是非ご覧いただけると嬉しいです。
【Flutter】ProviderにおけるConsumerについて解説してみたよ vol.2
ProviderのConsumerについて良く分からない? この記事では、Providerの基礎から始まり、Consumerについても詳しく解説しています。 是非ご覧ください♪

また、解説しているサンプルコードは以下に置いています。

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

では、解説していきたいと思います。

FlutterにおけるProviderとは?

provider | Flutter Package
A wrapper around InheritedWidget to make them easier to use and more reusable.

難しく言うと、framework.dartで定義されているInheritedWidgetと言うWidgetのラッパークラスです。
簡単に言うと、Flutterの仕組みに最適化されたObserverパターンの枠組みと思ってもらって大丈夫です。

そして、個人的な意見ですが、InheritedWidgetについてはひとまず知らなくて良いかと思います。

それではサンプルとして、本一覧を表示しつつその下にお気に入り数を表示するスクリーン、BookshelfScreenを作ります。

まずは、このサンプルをProviderなしで作ってみます。
その後、Providerありで書くとどうなるのか、と言う順番で見ていきましょう。

Providerを使わない場合

Widgetの構成図はこんなイメージでしょうか。

表示する本全体はBookschelfScreenで管理しています。
なので、お気に入りしているかどうかをオン/オフする関数toggleFavoriteも
BookschelfScreenで管理します。
これらを適宜、BookshelfやBookItemにコンストラクタで引き渡していく感じですね。

では、コードにしてみたいと思います。

import 'package:flutter/material.dart';

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

  Book(this.id, this.title);

  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(() {
      for (Book book in books) {
        if (book.id != id) continue;

        book.toggleFavorite();
        break;
      }
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Book List'),
      ),
      body: Center(
        child: Container(
          height: 200,
          child: Column(
            children: [
              Expanded(
                child: Bookshelf(books, toggleFavorite),
              ),
              Center(
                child: Text('totalFavoriteCount: $favoriteCount'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Bookshelf extends StatelessWidget {
  final List<Book> books;
  final Function(String) toggleFavorite;

  const Bookshelf(this.books, this.toggleFavorite);

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

class BookItem extends StatelessWidget {
  final Book book;
  final Function(String) toggleFavorite;

  const BookItem(this.book, this.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して
自分でも手を動かしてみてください。

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

そのほうが理解しやすいかもしれません!

それでは、長いですが張り切って行ってみましょー!
完成形が見たい人は、一気に9まで飛んじゃってください。

providerパッケージのインストール

provider | Flutter Package
A wrapper around InheritedWidget to make them easier to use and more reusable.

こちらのInstallingタブに飛び、


provider: ^4.1.2

の部分をpubspec.yamlに下記の様にコピペします。
(数字の部分は変わっているかもしれません。)

dependencies:
  flutter:
    sdk: flutter
  provider: ^4.1.2

保存すれば勝手にインストールしてくれます。

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) {
    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;
  }
}

まずは、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: Text('Book List'),
      ),
      body: Center(
        child: Container(
          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: Text('Book List'),
      ),
      body: Center(
        child: Container(
          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: Text('Book List'),
      ),
      body: Center(
        child: Container(
          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: Text('Book List'),
      ),
      body: Center(
        child: Container(
          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 {
  final Book book;

  const BookItem(this.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 {
  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(bookId),
      ),
    );
  }
}

まず、Bookインスタンスを渡す必要はなく
BookItemのみで探し出す(findById)事ができる様になったので最低限の情報、bookIdだけを渡す様に変更しました。
また、BookItem内でもBooksインスタンスを取得し、このインスタンスに対してtoggleFavoriteを呼び出しています。

完成!

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: [
              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),
      ),
    );
  }
}

と言うことで完成しました。
ここで、完成図のイメージ図を最後に見てみましょう。

この様に、不必要に途中経由するWidgetを変更する必要なく実装する事ができました。

次に読む

【Flutter】ProviderにおけるConsumerをサンプル付きで解説してみたよ
Providerについて勉強してるけど、Consumerって何?ってなってないですか? この記事では、サンプルを使ってConsumerの使い方や特徴を解説しています。 是非ご覧いただけると嬉しいです。
【Flutter】ProviderにおけるConsumerについて解説してみたよ vol.2
ProviderのConsumerについて良く分からない? この記事では、Providerの基礎から始まり、Consumerについても詳しく解説しています。 是非ご覧ください♪

実は、↑では無駄なWidgetのリビルドが走ってしまっています。
それを解決するConsumerについて解説しています。誰かのお役に立てば。

Twitterフォローお願いします

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

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

コメント

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