【Flutter】色んな状態管理で作ってみよう ⑤Scoped Model
※こちらの記事は【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フィールドで子孫となる_ScopedModelCounterPagewidgetにインスタンスを注入
class ScopedModelCounterPage extends StatelessWidget {
const ScopedModelCounterPage({Key key}) : super(key: key);
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);
Widget build(BuildContext context) {
return ScopedModel(
model: ScopedModelCounterState(),
child: _ScopedModelCounterPage(),
);
}
}
class _ScopedModelCounterPage extends StatelessWidget {
const _ScopedModelCounterPage({Key key}) : super(key: key);
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)
Discussion