Provider のススメ
Flutter の状態管理には BLoC (Business Logic Component) パターンがよく使われると思うんですが、package:provider
(正確には provider と ChangeNotifier) を使った方が楽だよ、という記事です。
Google I/O でも 2018 年は BLoC を推奨していましたが
(Build reactive mobile apps with Flutter (Google I/O '18))、 2019 年では意見を変えて provider パッケージの使用を推奨しています (Pragmatic State Management in Flutter (Google I/O'19)
)。ちなみに 2019 の発表は現地で見ていたのですが、終盤にもかかわらず満席で Flutter への注目度の高さを伺わせました。
20202/05/07 編集
provider 4.1.0 の更新に伴い、Consumer, Selector, Provider.of から context.watch, context.select, context.read へ記述を変更しました。
目次
BLoC ではどうして最高じゃないのか?
BLoC それ自身が悪いわけではないです。私も一度 BLoC パターンでアプリを構築したことはあり、StreamController や StreamBuilder (Stream は Rx でいう Observable に相当) などを使っていい具合にコードがかけた時は、いい気分ですし、何の支障もなかったです。しかし、本当にこのコード量や複雑性が必要なのだろうかとはよく思っていました(BLoC ではStreamとSink を入出力で使うことが要求されている)。
つまり BLoC は、複雑で学習コストが高いところに問題があります。
そこで、より簡易に状態管理を行う手法として、 package:provider
(と ChangeNotifier)が推奨されました。
package:provider とは
package:provider
は、コミュニティによって開発されている DI (Dependency Injection) 及び 状態管理用のパッケージです。
実は Google 側でも似たようなパッケージ(provide
)を開発していたのですが、provider の方がよいのでは?となり、今は provider を推奨しているらしいです。
package:provider
は効率的な状態へのアクセス制御や変更通知の機構
を提供します。
基本的には、上位 Widget でProvider
によって状態を作成し、下位 Widget で context.read
や context.select
を使って状態にアクセスします。
実際のコードを見た方がわかりやすいと思いますので、次節をご覧ください。
サンプルコード
package:provider
を使った状態管理の方法について、順を追って説明していきます。例のカウントアップするだけのアプリを簡略化したものを基にします。
一般的な StatefulWidget によるパターン
さて、まずこちらが、一般的な StatefulWidget による状態管理です。setStateを呼ぶことによって、再描画が行われます。状態やロジックが、UI にくっついていて、他の Widget からの状態操作やロジックのテストがしにくいです。provider を使った場合、どうなるでしょうか?(provider を使用したパターン)
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(home: MyHomePage());
}
}
class MyHomePage extends StatefulWidget {
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var _count = 0;
void _incrementCounter() {
setState(() {
_count++;
});
}
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text('$_count')),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
),
);
}
}
package:provider を使用したパターン
続いて、package:provider
を使ったパターンを見ていきます。
状態クラスを作る
まず、状態クラスを作ります。ChangeNotifier
を mixin することで notifyListeners
が使えるようになります。この関数が呼ばれると、変更を監視している Widget に状態の変更が通知されます。
class CounterStore with ChangeNotifier {
var count = 0;
void incrementCounter() {
count++;
notifyListeners();
}
}
状態クラスをアクセスできるようにする
下位 Widget が状態クラスにアクセスできるように ChangeNotifierProvider
を Widget ツリーの上の方に配置します(スコープを制限できる)。複数の状態クラスには MultiProvider
が使えます。
class MyApp extends StatelessWidget {
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 {
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 {
Widget build(BuildContext context) {
return MaterialApp(
home: ChangeNotifierProvider(
create: (context) => CounterStore(),
child: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
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 を使っている人も是非一度使ってみてください😄
参考資料
- provider | Flutter Package
- Pragmatic State Management in Flutter (Google I/O'19)
- Pragmatic State Management Using Provider (The Boring Flutter Development Show, Ep. 24)