※こちらの記事は【Flutter】色んな状態管理手法でカウンターアプリを作ってみるの一部として作成された記事です
今最も普及している状態管理手法? Provider
一発目はGoogleが公式に推している事もあり、最も普及している(であろう)状態管理手法の__Provider__でカウンターアプリを作っていきます
使用するPackage:
- provider v6.0.1
概要
- 現在最もポピュラーな状態管理用パッケージ
- Googleも状態管理パッケージの中でもオススメしているパッケージ
- providerだけでなく、stateNotifierやRiverpod、freezedなど数多くの人気FlutterパッケージをリリースしているRemi Rousselet氏が開発
- 初版リリースは2018年10月
全体像
特徴は2点、
- 依存関係がWidgetツリーに沿って下っていく
- 状態変更を明示的に通知する
状態を保持したクラスは依存関係を注入されたWidgetとそのWidgetツリー傘下のWidgetからアクセスが可能になります。Widgetツリーに沿って依存関係が下っていくようなイメージ。
Widgetは状態管理クラスの変数やメソッドをProviderを通してアクセスします。変数を変更した際は用意されている通知メソッドを実行し、変数を利用しているクラスに通知。その通知を受けったクラス達は新しい値で自身を再生成(リビルド)する事になります。
キーとなるクラスやメソッド
-
ChangeNotifier
クラス:状態管理クラスが継承するクラス -
ChangeNotifierProvider
クラス:状態管理クラスを注入するクラス -
notifyListener
メソッド:状態変更を通知するメソッド
準備
具体的にカウンターアプリを例に見ていきましょう。
サンプルコードはこちら
今回はcountフィールドを持つCounterObj
クラスの状態管理をしていきます。
class CounterObj {
CounterObj() : count = 0;
int count;
}
状態を保持するクラスProviderCounterState
を準備。
-
ChangeNotifier
クラスを継承する事で前述の通知メソッドnotifyListener
を使う事が出来ます。 - クラスに定義したメソッドで保持している状態の値に変更を加え、
notifiyListener
で変更を外部に通知します。
class ProviderCounterState extends ChangeNotifier {
ProviderCounterState() : obj = CounterObj();
CounterObj obj;
void incrementCounter() {
obj.count++;
notifyListeners();
}
void decrementCounter() {
obj.count--;
notifyListeners();
}
void resetCounter() {
obj.count = 0;
notifyListeners();
}
}
ChangeNotifierProvider
を使って状態管理クラスをUIに注入。
-
create
フィールドで状態管理クラスProviderCounterState
をインスタンス化 -
child
フィールドに定義した_ProvoiderCounterPage
widgetにインスタンスを注入
class ProviderCounterPage extends StatelessWidget {
const ProviderCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ProviderCounterState(),
child: _ProviderCounterPage(),
);
}
}
これによりwidgetツリー上で_ProviderCounterPage
widgetより下に位置する全てのWidgetでProviderCounterState
クラスにアクセスできる様になりました。
状態へのアクセス
状態管理クラスへのアクセス方法には二つあります。
単発的に状態管理クラスにアクセスするcontext.read()
と状態変化を監視するcontext.watch()
です。
メソッドなど定義が変更されないものはcontext.read()
でアクセスし、状態値など変更を検知したい対象についてはcontext.watch()
でアクセスします。
メソッドへのアクセス:
final ProviderCounterState unListenState = context.read<ProviderCounterState>();
FloatingActionButton(
onPressed: unListenState.incrementCounter,
tooltip: 'Increment',
heroTag: 'Increment',
child: Icon(Icons.add),
),
状態値の監視:
-
context.watch()
では前述のnotifyListeners
からの通知に応じて、値を取得し直します。 - 取得し直した値を元にその値を扱うWidgetを再描画(リビルド)します。
- その為、
build
メソッド内でcontext.watch()
を定義してしまうと状態が変化する度にwidgetが丸ごと再描画されてしまいます。
@override
Widget build(BuildContext context) {
// ProviderCounterStateの値が変わる度にbuildメソッドが走る
final ProviderCounterState state =
context.watch<ProviderCounterState>();
return Scaffold(
appBar: MainAppBar(
title: 'ChangeNotifier x Provider',
...
-
notifiListeners
メソッドの通知に応じてラップしているwidgetだけを再描画するConsumer
クラスを使用する事で再描画される範囲を限定する事が出来ます。
...,
Consumer<ProviderCounterState>(
builder: (context, state, _) => Text(
'${state.obj.count}',
style: Theme.of(context).textTheme.headline4,
),
),
...
- 実際に状態値を参照して描画しているText widgetだけを
Consumer
クラスでラップする事で状態値が変更しても再描画されるのはラップされたText Widgetだけになりました -
Consumer
クラスの他に特定の状態値だけを監視し、Selector
クラスを使う事で再描画される条件をより絞る事が出来ます。
全体
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:state_management_examples/widgets/main_appbar.dart';
// 状態クラス
class CounterObj {
CounterObj() : count = 0;
int count;
}
// 状態管理クラス
class ProviderCounterState extends ChangeNotifier {
ProviderCounterState() : obj = CounterObj();
CounterObj obj;
void incrementCounter() {
obj.count++;
notifyListeners();
}
void decrementCounter() {
obj.count--;
notifyListeners();
}
void resetCounter() {
obj.count = 0;
notifyListeners();
}
}
// 依存関係を注入
class ProviderCounterPage extends StatelessWidget {
const ProviderCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => ProviderCounterState(),
child: _ProviderCounterPage(),
);
}
}
// カウンター本体
class _ProviderCounterPage extends StatelessWidget {
const _ProviderCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('rebuild!');
final ProviderCounterState unListenState =
context.read<ProviderCounterState>();
return Scaffold(
appBar: MainAppBar(
title: 'ChangeNotifier x Provider',
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Consumer<ProviderCounterState>(
builder: (context, state, _) => Text(
'${state.obj.count}',
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FittedBox(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
FloatingActionButton(
onPressed: unListenState.incrementCounter,
tooltip: 'Increment',
heroTag: 'Increment',
child: Icon(Icons.add),
),
const SizedBox(width: 16),
FloatingActionButton(
onPressed: unListenState.decrementCounter,
tooltip: 'Decrement',
heroTag: 'Decrement',
child: Icon(Icons.remove),
),
const SizedBox(width: 16),
FloatingActionButton.extended(
onPressed: unListenState.resetCounter,
tooltip: 'Clear',
heroTag: 'Clear',
label: Text('CLEAR'),
),
],
),
),
);
}
}
実は...
Providerは状態管理手法の事じゃない:
最もポピュラーな状態管理手法としてProviderが紹介される事が多いが、実際Providerパッケージから利用してるのは状態管理クラスをWidgteツリーに沿って流し込むChangeNotifierProvider
であって、状態管理クラスのChangeNotifier
自体ではありません。
そういう意味で言うとProvider
は依存関係の注入を手助けするパッケージであって、状態管理手法自体ではありません。
どうもGoogleIOにてProvider
パッケージを状態管理手法として紹介した事が元凶のようです。
Providerを使うのはProviderだけじゃない:
禅問答の様ですが、先の捉え方がなぜややこしいかというとProviderが様々な状態管理手法で使われているからです。状態管理手法として有名なBLoCでもBlocProvider
クラスによって依存関係をWidgetツリーに沿って流し込んでいきます。
Providerと比較されるべきなのはRiverpodやGetItでは?:
BlocやRedux、GetXなど他の状態管理手法とProviderを同列で比較するような言い方が多いかと思いますが、「Provider = 依存関係の注入を手助けするパッケージ」という視点から考えれば、Providerパッケージと比較されるべきなのは、 厳密にはRiverpod
やServiceLocatorであるGetIt
だと思います。
Providerパターン、Providerパッケージ:
そんな訳でProvider
という言葉を使うとこんがらがるのですが、状態管理手法としてのProvider
と言う時は
「Provider
(パターン) = ChangeNotifier
x ChangeNotifierProvider
の組み合わせ」
と言うニュアンスで考えるのが良さそうです。ちょっと「Providerパターン」なんて名前があるかは定かじゃありませんが。
以上でした
Provider
については記事も溢れていて、特に目新しい事もないと思いますが、個人的には他の状態管理手法と比べていく中で見えた事が一番為になりました。言葉の定義や分類って難しいですねー