概要
Flutterにおいて、状態管理の方法の一つであるProvider
とConsumer
の使い方について紹介する。
サンプルとして、前回の記事【Flutter】GestureDetectorとCustomPaintを使い、タッチした場所に図形を描画するで作成したアプリを使う。まずはこちらを読み、アプリの内容を把握しておいてほしい。
前回はStatefulWidgetとsetState()を使って状態を扱っていたが、それをProvider
とConsumer
を使った方法に書き換えていく。
- 作成したコードはこちら
(pointer_drawing_lessonプロジェクトのstate_managementブランチ)
dependencyの追加
Provider
とConsumer
を使うため、pubspec.yamlのdependencies:にproviderを追加する。
pubspec.yaml
dependencies:
# ...
# 省略
# ...
provider: ^4.0.0
指定したバージョンは現時点での最新バージョンなので都度調整してほしい。
状態クラスの作成
このアプリでは、状態とはタッチされた点のリストである。そこで、以下のようなクラスを作る。ここでChangeNotifier
を継承しているが、このクラスに用意されたnotifyListeners()メソッドを使うと状態の変化を通知することができる。
// 状態クラス タッチされた点を記録する
class PointState extends ChangeNotifier {
final _points = List<Offset>();
// 変更不能なリストのビュー
UnmodifiableListView<Offset> get points => UnmodifiableListView(_points);
void add(Offset offset) {
_points.add(offset);
notifyListeners();
}
void clear() {
_points.clear();
notifyListeners();
}
}
上記のコードではadd()メソッドの中で、リストに値を追加した後notifyListeners()を呼び、このクラスを使っているWidgetに対して状態の変化を通知している。リストの内容を初期化するclear()メソッドも用意してある。
状態の供給
状態を供給するには、アプリ全体をProviderでラップする必要がある。コードは以下のようになる。
void main() => runApp(ChangeNotifierProvider(
create: (_) => PointState(),
child: MaterialApp(
title: 'Pointer drawing lesson',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: PointerDrawingWidget(title: 'Pointer drawing lesson'),
),
));
ここでは、runApp()にChangeNotifierProvider
を渡している。ChangeNotifierProvider
は状態を供給するWidgetの一つで、ChangeNotifierのインスタンスを下位の子Widgetへ供給する。
create:に状態を構築するための関数を渡す。このようにするとchild:以下のWidgetは、Widgetツリーのどの階層にいたとしても後述するProvider.of<T>()を使って状態にアクセスすることができるようになる。
描画Widgetの修正
修正後の描画Widgetは以下のようになる。
class PointerDrawingWidget extends StatelessWidget {
PointerDrawingWidget({Key key, this.title}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
// 状態の取得
// 状態が変化したことにより、リビルドが必要なのはCustomPaintのみのため、
// ここではlisten:falseを指定し、 Scaffold全体がリビルドされるのを回避する
final pointState = Provider.of<PointState>(context, listen: false);
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: GestureDetector(
// TapDownイベントを検知
onTapDown: (TapDownDetails details) {
// タッチされた点を追加して状態を更新する
pointState.add(details.localPosition);
},
// 状態のConsumer
// 状態の変化が通知されると、リビルドされる
child: Consumer<PointState>(
builder: (BuildContext context, PointState value, Widget _) {
// カスタムペイント
return CustomPaint(
painter: MyPainter(value.points),
// タッチを有効にするため、childが必要
child: Center(),
);
},
),
),
floatingActionButton: FloatingActionButton(
// タッチした点をクリアする
onPressed: pointState.clear,
tooltip: 'Clear',
child: Icon(Icons.clear),
),
);
}
}
いくつかのポイントについて説明する。
- StatelessWidgetを継承するよう修正
状態はProvider
から取得できるため、Widgetで保持する必要が無くなった。そのため、StatelessWidgetへと修正した。 - 状態の取得
Widgetのbuild()メソッドの中でProvider.of<T>()
メソッドを使い、状態を取得する。この時、オプション引数のlisten:にはfalseを指定している。これは、状態が変化した時(ChangeNotifierのnotifyListeners()が呼ばれた時)にWidgetをリビルドするかしないかを指定するフラグである。デフォルトはtrueであり、この場合はbuild()メソッド内で構築したUI全体がリビルドの対象となる。しかし、このWidget全体(Scaffold以下)の中にはタイトルバーやボタンなど、状態が変化しても表示内容は変わらないWidgetが含まれている。これらのWidgetまでリビルドされてしまうのは無駄である。そのため、それを避けるために明示的にfalseを指定している。 - 状態の変化によってリビルドが必要なWidgetはConsumerで囲む
タイトルバーやボタンと違い、CustomPaintは「タッチにより点が追加された」ことをトリガーにして表示を更新する必要がある。そのためにはConsumer
をchildとし、そのbuilder:にWidget構築のロジックを書く関数を渡す。関数の第2引数に更新された状態が渡ってくるので、それを使って新しい画面を構築する。
CustomPainter
CustomPainterは前回と変わらない。このクラスは、渡された内容をただ描画することに集中する。
// 描画クラス
class MyPainter extends CustomPainter {
final List<Offset> _points;
final _rectPaint = Paint()..color = Colors.blue;
MyPainter(this._points);
@override
void paint(Canvas canvas, Size size) {
// 記憶している点を描画する
_points.forEach((offset) => canvas.drawRect(
Rect.fromCenter(center: offset, width: 20.0, height: 20.0),
_rectPaint));
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
以上で前回のアプリのリファクタリングが完了した。
参考
Simple app state management(公式ドキュメント・英語)