【Flutter】 Provider のススメ

Flutter の状態管理には BLoC (Business Logic Component) パターンがよく使われると思うんですが、package:providerを使った方が楽だよ、という記事です。

※2019年に書いた記事なので少し内容は古い可能性があります。

Provider のススメ

Flutter の状態管理には BLoC (Business Logic Component) パターンがよく使われると思うんですが、package:provider (正確には provider と ChangeNotifier) を使った方が楽だよ、という記事です。
Google I/O でも 2018 年は BLoC を推奨していましたが

 2019 年では意見を変えて provider パッケージの使用を推奨しています 。

ちなみに 2019 の発表は現地で見ていたのですが、終盤にもかかわらず満席で Flutter への注目度の高さを伺わせました。

2020/05/07 編集
provider 4.1.0 の更新に伴い、Consumer, Selector, Provider.of から context.watch, context.select, context.read へ記述を変更しました。

Riverpod について
Provider パッケージの作者の人が、Provider の改良版として Riverpod というパッケージを開発していますので、そちらも合わせてご覧ださい:

BLoC はどうして最高じゃないのか?

BLoC それ自身が悪いわけではないです。私も一度 BLoC パターンでアプリを構築したことはあり、StreamController や StreamBuilder (Stream は Rx でいう Observable に相当) などを使っていい具合にコードがかけた時は、いい気分ですし、何の支障もなかったです。しかし、本当にこのコード量や複雑性が必要なのだろうかとはよく思っていました(BLoC ではStreamとSink を入出力で使うことが要求されている)。
つまり BLoC は、複雑で学習コストが高いところに問題があります。
そこで、より簡易に状態管理を行う手法として、 package:provider(と ChangeNotifier)が推奨されました。

package:provider とは

provider、コミュニティによって開発されている DI (Dependency Injection) 及び 状態管理用のパッケージです。
実は Google 側でも似たようなprovideというパッケージを開発していたのですが、provider の方がよいのでは?となり、今は provider を推奨しているらしいです。
package:provider は効率的な状態へのアクセス制御や変更通知の機構
を提供します。基本的には、上位 Widget で Provider によって状態を作成し、下位 Widgetcontext.read や context.select を使って状態にアクセスします。
実際のコードを見た方がわかりやすいと思いますので、次節をご覧ください。

サンプルコード

package:provider を使った状態管理の方法について、順を追って説明していきます。例のカウントアップするだけのアプリを簡略化したものを基にします。

一般的な StatefulWidget によるパターン
さて、まずこちらが、一般的な StatefulWidget による状態管理です。setStateを呼ぶことによって、再描画が行われます。状態やロジックが、UI にくっついていて、他の Widget からの状態操作やロジックのテストがしにくいです。provider を使った場合、どうなるでしょうか? 

import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(home: MyHomePage());
 }
}
class MyHomePage extends StatefulWidget {
 @override
 _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
 var _count = 0;
 void _incrementCounter() {
   setState(() {
     _count++;
   });
 }
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(child: Text('$_count')),
     floatingActionButton: FloatingActionButton(
       onPressed: _incrementCounter,
     ),
   );
 }
}

package:provider を使用したパターン
続いて、package:provider を使ったパターンを見ていきます。
状態クラスを作る
まず、状態クラスを作ります。ChangeNotifier を with で mixin することで notifyListeners が使えるようになります。この関数が呼ばれると、変更を監視している Widget に状態の変更が通知されます。

class CounterStore with ChangeNotifier {
 var count = 0;
 void incrementCounter() {
   count++;
   notifyListeners();
 }
}

状態クラスをアクセスできるようにする
下位 Widget が状態クラスにアクセスできるように ChangeNotifierProvider を Widget ツリーの上の方に配置します(スコープを制限できる)。複数の状態クラスには MultiProvider が使えます。

class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     home: ChangeNotifierProvider(
       create: (context) => CounterStore(),
       child: MyHomePage(),
     ),
   );
 }
}

状態クラスにアクセスする
最後に、状態クラスにアクセスします。それには主に context.watch context.select context.read を通じて行います。
この中で、context.watch か context.select は変更を監視して、context.read は変更を監視しません。
context.select は監視対象を変数一つに絞ることができますので、基本的には、context.select を使えばいいかと思います。
関数の呼び出しなど、監視を必要としない場所で context.read を使います。

ポイント
- 状態管理を分離できたので `StatelessWidget` を使える
- ロジックが分離されている
class MyHomePage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   final count = context.select((CounterStore store) => store.count);
   return Scaffold(
     body: Center(child: Text("$count")),
     floatingActionButton: FloatingActionButton(
       onPressed: () {
         context.read<CounterStore>().incrementCounter();
       }
     ),
   );
 }
}

コード全体
コード全体を貼っておきます。直感的で必要最小限のコードになっていると思います。
実際のプロジェクトでは、ファイルを分離しましょう。
ロジック、状態を分離できたので、テストを簡単に行うことができます。

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CounterStore with ChangeNotifier {
 var count = 0;
 void incrementCounter() {
   count++;
   notifyListeners();
 }
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     home: ChangeNotifierProvider(
       create: (context) => CounterStore(),
       child: MyHomePage(),
     ),
   );
 }
}
class MyHomePage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   final count = context.select((CounterStore store) => store.count);
   return Scaffold(
     body: Center(child: Text("$count")),
     floatingActionButton: FloatingActionButton(
       onPressed: () {
         context.read<CounterStore>().incrementCounter();
       }
     ),
   );
 }
}


さて、package:provider と ChangeNotifier を使ったパターンはいかがだったでしょうか? 学習することが少ないため、初心者にもおすすめだと思います。BLoC を使っている人も是非一度使ってみてください :)
まだβ版ですが Riverpod も今後メジャーになる可能性がありますので要チェックです:

参考資料



この記事が気に入ったらサポートをしてみませんか?