Scoped Modelの基本的な使い方（Flutter）

今回はscoped modelについて書いてみようと思います。
[Dart Meetup Tokyo #5](https://dartisans-jp.connpass.com/event/91133/)での発表の中でもちょっと触れているやつです。ちなみにその時のスライドはこちらです。
[FlutterとAngularDartを DIとClean Architectureでいい感じにする](https://speakerdeck.com/takayukihayashi/fluttertoangulardartwo-ditoclean-architecturede-iigan-zinisuru)


#Scoped Model

## Scoped Modelとは

Scoped Modelの特徴はざっくりまとめると下記のような感じです。

- 親Widgetから子孫のWidgetにModelを簡単に渡す事が出来るようになる
- Modelが更新された場合に、そのModelを参照しているWidgetを再レンダリングする
- **StatefullWidgetとStateを使用する必要が無くなる**

　個人的にはStatefullWidgetとStateを使う必要がないのが大きいです（Scoped Modelの内部では使用されていますが、そのあたりを自然な形で隠蔽してくれています）。Flutterのアーキテクチャーを考えれば必要なのは良くわかるのですが、ぶっちゃけあれって面倒ですよね？ｗ
なのでScoped Modelを使う事で全てのWidgetをStatelessにする事が出来るので個人的に凄く書きやすくなると思っています。

## Fuchsiaで取り入れられている
　日本では話題になることが少ないですが、現在Googleが開発している[Fuchsia](https://github.com/fuchsia-mirror)というマイクロカーネルのOSがあります。そのFuchsiaではFlutterでGUIが開発されているのですが、その中でScoped Modelの考えが使われています。ちなみにScoped ModelのライブラリーをFuchsia側で使っているわけではなくFuchsia側のソースをそのまま切り出してScoped Modelのプラグインにしています。ソースコードはコメントを入れても300行くらいです。

## 実際の使い方

　android studio等でflutterのプロジェクトを作成した際にデフォルトで作成されている「ボタンをクリックすると数字がカウントアップするアプリ」を例にScoped Modelを利用した場合にどうなるか見ていきます。READMEに書いてあるやつそのままですｗ

####依存関係の追加
 flutterのプラグインとして[提供](https://pub.dartlang.org/packages/scoped_model)されています。なのでpubspec.yamlに依存関係を追加すれば使えるようになります。

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  scoped_model: ^0.3.0 #これを追加
```

####Modelの作成

　親Widgetと子孫Widgetで共有するWidgetを作成します。

```dart
import 'package:scoped_model/scoped_model.dart';

class CounterModel extends Model {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    _counter++;
    //状態を変更したらnotifyListenersを呼ぶ。
    notifyListeners();
  }
}
```
Modelというクラスをextendsする形で独自のModelクラスを定義します。このModelはカウンターの数字情報を保持しつつその数字をincrementする処理を持ちます。
また、状態（_counter）を変更した場合にWidgetの再レンダリングを実行するためにnotifyListenersを呼び出します。これを呼び忘れると再レンダリングは実行されないので注意が必要です。


####Scoped Modelの定義
　Scoped Modelを定義すると指定したmodelがその子孫Widget内にて容易に参照出来るようになります。

```dart
void main() => runApp(new MyApp(CounterModel()));

class MyApp extends StatelessWidget {

  final CounterModel counterModel;

  MyApp(this.counterModel);

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Scoped Model Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      //共通でmodelを使用したいルート（親）WidgetにてWidgetをWrapするように定義する
      home: ScopedModel<CounterModel>(
          model: counterModel,
          child: new MyHomePage('Scoped Model Demo')
      )
    );
  }
}
```
　コンストラクタの `model` には子孫Widgetにて参照したいModelを指定します。また `child` には通常のWidgetを指定します。一瞬「？？」となるかもしれないですが`ScopedModel<T>`は単なるStatelessWidgetです。単に使いたいWidgetをSceopedModelでWrapすると考えれば大丈夫です。

####子孫WidgetにてModelを参照する

 子孫WidgetにてModelを参照するには`ScopedModelDescendant<T>`を使用します。

```dart
class MyHomePage extends StatelessWidget {

  final String title;

  MyHomePage(this.title);

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(title),
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            //Modelの情報を参照したいwidgetをScopedModelDescendantにてWrapし、builder関数を定義する
            ScopedModelDescendant<CounterModel>(
              builder: (context, child, model) =>
              new Text(
                '${model.counter}',
                style: Theme.of(context).textTheme.display1,
              )
            )
          ],
        ),
      ),
        //Modelの情報を参照したいwidgetをScopedModelDescendantにてWrapし、builder関数を定義する
      floatingActionButton:ScopedModelDescendant<CounterModel>(
          builder: (context, child, model) {
            return new FloatingActionButton(
              //Modelのincrementを呼び出してカウントアップする。
              onPressed: () => model.increment(),
              tooltip: 'Increment',
              child: new Icon(Icons.add),
            );
          }
      )
    );
  }
}
```
　CounterModelのカウント情報をText Widgetに表示しますが、ここで`ScopedModelDescendant<T>`を使用します。
そして`builder`という名前付き引数を指定します。ここで指定するのは下記定義に沿った関数です。

```dart
//scoped modelより抜粋
typedef Widget ScopedModelDescendantBuilder<T extends Model>(
  BuildContext context,
  Widget child,
  T model,
);
```
　Widgetを戻り値とし、第一引数にはBuildContext、第二引数にはScopedModelDescendantBuilderのコンストラクタにオプションで指定するWidget（これの使い道がよくわかっていないです・・・）、第三引数にはScopedModelで指定したModel（今回はCounterModel）を引数として受け取る関数となります。今回でいうと下記の2箇所です。

```dart
ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) =>
    new Text(
      '${model.counter}',
      style: Theme.of(context).textTheme.display1,
    )
)
```
```dart
floatingActionButton:ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) {
    return new FloatingActionButton(
      //Modelのincrementを呼び出してカウントアップする。
      onPressed: () => model.increment(),
      tooltip: 'Increment',
      child: new Icon(Icons.add),
    );
  }
)
```

　どうでしょうか。1つWrapするための記述（ScopedModelとScopedModelDescendant）が必要になりますがStatefullWidgetとStateの記述がなくなり、より本質的なUIに専念しやすくなっていると思います。

####ScopedModelDescendantを使わずにModelを参照する
　ScopedModelDescendantはbuilder関数を定義しますが、第一引数のBuildContextや第二引数のWidgetを使わない事も多いと思います。その場合はもっとダイレクトにModelを取得するやり方がScoped Modelにはあります。上記カウント情報を参照している部分は下記のようになります。

```dart
children: <Widget>[
            new Text(
              'You have pushed the button this many times:',
            ),
            new Text(
              '${ScopedModel.of<CounterModel>(context, rebuildOnChange: true).counter}',
              style: Theme.of(context).textTheme.display1,
            )
          ]
```
　ScopedModelDescendantの記述が無くなったのでよりすっきりしたコードになったと思います。
　ScopedModelにstaticな`of`メソッドが定義されていているのでそれを呼び出す形です。Genericsの指定と`rebuildOnChange`にtrueを指定する必要があります。
もしくは下記のようにCounterModel内にstaticメソッドを指定する事も可能で、その場合は`rebuildOnChange`にtrueを指定する必要はありません。

```dart
class CounterModel extends Model {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    _counter++;
    notifyListeners();
  }
  //このofメソッドを追加
  static CounterModel of(BuildContext context) => ScopedModel.of<CounterModel>(context);
}
```

```dart
new Text(
  '${CounterModel.of(context).counter}',
  style: Theme.of(context).textTheme.display1,
)
```

　とりあえず今回は以上になります。Dart Meetupの時にも話をしたのですがScoped ModelとDIと組み合わせる事でClean Architectureでプログラミングするのがとても容易になるのが自分が好きなポイントです。大きなアプリを作った時にどうなのかという点はあるかと思いますが、その点はそもそもFuchsiaで使用されているので上手く行かなかったら自分のスキルが足りないという事にしたいと思いますｗ。
　Flutterの理解がある程度あればScoped Modelを使うのも難しくないと思うのでそのあたりも良いポイントかなと思います。