[Flutter] (Cocoa) MVCぽく作ってみる

# やりたいこと
iOSアプリ開発からFlutter開発に移行するにあたって、できるだけCocoa MVCぽく作りたい。ViewControllerクラスを作成して、Viewを制御（addSubViewやvisibleの切り替え）したり、自由にModelクラスを生成したりしたい。

# やったこと
- 画面を構成するルートWidgetはStatelessWidgetとする。
 - buildメソッドの中でViewController（自作）を生成する。
 - state管理は、Providerパターン（ChangeNotifierProvider）を使用する。
- ルートのWidgetはStackを使用する。
 - Stackで表示するListをViewControllerクラスが保持する。
 - こうすることで、Viewの生成や制御をViewControllerで行うようにする。
 - ListはWidgetをそのまま配列にするのではなく、visible（表示/非表示フラグ）などその他の制御をするために構造体の配列とする。

# ソースコード
今回ViewControllerと、View（画面を表すWidget）をそれぞれBaseクラスに分けたので、以下のような構成で3ファイルに分割している。

1. StartPage.dart
2. BaseViewContoller.dart
3. BasePageView.dart

#### 最初に、StartPage.dart の全文
```StartPage.dart
import 'package:sample/page/base/BaseViewController.dart';
import 'package:sample/page/base/BasePageView.dart';
import 'package:sample/widget/BaseButton.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// （1）画面Widgetクラス
class StartPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final viewController = PageViewController(context);
    return ChangeNotifierProvider(
      create: (context) => viewController,
      child: StartPageView(viewController),
    );
  }
}

// （2）画面Widgetクラス ビュー
class StartPageView extends BasePageView {
  BaseViewController viewController;
  StartPageView(this.viewController) : super(viewController);

  @override
  Widget build(BuildContext context) {
    final pageViewController = context.watch<PageViewController>();
    return super.build(context);
  }
}

// （3）画面ViewController 処理実装
class PageViewController extends BaseViewController {
  BuildContext context;
  PageViewController(this.context) : super(context) {
  }

  @override
  void initialWidget() {
    addSubWidgets(name: "wid1", widget: Positioned(top: 50, left: 100, child: BaseButton(160, 60, onButtonPush)));
  }
  void onButtonPush() {
    addSubWidgets(name: "wid2", widget: Positioned(top: 120, left: 100, child: BaseButton(160, 60, someMethod)));
    redraw();
  }
}
```
各画面ファイルは上記のような構成となり、基本的に、画面ViewController（PageViewControllerクラス）にViewの作成も含めて処理を実装していく。
各Baseクラスと合わせて、解説は以下で記述する。

#### まずは、ViewContollerクラス

```BaseViewContoller.dart
import 'package:flutter/material.dart';

// [※1]
class BaseViewController with ChangeNotifier {
  BuildContext context;
  // [※2]
  List<SubWidgetItem> subWidgets;
  // [※3]
  BaseViewController(this.context) {
    subWidgets = List<SubWidgetItem>();
    initialWidget();
  }

  // [※4]
  void initialWidget() {
  }

  // [*5]
  void addSubWidgets({@required String name, @required Widget widget, bool isVisible}) {
    // すでにリストにあるかどうかを探す
    SubWidgetItem createWidget = null;
    for (final swi in subWidgets) {
      if (swi.name == name) {
        createWidget = swi;
        break;
      }
    }

    // なければ新規で作成
    if (createWidget == null) {
      createWidget = SubWidgetItem();
      subWidgets.add(createWidget);
      createWidget.name = name;
      // isVisible省略時はtrue
      if (isVisible == null) {
        createWidget.isVisible = true;
      }
    }

    // 値をセット
    createWidget.widget = widget;
    if (isVisible != null) createWidget.isVisible = isVisible;
  }

  // [*6]
  void modVisible({@required String name, @required bool isVisible}) {
    for (final swi in subWidgets) {
      if (swi.name == name) {
        swi.isVisible = isVisible;
        return;
      }
    }
  }

 // [※7]
  SubWidgetItem getWidgetItemByName(String name) {
    for (final swi in subWidgets) {
      if (swi.name == name) {
        return swi;
      }
    }
  }

  // [*8]
  List<Widget> visibleSubWidget() {
    List<Widget> wlist = List<Widget>();
    subWidgets.forEach((subWidgetItem) {
      if (subWidgetItem.isVisible) {
        wlist.add(subWidgetItem.widget);
      }
    });
    return wlist;
  }

  // [※9]
  void redraw() {
    notifyListeners();
  }
}

// [*10]
class SubWidgetItem {
  Widget widget;
  bool isVisible;
  String name;
}

```
共通化するためにBaseクラスにしてしまったので、少々わかり難くなってしまっているが、必要な要素は今のところこれだけ。

1. このクラスがChangeNotifierProviderの送り手となるので、ChangeNotifierをmixin（with）する。
2. Stackで描画するWidgetを中で生成するので、WidgetのListを持つ。このとき、ViewControllerからWidgetの表示/非表示の切り替え（や後々その他制御）をするために、後述（※9）のSubWidgetItemクラスのListにしている。
3. 画面遷移（Navigator.push/pop）なんかもやりたいので、contextを持つ。
4. ここで（子クラスでoverrideして）初期画面を生成する。
5. subWidgetsにaddするメソッドを用意している。説明は省くが、同じnameの物があれば上書き、なければ新規で登録（Listに追加）、のような処理としている。
6. subWidgetsをfor/inで回してvisibleの値を変更するメソッド。ちなみに、subWidgetsをMapにしなかったのは、表示順をListのadd順にしたかったため。Mapにして、sort値を持たせても良いが面倒だったので・・・。
7. List&lt;SubWidgetItem&gt;からnameを指定して値を取り出している。このままではダサいのでMapでインデックスを作成するか、上記（※6）の通り、そもそもSubWidgetItemのコンテナをMapにしてしまうか、要改善。
8. 後述のBasePageViewで表示するために、subWidgetsのList（List&lt;SubWidgetItem&gt;）から、visible値を見ながら、中身のWidgetだけを取り出したList（ListList&lt;Widget&gt;）に変換している。
9. Providerパターンでは、notifyListenersが呼ばれると画面が再描画されるので、一応Viewからも再描画を呼べるようにラップしておく。
10. widgetに付随する、visible値などを持った構造体（クラス）。

#### 次に、このページ固有の実装をする、PageViewControllerクラス

```StartPage.dart
// （3）画面ViewController 処理実装
class PageViewController extends BaseViewController {
  //[※1]
  BuildContext context;
  PageViewController(this.context) : super(context) {
  }

  //[※2]
  @override
  void initialWidget() {
    addSubWidgets(name: "wid1", widget: Positioned(top: 50, left: 100, child: BaseButton(160, 60, onButtonPush)));

    // [*3]
    addSubWidgets(name: "wid2", widget: その他Widget);
    addSubWidgets(name: "wid3", widget: その他Widget);
    addSubWidgets(name: "wid4", widget: その他Widget);
  }

  //[※4]
  void onButtonPush() {
    addSubWidgets(name: "wid5", widget: Positioned(top: 120, left: 100, child: BaseButton(160, 60, () {
      modVisible(name: "wid2", isVisible: false);
      redraw(); // wid2が非表示になる
    })));
    redraw(); // wid5が表示される
  }
}
```
このクラスでViewを作成したり、今回未登場だがModelクラスを操作したりして、アプリを開発していく。先述のBaseViewControllerを継承して実装する。

1. BaseViewController側でも利用できるように、インスタンス引数にcontextを渡す。
2. 初期画面を構成するメソッド。BaseViewControllerが持っているので、overrideして実装する。（ここで登場するBaseButtonは自作のボタンWidgetで、第1、2引数でサイズを指定し、第3引数がCallBackメソッドを渡している。）これをBaseViewControllerで実体化したsubWidgets（List&lt;SubWidgetItem&gt;）にaddSubWidgetsメソッドで追加することで、後述するBasePageViewの中で描画される。ここでは、50,100の座標に、160x60のサイズでボタンを表示しており、これをタップすると、onButtonPushメソッドが呼ばれるようになっている。
3. ここでいろいろなWidgetをStackに重ねて画面を作成していく。
4. 上述のボタン（wid1）をタップするとここが呼ばれる。ここでもaddSubWidgetsしてWidgetを追加しているが、redraw（＝notifyListeners）を呼ぶことで画面が再描画され、そのタイミングで画面上にもう一つボタン（wid5）が表示される。（ここではダイアログのようなイメージ。ちなみにそのボタンをタップすると、modVisibleが呼ばれ、wid2の登録したWidgetが非表示になる。）

#### 次に、これを利用する画面Widget（StatelessWidget）クラス

```StartPage.dart
// 画面Widgetクラス
class StartPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // [※1]
    final viewController = PageViewController(context);
    return ChangeNotifierProvider(
      create: (context) => viewController, // [※2]
      child: StartPageView(viewController), // [※3]
    );
  }
}
```
これが、この画面のルートとなるWidgetクラスとなる。StatelessWidgetで実装する。

1. PageViewControllerの親クラス（BaseViewController）にcontextを持たせるためにインスタンス引数に渡す。
2. 上で生成したPageViewControllerが、ChangeNotifierProviderの送り手となるので、ChangeNotifierProviderのcreateに渡す。
3. 同様に、PageViewControllerの親クラス（BaseViewController）がStackで描画するためのWidgetのListを持っているので、次で述べるViewクラス（といってもただのStatelessWidget）のインスタンス引数に渡す。

#### 次は、Viewクラス
これもまずBaseクラス。

```BasePageView.dart
import 'package:sample/page/base/BaseViewController.dart';
import 'package:flutter/material.dart';

class BasePageView extends StatelessWidget {
  BaseViewController viewController;
  // [※1]
  BasePageView(this.viewController);
  @override
  Widget build(BuildContext context) {
    return Stack(
        children: <Widget>[
              // [※2]
              Container(
                width: MediaQuery.of(context).size.width,
                height: MediaQuery.of(context).size.height,
                // 背景画像セット
                decoration: BoxDecoration(
                  image: DecorationImage(
                      image: AssetImage("assets/widget/back.png"),
                      fit: BoxFit.fill),
                ),
              )
            ] +
            viewController.visibleSubWidget()); // [※3]                 
  }
}
```
ここでは、Stack Widgetを返しているだけ。

1. StackするWidgetはViewControllerで生成したいので、ViewControllerの持つListを参照できるようにプロパティに持つ。このあたり親子関係が気持ち悪いがいったん気にしないでおく。
2. このContainerは単純に全てのページで共通のWidgetとして背景画像を読み込んでいるだけ。（そのためにBaseクラスにしたので。）
3. ここでViewControllerのもつList&lt;SubWidgetItem&gt;のうち、isVisibleがtrueのものだけを描画している。

#### 最後に、BasePageViewを継承した、StartPageViewクラス

```StartPage.dart
// 画面Widgetクラス ビュー実装
class StartPageView extends BasePageView {
  //[※1]
  BaseViewController viewController;
  StartPageView(this.viewController) : super(viewController);

  @override
  Widget build(BuildContext context) {
    //[※2]
    final pageViewController = context.watch<PageViewController>();
    //[※3]
    return super.build(context);
  }
}
```
ここでは大したことはやっていない。

1. コンストラクタで先述のBasePageViewにViewControllerを渡している。
2. notifyListenersを検知するために、context.watchしている。（いわゆるProviderパターン。）ここは適切な値（state）を個別でwatchしないと効率が良くないという記事もあるので、要改善か・・・。
3. 画面個別のWidgetはStackの上に重ねる予定で、そしてそれはViewControllerクラスの中で実装するので、ここでは何もせず、親クラスであるBasePageViewのbuildをそのまま返している。

以上の構成で、ViewControllerクラスでView（Widget）を生成したり、再描画を呼び出したり、Cocoa MVCぽく動作するようにしている。

# やってみて
とりあえず、最低限アプリを開発するために必要なところは出来たので、まずはこれで進めてみる。とは言え、記事中でも触れたが、ChangeNotifier部分でパフォーマンスに疑念が残るので、もう少し理解を深めて改善に挑戦したい。