※こちらの記事は【Flutter】色んな状態管理手法でカウンターアプリを作ってみるの一部として作成された記事です
主流の元祖 Scoped Model
今回はProvider
を使う際にも出てくるChangeNotifier
の元となった仕組みを持つ状態管理手法Scoped Model
でカウンターアプリを作っていきます
使用するPackage:
- scoped_model v1.1.0
概要
- 元々はGoogleが開発しているOS Fuchsiaで用いられていた
- そのコードを取り出しパッケージ化したもの
- コメント含めて300行くらいのコンパクトなパッケージ
-
Provider
パッケージに似た管理手法であり、Provider普及後は存在感が希薄に - flutter_reduxなどの開発も手掛けたBrian Eganなどが開発に関わっている
- 初版リリースは2017年8月
全体像
特徴はProviderと同じく下記の2点,
- 依存関係がWidgetツリーに沿って下っていく
- 状態変更を明示的に通知する
状態を保持したクラスは依存関係を注入されたWidgetとそのWidgetツリー傘下のWidgetからアクセスが可能になります。Widgetツリーに沿って依存関係が下っていくようなイメージ。
Widgetは状態管理クラスの変数やメソッドをProviderを通してアクセスします。変数を変更した際は用意されている通知メソッドを実行し、変数を利用しているクラスに通知。その通知を受けったクラス達は新しい値で自身を再生成(リビルド)する事になります。
キーとなるクラスやメソッド
-
Model
クラス:状態管理クラスが継承するクラス -
ScopedModel
クラス:状態管理クラスを注入するクラス -
ScopedModelDescendant
クラス:子孫Widgetから注入されたModel
クラスにアクセスする為のクラス -
notifyListener
メソッド:状態変更を通知するメソッド
準備
具体的にカウンターアプリを例に見ていきましょう。
サンプルコードはこちら
今回はcountフィールドを持つCounterObj
クラスの状態管理をしていきます。
class CounterObj {
CounterObj() : count = 0;
int count;
}
状態を保持するクラスScopedModelCounterState
を準備。
-
Model
クラスを継承する事で前述の通知メソッドnotifyListener
を使う事が出来ます。 - クラスに定義したメソッドで保持している状態の値に変更を加え、
notifiyListener
で変更を外部に通知します。
class ScopedModelCounterState extends Model {
ScopedModelCounterState() : obj = CounterObj();
CounterObj obj;
void incrementCounter() {
obj.count++;
notifyListeners();
}
void decrementCounter() {
obj.count--;
notifyListeners();
}
void resetCounter() {
obj.count = 0;
notifyListeners();
}
}
ScopedModel
クラスを使って状態管理クラスをUIに注入。
-
model
フィールドに状態管理クラスScopedModelCounterState
のインスタンスを渡す -
child
フィールドで子孫となる_ScopedModelCounterPage
widgetにインスタンスを注入
class ScopedModelCounterPage extends StatelessWidget {
const ScopedModelCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ScopedModel(
model: ScopedModelCounterState(),
child: _ScopedModelCounterPage(),
);
}
}
これによりwidgetツリー上で_ScopedModelCounterPage
widgetより下に位置する全てのWidgetでScopedModelCounterState
クラスにアクセスできる様になりました。
状態へのアクセス
状態管理クラスへのアクセス方法は中身は同じですが、二通りの書き方が有ります
ベースとなるのはScopedModelDescendant
クラスを使ったアクセスです。builder、child、rebuildOnChangeと3つの引数を持ち、builderで状態管理クラスへのアクセス権を付与したwidgetを返します
builderではBuildContext, child引数で渡したWidget, 型で定義したModelクラスをラップしたwidgetに渡す事ができ、これを介して状態管理クラスへアクセスします
child引数で渡したWidgetはあまり使うことはないかも知れませんが、Model
クラスと関わりのないWidgetでModelの変更時もリビルドされたくないWidgetを渡す事ができます
rebuildOnChange
はnotifyListeners
が実行された際に、Model
クラスを参照しているWidgetをリビルドするかを制御します
ScopedModelDescendant<ScopedModelCounterState>(
builder: (context, _, model) => Text(
'${model.obj.count}',
style: Theme.of(context).textTheme.headline4,
),
),
もう一つのアクセス方法はScopedModel.of
を使ったアクセスです
こちらではWidgetをラップする必要はなく、参照する箇所に直接値を渡せます
上記のScopedModelDescendant
クラスを使った例を書き直すと下記の様になります
Text(
'${ScopedModel.of<ScopedModelCounterState>(context).obj.count}',
style: Theme.of(context).textTheme.headline4,
),
これらの大きな違いはScopedModelDescendant
クラスではrebuildOnChange
がデフォルトでtrue
となっている事とリビルドされるスコープはラップしたwidgetに限定されます
一方、ScopedModel.of
ではrebuildOnChange
がデフォルトでfalse
になっている事、またrebuildOnChange
の引数にtrue
を渡す事でリビルドを走らせる事も可能ですが、Modelクラスを注入したWidgetが丸ごとリビルドされてしまいます
全体
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:state_management_examples/widgets/main_appbar.dart';
class CounterObj {
CounterObj() : count = 0;
int count;
}
class ScopedModelCounterState extends Model {
ScopedModelCounterState() : obj = CounterObj();
CounterObj obj;
void incrementCounter() {
obj.count++;
notifyListeners();
}
void decrementCounter() {
obj.count--;
notifyListeners();
}
void resetCounter() {
obj.count = 0;
notifyListeners();
}
}
class ScopedModelCounterPage extends StatelessWidget {
const ScopedModelCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ScopedModel(
model: ScopedModelCounterState(),
child: _ScopedModelCounterPage(),
);
}
}
class _ScopedModelCounterPage extends StatelessWidget {
const _ScopedModelCounterPage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
print('rebuild!');
final ScopedModelCounterState unListenState = ScopedModel.of(context);
return Scaffold(
appBar: MainAppBar(
title: 'Scoped Model',
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
ScopedModelDescendant<ScopedModelCounterState>(
builder: (context, _, model) => Text(
'${model.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とScoped Modelの違い
Provider
パターンに馴染みがある方はProvider
とどこが違うの?と思ったかと思います
それについては Provider
パッケージの作者であるRemi Rousselet氏本人が以下で返答しています
曰くScoped Model
はListenable
クラスを継承したModel
クラスを使ったアーキテクチャであり、この仕組みはその後ChangeNotifier
としてFlutterに標準実装されています
Provider
はこのChangeNotifier
を活用する事でScoped Model
アーキテクチャを模倣する事も出来るし、それ以外の使い方も出来るパッケージであると説明しています
どちらにせよModel
クラスと同機能のChangeNotifier
が標準実装された事により、Scoped Model
パッケージを使うメリットが無くなったのは事実で、特殊な理由がない限りはChangeNotifier
x Provider
を使えば良いと個人的には思いました
参考
- https://qiita.com/hayassh/items/690fa0d6528e056617b5 (JP)
- https://www.reddit.com/r/FlutterDev/comments/brz0nu/scoped_model_vs_provider/ere338x/ (EN)
- https://stackoverflow.com/questions/56886805/difference-between-changenotifierprovider-and-scopedmodel-in-flutter (EN)
- https://qiita.com/kabochapo/items/ 2b992cc00e9f464c1ea9#bloc%E4%BB%A5%E5%A4%96%E3%81%AE%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%E6%96%B9%E6%B3%95 (JP)