Help us understand the problem. What is going on with this article?

Scoped Modelの基本的な使い方(Flutter)

More than 1 year has passed since last update.

今回はscoped modelについて書いてみようと思います。
Dart Meetup Tokyo #5での発表の中でもちょっと触れているやつです。ちなみにその時のスライドはこちらです。
FlutterとAngularDartを DIとClean Architectureでいい感じにする

Scoped Model

Scoped Modelとは

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

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

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

Fuchsiaで取り入れられている

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

実際の使い方

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

依存関係の追加

flutterのプラグインとして提供されています。なのでpubspec.yamlに依存関係を追加すれば使えるようになります。

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

Modelの作成

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

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内にて容易に参照出来るようになります。

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>を使用します。

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という名前付き引数を指定します。ここで指定するのは下記定義に沿った関数です。

//scoped modelより抜粋
typedef Widget ScopedModelDescendantBuilder<T extends Model>(
  BuildContext context,
  Widget child,
  T model,
);

 Widgetを戻り値とし、第一引数にはBuildContext、第二引数にはScopedModelDescendantBuilderのコンストラクタにオプションで指定するWidget(これの使い道がよくわかっていないです・・・)、第三引数にはScopedModelで指定したModel(今回はCounterModel)を引数として受け取る関数となります。今回でいうと下記の2箇所です。

ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) =>
    new Text(
      '${model.counter}',
      style: Theme.of(context).textTheme.display1,
    )
)
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にはあります。上記カウント情報を参照している部分は下記のようになります。

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を指定する必要はありません。

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);
}
new Text(
  '${CounterModel.of(context).counter}',
  style: Theme.of(context).textTheme.display1,
)

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした