1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-02-10

※こちらの記事は【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);

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

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);

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

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

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

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?