Flutterのproviderパターンでページを追加するプログラムを書いてみた

どうもみやジックです。

僕は普段providerパターンでflutterのアプリ開発をしています。
providerパッケージを用いたMVVMモデルの開発です。
ここではproviderパターンの説明はしませんが、ざっくり言うとアプリの画面の見た目を構成するViewとロジックを書くModelを用意してアプリを開発していく感じです(かなり不十分な説明)。

つまり、アプリの画面1ページにつき2つのdartファイルが必要です。今までは一つ画面を増やすたびに、ディレクトリ作成、ファイル作成、それぞれコピペしてきて、クラス名等を書き換えていました。
何度も同じことを繰り返す作業は自動化するのがエンジニアの性なので上記のファイルを自動生成するプログラムを書きました。これを紹介します。


## やりたいこと
・新しいディレクトリを作成(作成するページ名のディレクトリ、今回はhogeとする)
・作成したディレクトリにdartファイルを作成(hoge_page.dart,hoge_model.dart)
・最小限のページを表示できるHogePageクラスとHogeModelクラスをコピペする


## 実装していく
上記のやりたいことを実現するプログラムをPythonで書くことにした。
理由は僕が一番慣れているから、、、

多分今回やりたいことを一番簡単にできる言語はPythonだと思う。知らんけど

今回のやりたいことから実装することを整理すると以下の4つの工程に分けられる。
・ページ名を取得する
・適切なパスにディレクトリを作成する
・作成したディレクトリにファイルを作成する
・上記のファイルにテキストを貼り付ける


開発しているアプリのディレクトリに今回のプロジェクトを入れたくはなかったので、同じ階層にプロジェクトを作成。
下記に必要なところだけ抜粋したプロジェクト構成を示す。SepakJudgeがFlutterプロジェクトでPageCreateがPythonプロジェクトのディレクトリである。<font color="Red">赤字</font>で示しているのはPythonプログラムによって追加されるディレクトリとファイルである。<font color="Blue">青字</font>で示しているのは新しく作成したファイルにコピペするためのテキストファイルである。

├SepakJudge
&emsp;└ lib
&emsp;&emsp;└ presentation
&emsp;&emsp;&emsp;└ <font color="Red">hoge</font>
&emsp;&emsp;&emsp;&emsp;├ <font color="Red">hoge_page.dart</font>
&emsp;&emsp;&emsp;&emsp;└ <font color="Red">hoge_model.dart</font>
└PageCreate
&emsp;├ main.py
&emsp;├ create_page_for_mobile.py
&emsp;├ <font color="Blue">mobile_page.txt</font>
&emsp;└ <font color="Blue">mobile_model.txt</font>

最初にmain()を定義してPythonのおまじないも書いておく。

```main.py
def main():
    print('新しく作成するページ名を入力してください(UpperCamelCase)')
    new_page_name = input()  ## アッパキャメルケースを想定
    print('新しく', new_page_name, 'ページを作成しますか？ y/N')
    isOK = input()

if __name__ == '__main__':
    main()
```
まず初めにページ名を入力している。
ページ名はアッパーキャメルケースで要求している。クラスの定義に使う用である。

多分dartの慣習であるが、自分は
ファイル名はスネークケース
クラス名はアッパーキャメルケース
変数名はロワーキャメルケース
で書いているため、受け取ったページ名を後から適宜編集する必要がある。(英語を無理矢理カタカナで書くと頭悪そう)

次にCreatePageForMobileクラスを定義、ここの中でディレクトリやファイルを作成したりしていく。
なぜForMobileかというとFlutterWebはFlutterWebでクラスを定義したいから。

CreatePafeForMobileではコンストラクタとしてさっきのpage名とかを取得したい。

```create_page_for_mobile.py
import inflection
class CreatePageForMobile():
    def __init__(self, newPageName: str):
        self.newPageName = newPageName
        self.newPageNameSnakeCase = inflection.underscore(newPageName)
```
inflectionをインポートしてinflection.underscoreでアッパーケースからスネークケースを取得している。便利

次にディレクトリ作成メソッドを記述する。

```create_page_for_mobile.py
    def create_the_directory(self):
        path = '../SepakJudge/lib/presentation/' + self.newPageNameSnakeCase
        os.mkdir(path)
```
※osモジュールをインポートする必要あり
ディレクトリ階層に注意してpresentation配下にスネークケースでディレクトリ作成


次にファイル作成メソッドを記述する。
今回はpageかmodelを引数にとってメソッド内で条件分岐させてファイルを生成する。mainで2回呼ぶことになる。

```create_page_for_mobile.py
    def create_the_dart_file(self, temp: str):  ##tempには'page'or'model'
        ### テキストファイルの読み込み
        originalFile = open('./mobile_%s.txt' % temp, 'r')
        originalText = originalFile.read().replace('Temp', self.newPageName)
        if temp == 'page':
            originalText = originalText.replace('//pythonプログラムによって自動生成',
                                                "import '%s_model.dart';" % self.newPageNameSnakeCase)
        originalFile.close()

        ### dartファイルの作成
        path = '../SepakJudge/lib/presentation/' + self.newPageNameSnakeCase
        f = open(path + '/' + self.newPageNameSnakeCase + '_%s.dart' % temp, 'w')
        f.write(originalText)
        f.close()
```

まず、用意しておいたテキストファイルを読み込む。orginalTextに文字列として格納する。
ここで、replaceメソッドを使ってテキストファイル内のTempを新しいページの名前に置き換える。
また、pageの方ではmodelをインポートする必要があるので、インポート分も差し込む。
replaceを使うための印として`//pythonプログラムによって自動生成`とテキストファイルに入れておくことでこの文とインポート文を置き換えている。
次に新しく生成するファイルを開き、originalTextを書き込む。


今回読み込んだテキストファイルは以下の二つになっている。
今回は汎用性を高めるためにかなり最小限の記述しかしていないが、毎回かくWidgetやコンストラクタも書いておくと便利かもしれない。

```mobile_page.txt
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
//pythonプログラムによって自動生成

class TempPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<TempModel>(
      create: (_) => TempModel(), //TempModelを作成
      child: Scaffold(
        appBar: AppBar(
          title: Text('TempPage'),
        ),
        body: Consumer<TempModel>(
          builder: (context, model, child) {
            return Container();
          },
        ),
      ),
    );
  }
}

```
```mobile_model.txt
import 'package:flutter/material.dart';

class TempModel extends ChangeNotifier {}
```

以上でページ作成のクラスは出来上がり！
これらをmain()で呼んであげる必要がある。

```main.py
from create_page_for_mobile import CreatePageForMobile

def main():
    print('新しく作成するページ名を入力してください(UpperCamelCase)')
    new_page_name = input()  ## アッパキャメルケースを想定
    print('新しく作成するページのタイトルを入力してください')
    new_page_title = input()
    print('新しく', new_page_name, 'ページを作成しますか？ y/N')
    isOK = input()
    if isOK == 'y' or isOK == 'yes':
        createPage = CreatePageForMobile(new_page_name, new_page_title)
        createPage.create_the_directory()
        createPage.create_the_dart_file('page')
        createPage.create_the_dart_file('model')

if __name__ == '__main__':
    main()
```
## できたこと

まずは実行した時の流れを示す。

```
新しく作成するページ名を入力してください(UpperCamelCase)
>hoge
新しく Hoge ページを作成しますか？ y/N
>y
```

次に作成されたファイルを示す。

```hoge_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'hoge_model.dart';

class HogePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<HogeModel>(
      create: (_) => HogeModel(), //HogeModelを作成
      child: Scaffold(
        appBar: AppBar(
          title: Text('HogePage'),
        ),
        body: Consumer<HogeModel>(
          builder: (context, model, child) {
            return Container();
          },
        ),
      ),
    );
  }
}
```

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

class HogeModel extends ChangeNotifier {}
```

読み込んだテキストファイルと比べると必要なところが書き換えられていることがわかる。
また出来上がったページはこんな感じである。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/935513/39b85522-b92a-2a59-7b1e-66de859b1a46.png" width= 200>

このページへの導線は引く必要があるが画面を追加するための手間が少し楽になった。

webの方ではもう少しだけやらなければいけないことがあるので記事にするか迷ってる。