【Flutter】わずか数分でアプリ開発！？超速アプリ開発総合フレームワーク「Masamune」

こんにちは。広瀬マサルです。

これまでのパッケージをまとめて統合Flutterフレームワークを作成しました。

コンセプトは

**自動生成を用いて安全かつ高速に高品質のアプリを開発可能にするフレームワーク**

です。

使い方をまとめたので興味ある方はぜひ使ってみてください！

# masamune

https://pub.dev/packages/masamune

https://pub.dev/packages/masamune_builder

# はじめに

まずはこちらを御覧ください。


https://www.youtube.com/watch?v=K2wze5wFwiM

※動画のサンプルコードは[こちら](https://github.com/mathrunet/flutter_sample_memo_test)に公開しています。


こちらはメモ帳アプリを全くの空の状態からわずか**10分以内**で完成させる動画です。

この`Masamuneフレームワーク`を利用することで**アプリ開発で行うコーディングの大半を削減することが可能**になります。

このフレームワークは下記の機能を中心としています。

- CLI（コマンドラインインターフェース）ツールによるコードテンプレートの生成
- [build_runner](https://pub.dev/packages/build_runner) による追加コードの自動生成

つまりコードの大半を機械的に生成させることにより人間がコーディングする部分を極力減らしてくれるということです。

また、人間がコーディングする部分もほぼタイプセーフとなるためIDEのサジェスト機能などのサポートを受けながら迷うことなく実装を行うことが可能です。

このような機能が提供されることにより下記のベネフィットを享受可能です。

- **実装が早くなる**
    - 実際にコーディングする箇所が減るので実装が早くなります。
- **コーディングミスが少なくなる**
    - 実際にコーディングする箇所が減るのでミスする確率もその分減ります。
- **ソースコードを追いやすくなる**
    - 実際にコーディングする箇所が減るので時間を置いて後からソースを見ても追いやすいです。
- **人によってコードの差がでにくい**
    - 生成されたテンプレートに沿ってコーディングを行うため、人によってのコードの違いがでにくいです。
- **チーム開発を行いやすくなる**
    - `人によってコードの差がでにくい`ためチーム内でのコード確認や人員の割当もやりやすくなります。

さらにこのフレームワークは下記の機能を提供し、アプリ開発を多方面からサポートします。

- `ルーティング機能`
    - WebのURL対応や条件付きリダイレクト、ネストナビゲーションも可能。
- `データベース機能`
    - Firestoreの構造をベースにしたNoSQLデータベース。
    - アダプターを切り替えることで簡単にローカルとFirestoreのスイッチが可能。
- `状態管理機能`
    - [flutter_hooks](https://pub.dev/packages/flutter_hooks)のようなシンプルな形で状態管理を行います。
- `翻訳機能`
    - Googleスプレッドシートを利用した翻訳管理。
- `テーマ管理機能`
    - テーマの色やテキストを定義することができます。
    - 画像ファイルやフォントファイルをコード内でタイプセーフに取り出すことが可能です。
- `SharedPreferences機能`
    - 上記のデータベースとは別にSharedPreferencesを利用したデータストアが可能です。
- `フォーム構築機能`
    - フォームを中心としたユーザーからのデータ入力をUIレベルからサポートします。
- `UIサポート`
    - データの更新があったときに負荷が少なくウィジェットを更新可能なリストウィジェットや簡易モーダルを実装可能な機能を提供します。
- `Firebase/Firestoreサポート`
    - `Authentication`や`Cloud Firestore`、`Cloud Storage`、`Firebase DataConnect`などFirebaseの機能に簡単にスイッチできる機能を提供します。

このフレームワークを利用することで例えば簡単なCRUDアプリの場合実装する部分が下記のみになります。

- `DataScheme`
    - 型と変数名（場合によっては初期値）のみを定義すればよい。
- `View`
    - ウィジェットによるアプリデザインの構築を行います。
        - フォームなどいくつかの要素についてはサポートされます。
    - データバインディングなどは簡単に行うことが可能です。

# インストール

下記コマンドで`CLI`をインストールします。

```bash
flutter pub global activate katana_cli
```

既存のプロジェクトにMasamune Frameworkを導入する場合は下記のコマンドでパッケージを追加してください。

[build_runner](https://pub.dev/packages/build_runner) や [freezed](https://pub.dev/packages/freezed)、[json_serializable](https://pub.dev/packages/json_serializable) も合わせてインストールする必要があります。

```bash
flutter pub add masamune
flutter pub add json_annotation
flutter pub add freezed_annotation
flutter pub add --dev build_runner
flutter pub add --dev masamune_builder
flutter pub add --dev json_serializable
flutter pub add --dev freezed
```

# プロジェクトの作成

プロジェクトを作成したフォルダ内で下記コマンドを実行します。

`Application ID`には**リバースドメイン**（com.test.myapplication）でIDを記述してください。

```bash
katana create [Application ID(e.g. com.test.myapplication)]
```

基本的には`flutter create`と同じになりますが下記が自動で変更されます。

- 自動で`assets`フォルダ以下に画像ファイルを置けるようにしています。
- `katana.yaml`が置かれます。
- 必要パッケージが自動でインストールされます。
- VSCode用のランチャー設定が自動でセットされます。
- `main.dart`が書き換わり、[build_runner](https://pub.dev/packages/build_runner) でコード生成が行われます。

# コード変更の監視

build_runnerの監視機能を用いて対象コードの変更を検知し変更があれば即コードを自動生成します。

build_runner自体のコード解析が非常に遅いため常時監視していた方が開発者体験がとても良くなります。

コードの監視を行う場合は**別ターミナル**で下記のコマンドを実行します。

```bash
katana code watch
```

コマンドを打つと監視状態に入るのでそのまま放置します。

(そのままでは後述のコードテンプレート作成コマンドが打てなくなるので別ターミナルで起動してください)

コード監視を行わない場合は適宜下記コマンドを入力してください。

```bash
katana code generate
```

# 実装

## ページ

### ページの作成

アプリの画面(ページ)を作るには下記のコマンドを実行します。

```bash
katana code page [Page name]
```

`lib/pages`以下に`(Page name).dart`という名前でファイルが作成されます。

`(Page name)Page`というクラスが下記のように作成されます。

StatelessWidgetやStatefulWidgetなどと同じ様に`build`の内部に画面のUIの内容を記述してください。

```dart
// test.dart

// ignore: unused_import, unnecessary_import
import 'package:flutter/material.dart';
// ignore: unused_import, unnecessary_import
import 'package:masamune/masamune.dart';
// ignore: unused_import, unnecessary_import
import 'package:masamune_universal_ui/masamune_universal_ui.dart';

// ignore: unused_import, unnecessary_import
import '/main.dart';

part 'test.page.dart';

@immutable
// TODO: Set the path for the page.
@PagePath("test")
class TestPage extends PageScopedWidget {
  const TestPage({
    super.key,
    // TODO: Set parameters for the page.
    
  });

  // TODO: Set parameters for the page in the form [final String xxx].

  /// Used to transition to the TestPage screen.
  ///
  /// ```dart
  /// router.push(TestPage.query(parameters));    // Push page to TestPage.
  /// router.replace(TestPage.query(parameters)); // Replace page to TestPage.
  /// ```
  @pageRouteQuery
  static const query = _$TestPageQuery();

  @override
  Widget build(BuildContext context, PageRef ref) {
    // Describes the process of loading
    // and defining variables required for the page.
    // TODO: Implement the variable loading process.
    

    // Describes the structure of the page.
    // TODO: Implement the view.
    return UniversalScaffold();
  }
}
```

`@PagePath(””)`内のパスを指定することでディープリンクのパスを指定できます。

```dart
@PagePath("user/:user_id")
```

ページに引数が必要な場合は、単純に下記のようにパラメーターを追加してください。

```dart
const TestPage({
  super.key,
  // TODO: Set parameters for the page.
  required this.name,
  this.text,
});

// TODO: Set parameters for the page in the form [final String xxx].
final String name;
final String? text;

~~~~~~~~~~~~~~~~~~
```

### ページの遷移

main.dartに定義されている`router`を利用してページ遷移を行います。

作成されたクラス中にすでに記述されている`query`を指定してそのページに遷移することができます。

```dart
// TestPageへの遷移
router.push(TestPage.query());

// ページの置き換え
router.replace(TestPage.query());

// 前のページに戻る
router.pop();
```

## 初期ページの設定

main.dartに定義されている`initialQuery`に初期ページに設定したいqueryを渡すことでアプリ立ち上げ時のページを設定できます。

```bash
/// Initial page query.
// TODO: Define the initial page query of the application.
final initialQuery = TestPage.query();
```

その他、下記の機能について詳しく知りたいときはパッケージの詳細ページをご覧ください。

- AppRouterの指定方法
- ディープリンクへの対応
- ネストナビゲーション

https://pub.dev/packages/katana_router

## データモデル

### データ構造

MasamuneフレームワークではFirestoreのデータ構造を参考にしてデータを保存することが出来ます。

https://pub.dev/packages/katana_model#structure

### コレクション（ドキュメント）モデル作成

コレクションモデルを作成するためには下記のコマンドを入力します。

```bash
katana code collection [Collection name]
```

`lib/models`以下に`(Collection name).dart`という名前でファイルが作成されます。

`(Collection name)Model`というクラスが下記のように作成されます。

上記コマンドでコレクションモデルを作成した場合、同一のデータスキームでドキュメントモデルも利用可能になります。

```dart
// test.dart

// ignore: unused_import, unnecessary_import
import 'package:flutter/material.dart';
// ignore: unused_import, unnecessary_import
import 'package:masamune/masamune.dart';

// ignore: unused_import, unnecessary_import
import '/main.dart';

import 'package:freezed_annotation/freezed_annotation.dart';

part 'test.m.dart';
part 'test.g.dart';
part 'test.freezed.dart';

/// Value for model.
@freezed
@formValue
@immutable
// TODO: Set the path for the collection.
@CollectionModelPath("test")
abstract class TestModel with _$TestModel {
  const factory TestModel({
     // TODO: Set the data schema.
     
  }) = _TestModel;
  const TestModel._();

  factory TestModel.fromJson(Map<String, Object?> json) => _$TestModelFromJson(json);

  /// Query for document.
  ///
  /// ```dart
  /// appRef.model(TestModel.document(id));       // Get the document.
  /// ref.app.model(TestModel.document(id))..load();  // Load the document.
  /// ```
  static const document = _$TestModelDocumentQuery();

  /// Query for collection.
  ///
  /// ```dart
  /// appRef.model(TestModel.collection());       // Get the collection.
  /// ref.app.model(TestModel.collection())..load();  // Load the collection.
  /// ref.app.model(
  ///   TestModel.collection().data.equal(
  ///     "data",
  ///   )
  /// )..load(); // Load the collection with filter.
  /// ```
  static const collection = _$TestModelCollectionQuery();

  /// Query for form value.
  ///
  /// ```dart
  /// ref.app.form(TestModel.form(LogModel()));    // Get the form controller in app scope.
  /// ref.page.form(TestModel.form(LogModel()));    // Get the form controller in page scope.
  /// ```
  static const form = _$TestModelFormQuery();
}

/// [Enum] of the name of the value defined in TestModel.
typedef TestModelKeys = _$TestModelKeys;

/// Alias for ModelRef<TestModel>.
///
/// When defining parameters for other Models, you can define them as follows
///
/// ```dart
/// @RefParam(TestModelDocument) TestModelRef test
/// ```
typedef TestModelRef = ModelRef<TestModel>?;

/// It can be defined as an empty ModelRef<TestModel>.
///
/// ```dart
/// TestModelRefPath("xxx") // Define as a path.
/// ```
typedef TestModelRefPath = _$TestModelRefPath;

/// Class for defining initial values to be passed to `initialValue` of [RuntimeModelAdapter].
///
/// ```dart
/// RuntimeModelAdapter(
///   initialValue: [
///     TestModelInitialCollection(
///       "xxx": TestModel(...),
///     ),
///   ],
/// );
/// ```
typedef TestModelInitialCollection = _$TestModelInitialCollection;

/// Document class for storing TestModel.
typedef TestModelDocument = _$TestModelDocument;

/// Collection class for storing TestModel.
typedef TestModelCollection = _$TestModelCollection;

/// It can be defined as an empty ModelRef<TestModel>.
///
/// ```dart
/// TestModelMirrorRefPath("xxx") // Define as a path.
/// ```
typedef TestModelMirrorRefPath = _$TestModelMirrorRefPath;

/// Class for defining initial values to be passed to `initialValue` of [RuntimeModelAdapter].
///
/// ```dart
/// RuntimeModelAdapter(
///   initialValue: [
///     TestModelMirrorInitialCollection(
///       "xxx": TestModel(...),
///     ),
///   ],
/// );
/// ```
typedef TestModelMirrorInitialCollection = _$TestModelMirrorInitialCollection;

/// Document class for storing TestModel.
typedef TestModelMirrorDocument = _$TestModelMirrorDocument;

/// Collection class for storing TestModel.
typedef TestModelMirrorCollection = _$TestModelMirrorCollection;
```

コレクションが必要なくドキュメントモデルのみで良い場合は下記コマンドで作成可能です。

```bash
katana code document [Document name]
```

`@CollectionModelPath("")`（`@DocumentModelPath(””)`）の中身を指定してデータパスを指定します。

Firestoreと同じ様にパス階層の制限があります。

（奇数：コレクション、偶数：ドキュメント）

```dart
@CollectionModelPath("user")
```

また、`factory`コンストラクタの中にデータスキームとなる変数の一覧を記載します。

これがこのコレクション（ドキュメント）で取り扱えるデータの内容となります。

```dart
const factory TestModel({
   // TODO: Set the data schema.
   required String name,
   String? test,
}) = _TestModel;
```

### モデルの利用

作成されたモデルは、ページ内もしくは`ScopedWidget`や`Scoped`で作成されたウィジェット内の場合`build`メソッド内で渡される`PageRef（WidgetRef）`から`ref.app.model`を使用することで取り扱うことが可能です。

`ref.app.model`の中に作成されたモデル内で定義されている`collection`や`document`を渡すことで実態のオブジェクトを取得することができます。

また、取得したオブジェクトの`load`メソッドを実行することでデータベースからデータをロードすることができます。

ロードが完了したときやデータを書き換えた場合、`ref.app.model`を実行したウィジェットがリビルドされます。

```dart
@override
Widget build(BuildContext context, PageRef ref) {
  // Describes the process of loading
  // and defining variables required for the page.
  // TODO: Implement the variable loading process.
  final testModelCollection = ref.app.model(TestModel.collection()); // TestModelのコレクションを取得。
  testModelCollection.load();  // モデルデータを読み込み

  ~~~~~~~~~
}
```

また、ページやウィジェット外で利用したい場合はmain.dartで定義されている`appRef`で利用することが出来ます。

```dart
final testModelCollection = appRef.model(TestModel.collection());
```

### SharedPreferences機能

SharedPreferencesのように通常のDBとは別にローカルにデータを保存したい場合、`ModelAdapter`を用いることで可能です。

詳しくは下記をご参照ください。

https://qiita.com/mathru/items/66ec425d8cf3dbe64dd1

### Googleスプレッドシートのデータソースとしての利用

GoogleスプレッドシートをCSVデータソースとして利用可能にします。

非エンジニアの方が触れるのでデータの設定等を顧客にお願いする場合などにお使いください。

事前にGoogleスプレッドシートを利用可能にします。

1. [こちらのテンプレート](https://docs.google.com/spreadsheets/d/1bfNX8clPH9PFOcfFIStNCGNGjeCKwGv-24iChSJn8yM/edit#gid=0)からスプレッドシートを自分のGoogleドライブにコピーします。
    - `CollectionModelPath`を利用する場合はコレクション用のシートを利用し、`DocumentModelPath`を利用する場合はドキュメント用のシートを利用します。
    - `ファイル` -> `コピーの作成`からコピーが可能です。
2. コピーしたスプレッドシート内で`ファイル` -> `共有` -> `他のユーザーと共有`をクリックします。
3. `（作成したスプレッドシート名）を共有`ウィンドウ内で、`一般的なアクセス`を`**リンクを知っている全員**`に変更します。
4. 作成したクラスに追加する形で「`GoogleSpreadSheetDataSource`」のアノテーションを追加し、ブラウザで表示されているURL（例：`https://docs.google.com/spreadsheets/d/1bfNX8clPH9PFOcfFIStNCGNGjeCKwGv-24iChSJn8yM/edit#gid=0`）をコピーし記載します。
    
    ```dart
    /// Value for model.
    @freezed
    @formValue
    @immutable
    @GoogleSpreadSheetDataSource(
      "https://docs.google.com/spreadsheets/d/1bfNX8clPH9PFOcfFIStNCGNGjeCKwGv-24iChSJn8yM/edit#gid=0",
      version: 1,
    )
    // TODO: Set the path for the collection.
    @CollectionModelPath("test")
    abstract class TestModel with _$TestModel {
      const factory TestModel({
         // TODO: Set the data schema.
         
      }) = _TestModel;
      const TestModel._();
    ```

その他、下記の機能について詳しく知りたいときはパッケージの詳細ページをご覧ください。

- データの編集・削除
- データのフィルタークエリーの指定
- コレクションのソート機能
- テキスト検索
- リレーショナルデータの指定・取得
- トランザクション・バッチ処理
- 特殊なフィールド値

https://pub.dev/packages/katana_model

## コントローラー

`ScrollController`や`TextEditingController`などFlutterですでに用意されているコントローラーをページやウィジェット内で利用したい場合や、データモデルを束ねて使うなど細かい調整を行いたい場合はコントローラーを作成します。

```bash
katana code controller [Controller name]
```

`lib/controllers`以下に`(Page name).dart`という名前でファイルが作成されます。

`(Page name)Controller`というクラスが下記のように作成されます。

```dart
// test.dart

// ignore: unused_import, unnecessary_import
import 'package:flutter/material.dart';
// ignore: unused_import, unnecessary_import
import 'package:masamune/masamune.dart';

// ignore: unused_import, unnecessary_import
import '/main.dart';

part 'test.m.dart';

/// Controller.
@Controller(autoDisposeWhenUnreferenced: false)
class TestController extends ChangeNotifier {
  TestController(
    // TODO: Define some arguments.
    
  );

  // TODO: Define fields and processes.
  
  /// Query for TestController.
  ///
  /// ```dart
  /// appRef.controller(TestController.query(parameters));     // Get from application scope.
  /// ref.app.controller(TestController.query(parameters));    // Watch at application scope.
  /// ref.page.controller(TestController.query(parameters));   // Watch at page scope.
  /// ```
  static const query = _$TestControllerQuery();
}
```

利用する場合は`PageRef（WidgetRef）`を利用して`ref.(page/app).controller()`に`query`を渡すことで実態のオブジェクトを取得できます。

`ref.app.controller`の場合はページ間をまたがって管理され、`ref.page.controller`の場合はそのページのみで管理されます。

`ref.app.controller`で定義した場合は基本的にアプリを終了するまでコントローラーは破棄されません。

`ref.page.controller`で定義した場合はそのページが破棄されるタイミングでコントローラーも破棄されます。

```dart
@override
Widget build(BuildContext context, PageRef ref) {
  // Describes the process of loading
  // and defining variables required for the page.
  // TODO: Implement the variable loading process.
  final testController = ref.page.controller(TestController.query()); // TestControllerを取得。    

  ~~~~~~~~~
}
```

内部としては`ChangeNotifier`を継承しているため、`notifyLisnteners()`を実行すると読み込んだウィジェットがリビルドされます。

また、`appRef`を利用してウィジェット外からも利用することが可能です。

`appRef`の場合はページ間をまたがって管理されます。（`ref.app.controller`と同じ）

```dart
final testController = appRef.controller(TestController.query());
```

## 状態管理

基本的には上記`ref.app.model`、`ref.(page/app).controller`で状態管理の大部分はカバーできるかと思います。

状態管理については拡張も可能なので詳しく知りたいときはパッケージの詳細ページをご覧ください。

https://pub.dev/packages/katana_scoped

## 翻訳

翻訳はGoogleスプレッドシートを通して行います。

準備は下記のページをご覧ください。

https://pub.dev/packages/katana_localization#advance-preparation

翻訳の更新はmain.dartにAppLocalizeが定義されているのでそこの`version`を更新します。

```dart
@GoogleSpreadSheetLocalize(
  "https://docs.google.com/spreadsheets/d/1bw7IXEr7BGkZ4U6on0OuF7HQkTMgDSm6u5ThpBkDPeo/edit#gid=551986808",
  version: 1, // 更新時はこのバージョンをインクリメント。
)
class AppLocalize extends _$AppLocalize {}
```

翻訳テキストの取得は`l`オブジェクトを利用して行います。

```dart
Text(l().success);
```

その他、下記の機能について詳しく知りたいときはパッケージの詳細ページをご覧ください。

- パラメーターの指定
- 翻訳言語の変更

https://pub.dev/packages/katana_localization

## テーマ管理

### アセットの定義

下記の公式サイトを参考にpubspec.yamlを編集し、アプリ内でアセットを読み込めるようにしてください。

https://docs.flutter.dev/development/ui/assets-and-images

```yaml
// pubspec.yaml

flutter:
  assets:
    - assets/images/
```

### フォントの定義

下記の公式サイトを参考にpubspec.yamlを編集し、アプリ内でフォントを読み込めるようにしてください。

https://docs.flutter.dev/cookbook/design/fonts

```yaml
// pubspec.yaml

flutter:
  fonts:
    - family: RobotoMono
      fonts:
        - asset: fonts/RobotoMono-Regular.ttf
        - asset: fonts/RobotoMono-Bold.ttf
          weight: 700
```

### 色とテキストの定義

main.dartの下記の部分を書き換えることで色の指定及びテキストの指定が可能です。

マテリアルデザインのカラースキームに従い下記の色を指定可能です。

https://m3.material.io/styles/color/the-color-system/key-colors-tones

また、マテリアルデザインのTypographyに従いテキストを指定可能です。

https://m3.material.io/styles/typography/type-scale-tokens

```dart
// main.dart

/// App Theme.
///
/// ```dart
/// theme.color.primary   // Primary color.
/// theme.text.bodyMedium // Medium body text style.
/// theme.asset.xxx       // xxx image.
/// theme.font.xxx        // xxx font.
/// ```
@appTheme
final theme = AppThemeData(
  // TODO: Set the design.
  primary: Colors.blue,
  secondary: Colors.cyan,
  onPrimary: Colors.white,
  onSecondary: Colors.white,
);
```

### テーマの利用

main.dartの`theme`を利用して下記のテーマの取得が可能です。

- `color`
    - `AppThemeData`作成時に定義したColorScheme。
- `text`
    - `AppThemeData`作成時に定義したTypeScale
- `asset`
    - コードジェネレーションで作成した`assets`フォルダ以下のアセット
- `font`
    - コードジェネレーションで作成したFontFamily。

```dart
@override
Widget build(BuildContext context, PageRef ref) {
  // Describes the process of loading
  // and defining variables required for the page.
  // TODO: Implement the variable loading process.

  // Describes the structure of the page.
  // TODO: Implement the view.
  return UniversalScaffold(
    appBar: UniversalAppBar(title: Text("Title"), backgroundColor: theme.color.secondary),
    body: UniversalColumn(
      crossAxisAlignment: CrossAxisAlignment.start,
      children:[
        Center(child: CircleAvatar(backgroundImage: theme.asset.userIcon.provider)),
        Text("User Name", style: theme.text.displayMedium)
      ]
    )
  );
}
```

その他、下記の機能について詳しく知りたいときはパッケージの詳細ページをご覧ください。

- テーマ拡張
- グラデーション
- 変換用メソッド

https://pub.dev/packages/katana_theme

## フォーム構築

### フォームコントローラーの取得

まず、フォームの値を制御・保持するためのフォームコントローラーを取得します。

プロフィールデータの編集など`データモデル`を対象にしたフォームを作成する場合、既存のデータモデルに定義される`form`を利用します。

```dart
/// Query for form value.
///
/// ```dart
/// ref.app.form(TestModel.form(LogModel()));    // Get the form controller in app scope.
/// ref.page.form(TestModel.form(LogModel()));    // Get the form controller in page scope.
/// ```
static const form = _$TestModelFormQuery();
```

`form`はコントローラーと同じ様にappスコープ（`ref.app.form`）とpageスコープ（`ref.page.form`）に渡しますが、その際に元となるオブジェクト（`TestModel`）を引数に渡す必要があります。

新規データ登録時は`TestModel`を作成して渡せばよく、既存データの編集時には`データモデル`から読み込んだ値をそのまま渡せばよいです。

```dart
// 新規データ作成時
final memo = const MemoModel(title: "", text: "");
final formController = ref.page.form(MemoModel.form( memo ));

// 既存データ作成時
final memo = ref.app.model(MemoModel.document("Memo ID"))..load();
final formController = ref.page.form(MemoModel.form( memo ));
```

ログインなどデータモデルで定義されていないデータのフォームを作成する場合、フォーム用のデータ定義を下記コマンドで作成します。

```bash
katana code value [Value name]
```

`lib/models/(Value name).dart`に下記のファイルが作成されます。

内部では`(Value name)Value`というクラスが作成されます。

```dart
// login.dart

// ignore: unused_import, unnecessary_import
import 'package:flutter/material.dart';
// ignore: unused_import, unnecessary_import
import 'package:masamune/masamune.dart';

// ignore: unused_import, unnecessary_import
import '/main.dart';

import 'package:freezed_annotation/freezed_annotation.dart';

part 'login.g.dart';
part 'login.m.dart';
part 'login.freezed.dart';

/// Immutable value.
@freezed
@formValue
@immutable
abstract class LoginValue with _$LoginValue {
  const factory LoginValue({
    // TODO: Set the data schema.

  }) = _LoginValue;
  const LoginValue._();

  factory LoginValue.fromJson(Map<String, Object?> json) =>
      _$LoginValueFromJson(json);

  /// Query for form value.
  ///
  /// ```dart
  /// ref.app.form(LoginValue.form(LogModel()));    // Get the form controller in app scope.
  /// ref.page.form(LoginValue.form(LogModel()));    // Get the form controller in page scope.
  /// ```
  static const form = _$LoginValueFormQuery();
}
```

factoryメソッド内に必要なデータスキームを追記してください。

```dart
const factory LoginValue({
  // TODO: Set the data schema.
  required String email,
  required String password,
}) = _LoginValue;
```

フォームコントローラーの取得には、同じ用にこのオブジェクトで定義されている`form`を利用してください。

```dart
final login = const LoginValue(email: "", password: "");
final formController = ref.app.form(LoginValue.form( login ));
```

### フォームの描画と検証・確定

フォーム用の各ウィジェットの`form`パラメーターに上記のフォームコントローラーを渡してください。

またその際、`onSaved`のパラメーターに渡された対象の値でフォームの値を書き換えて返す処理を記述してください。

```dart
FormTextField(
  form: formController,
  onSaved: (value) => formController.value.copyWith(email: value),
),
```

上記の処理を含めながらフォームウィジェットの記述を行った後、確定ボタンを押したときに`formController.validate`を実行することでフォームの値の検証と確定が行われます。

その後検証に通った後、返ってきた値で保存処理等を行ってください。

```dart
FormButton(
  "Login",
  onPressed: () async {
    final LoginValue loginValue = formController.validate(); // 検証しフォームの値を取得
    if (loginValue == null) {
      return;
    }
    try {
      // 正常処理
    } catch (e) {
      // エラー処理
    }
  },
),
```

その他、詳しく知りたいときはパッケージの詳細ページをご覧ください。

https://pub.dev/packages/katana_form

## UIサポート

### ダイアログ

ダイアログを下記のコードで表示可能です。

```dart
// Alert dialog.
Modal.alert(
  title: "Title",
  text: "Contents text",
  submitText: "OK",
  onSubmit: () {
    // Processing when the OK button is pressed
  },
);

// Confirmation dialog.
Modal.confirm(
  title: "Title",
  text: "Contents text",
  submitText: "Yes",
  cancelText: "No",
  onSubmit: () {
    // Processing when the Yes button is pressed
  },
  onCancel: () {
    // Processing when the No button is pressed
  }
);
```

## レスポンシブレイアウト

UniversalUIを用いることで意識することなくPCやモバイルなど画面サイズや向きがことなるプラットフォーム間でUIを作成することができます。。

```dart
@override
Widget build(BuildContext context, PageRef ref) {
  return UniversalScaffold(
    breakpoint: Breakpoint.sm,
    sideBar: UniversalSideBar(
      decoration: const BoxDecoration(
        border: Border(right: BorderSide(color: Colors.grey)),
      ),
      children: [
        for (var i = 0; i < 100; i++)
          ListTile(
            tileColor: Colors.blue,
            title: Text((i + 1).toString()),
          ),
      ],
    ),
    appBar: UniversalSliverAppBar(
      title: const Text("UniversalViewPage"),
      subtitle: const Text("UniversalViewPage"),
      titlePosition: UniversalAppBarTitlePosition.bottom,
      background: UniversalAppBarBackground(theme.asset.image.provider),
    ),
    body: UniversalListView(
      onRefresh: () {
        return Future.delayed(1.s);
      },
      padding: const EdgeInsets.only(top: 32),
      children: [
        UniversalColumn(
          children: [
            ...List.generate(100, (i) {
              return ListTile(
                tileColor: Colors.red,
                title: Text((i + 1).toString()),
              );
            }).mapResponsive(sm: 6, md: 4),
          ],
        ),
      ],
    ),
  );
}
```

その他、詳しく知りたいときはパッケージの詳細ページをご覧ください。

https://pub.dev/packages/katana_ui

https://pub.dev/packages/masamune_universal_ui

## 認証

ユーザー登録や認証を行う場合はmain.dartにある`appAuth`オブジェクトを利用します。

appAuthの各種メソッドを実行することでユーザー登録やログイン、ログアウトが可能です。

```dart
// ユーザー登録
await appAuth.register(
  EmailAndPasswordAuthQuery.register(
    email: "test@email.com",
    password: "12345678",
  ),
);

// ログイン
await appAuth.signIn(
  EmailAndPasswordAuthQuery.signIn(
    email: "test@email.com",
    password: "12345678",
  ),
);

// ログアウト
await appAuth.signOut();
```

初期状態だとこれらはアプリのメモリ上に保存されるのみでアプリを再起動すると元に戻ってしまいます。

データを永続化したい場合は後述の`Firebase/Firestoreサポート`を参照してください。

その他、詳しく知りたいときはパッケージの詳細ページをご覧ください。

https://pub.dev/packages/katana_auth

## ファイルストレージ

画像ファイルのアップロードなどを行いたい場合は`Storage`オブジェクトを利用します。

ファイルピッカーなどを利用してアップロードするファイルの`ファイルパス`や`バイトデータ（UInt8List）`を取得して各種メソッドに渡してください。

```dart
final storage = Storage(const StorageQuery("test/file"));

final pickedData = await FilePicker.platform.pickFiles();
storage.upload(pickedData.first.path);
```

初期状態だとこれらはアプリのメモリ上に保存されるのみでアプリを再起動すると元に戻ってしまいます。

データを永続化したい場合は後述の`Firebase/Firestoreサポート`を参照してください。

その他、詳しく知りたいときはパッケージの詳細ページをご覧ください。

https://pub.dev/packages/katana_storage

## Firebase/Firestoreサポート

`データモデル（データベース）`、`認証`、`ファイルストレージ`に関してはデフォルトだとアプリ内のみに保存する機能を提供しますが、アダプターを置き換えることで端末ローカル内やFirebaseを対象としたものに切り替えることが可能です。

Firebase/Firestore用のアダプターを利用する場合は下記のパッケージを予めインポートしておきます。

```bash
# If you want to use Firebase Authentication
flutter pub add katana_auth_firebase

# If you want to use Firestore
flutter pub add katana_model_firestore

# If you want to use Cloud Storage for Firebase
flutter pub add katana_storage_firebase
```

また、[FlutterFire](https://firebase.flutter.dev/)等を利用して、Firebaseの初期設定を完了してください。

```bash
flutterfire configure
```

データモデル（データベース）、認証、ファイルストレージをそれぞれ`Firestore`、`Firebase Authentication`、`Cloud Storage for Firebase`に切り替えるには、下記のアダプターを対応するものに置き換える必要があります。

```dart
/// App Model.
///
/// By replacing this with another adapter, the data storage location can be changed.
// TODO: Change the database.
// final modelAdapter = RuntimeModelAdapter();
final modelAdapter = FirestoreModelAdapter(options: DefaultFirebaseOptions.currentPlatform);

/// App Auth.
/// 
/// Changing to another adapter allows you to change to another authentication mechanism.
// TODO: Change the authentication.
// final authAdapter = RuntimeAuthAdapter();
final authAdapter = FirebaseAuthAdapter(options: DefaultFirebaseOptions.currentPlatform);

/// App Storage.
/// 
/// Changing to another adapter allows you to change to another storage mechanism.
// TODO: Change the storage.
// final storageAdapter = LocalStorageAdapter();
final storageAdapter = FirebaseStorageAdapter(options: DefaultFirebaseOptions.currentPlatform);
```

**アダプターを変えるのみ、他のコードを気にすることなくFirebaseへ変更することが可能です。**

## その他の機能

Masamuneフレームワークにはその他の便利な機能が用意されています。

それぞれ別パッケージで提供されているものなので詳しくはそちらをご覧ください。

### 短縮記法の提供

https://pub.dev/packages/katana_shorten

### Futureを待つ間のインジケーター表示

https://pub.dev/packages/katana_indicator

# おわりに

自分で使う用途で作ったものですが実装の思想的に合ってそうならぜひぜひ使ってみてください！

また、[こちら](https://github.com/mathrunet/flutter_masamune)にソースを公開しているのでissueやPullRequestをお待ちしてます！

実際にアプリを作りながらMasamuneフレームワークの使い方を学べる記事を投稿しました！

モックデータを作りながらアプリを作成したり、Firebaseとの連携やその他プラグインとの連携など様々なことを行っています。是非ご参照ください！

https://qiita.com/mathru/items/f66cfb7e52503de1d811

また仕事の依頼等ございましたら、私の[Twitter](https://twitter.com/mathru)や[Webサイト](https://mathru.net)で直接ご連絡をお願いいたします！

https://mathru.net/ja/contact

# GitHub Sponsors

スポンサーを随時募集してます。ご支援お待ちしております！

https://github.com/sponsors/mathrunet