🔬

【Flutter】色んな状態管理で作ってみよう ⑤Scoped Model

2022/02/11に公開

※こちらの記事は【Flutter】色んな状態管理手法でカウンターアプリを作ってみるの一部として作成された記事です

主流の元祖 Scoped Model

今回はProviderを使う際にも出てくるChangeNotifierの元となった仕組みを持つ状態管理手法Scoped Modelでカウンターアプリを作っていきます

使用するPackage:

概要

  • 元々はGoogleが開発しているOS Fuchsiaで用いられていた
  • そのコードを取り出しパッケージ化したもの
  • コメント含めて300行くらいのコンパクトなパッケージ
  • Providerパッケージに似た管理手法であり、Provider普及後は存在感が希薄に
  • flutter_reduxなどの開発も手掛けたBrian Eganなどが開発に関わっている
  • 初版リリースは2017年8月

全体像

特徴はProviderと同じく下記の2点,

  1. 依存関係がWidgetツリーに沿って下っていく
  2. 状態変更を明示的に通知する

状態を保持したクラスは依存関係を注入された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);

  
  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を渡す事ができます

rebuildOnChangenotifyListenersが実行された際に、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 ModelListenableクラスを継承したModelクラスを使ったアーキテクチャであり、この仕組みはその後ChangeNotifierとしてFlutterに標準実装されています

ProviderはこのChangeNotifierを活用する事でScoped Modelアーキテクチャを模倣する事も出来るし、それ以外の使い方も出来るパッケージであると説明しています

どちらにせよModelクラスと同機能のChangeNotifierが標準実装された事により、Scoped Modelパッケージを使うメリットが無くなったのは事実で、特殊な理由がない限りはChangeNotifier x Providerを使えば良いと個人的には思いました

参考

Discussion