📲

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

2023/01/26に公開

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

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

コンセプトは

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

です。

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

masamune

https://pub.dev/packages/masamune

https://pub.dev/packages/masamune_builder

はじめに

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

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

※動画のサンプルコードはこちらに公開しています。

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

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

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

  • CLI(コマンドラインインターフェース)ツールによるコードテンプレートの生成
  • build_runner による追加コードの自動生成

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

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

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

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

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

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

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

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

インストール

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

flutter pub global activate katana_cli

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

build_runnerfreezedjson_serializable も合わせてインストールする必要があります。

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を記述してください。

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

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

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

コード変更の監視

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

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

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

katana code watch

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

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

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

katana code generate

実装

ページ

ページの作成

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

katana code page [Page name]

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

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

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

// 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';


// TODO: Set the path for the page.
("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.
  /// ```
  
  static const query = _$TestPageQuery();

  
  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(””)内のパスを指定することでディープリンクのパスを指定できます。

("user/:user_id")

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

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を指定してそのページに遷移することができます。

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

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

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

RouteQueryに対してpushreplaceメソッドが用意されているのでそちらを利用することも可能です。

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

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

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

初期ページの設定

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

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

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

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

katana_router

https://pub.dev/packages/katana_router

データモデル

データ構造

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

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

コレクション(ドキュメント)モデル作成

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

katana code collection [Collection name]

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

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

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

// 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.



// TODO: Set the path for the collection.
("test")
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.model(TestModel.document(id))..load();  // Load the document.
  /// ```
  static const document = _$TestModelDocumentQuery();

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

  /// Query for form value.
  ///
  /// ```dart
  /// ref.form(TestModel.form(TestModel()));    // Get the form controller.
  /// ```
  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;

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

katana code document [Document name]

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

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

(奇数:コレクション、偶数:ドキュメント)

("user")

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

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

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

モデルの利用

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

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

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

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


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.model(TestModel.collection()); // TestModelのコレクションを取得。
  testModelCollection.load();  // モデルデータを読み込み

  ~~~~~~~~~
}

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

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

watchreadメソッドを使うことでメソッドチェーンで簡潔に書くこともできます。

final testModelCollection = TestModel.collection().watch(ref);
final testModelCollection = TestModel.collection().read(appRef);

SharedPreferences機能

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

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

https://zenn.dev/mathru/articles/48e5febda53454

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

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

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

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

  1. こちらのテンプレートからスプレッドシートを自分のGoogleドライブにコピーします。

    • CollectionModelPathを利用する場合はコレクション用のシートを利用し、DocumentModelPathを利用する場合はドキュメント用のシートを利用します。
    • ファイル -> コピーの作成からコピーが可能です。
  2. コピーしたスプレッドシート内でファイル -> 共有 -> 他のユーザーと共有をクリックします。

  3. (作成したスプレッドシート名)を共有ウィンドウ内で、一般的なアクセス**リンクを知っている全員**に変更します。

  4. 作成したクラスに追加する形で「GoogleSpreadSheetDataSource」のアノテーションを追加し、ブラウザで表示されているURL(例:https://docs.google.com/spreadsheets/d/1bfNX8clPH9PFOcfFIStNCGNGjeCKwGv-24iChSJn8yM/edit#gid=0)をコピーし記載します。

    /// Value for model.
    
    
    
    (
      "https://docs.google.com/spreadsheets/d/1bfNX8clPH9PFOcfFIStNCGNGjeCKwGv-24iChSJn8yM/edit#gid=0",
      version: 1,
    )
    // TODO: Set the path for the collection.
    ("test")
    class TestModel with _$TestModel {
      const factory TestModel({
         // TODO: Set the data schema.
         
      }) = _TestModel;
      const TestModel._();
    

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

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

katana_model

https://pub.dev/packages/katana_model

コントローラー

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

katana code controller [Controller name]

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

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

// 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.
(autoDisposeWhenUnreferenced: true)
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で定義した場合は参照されるウィジェットが0になったタイミングでコントローラーが破棄されます。

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


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と同じ)

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

watchreadメソッドを使うことでメソッドチェーンで簡潔に書くこともできます。

final testController = TestController.query().watchOnApp(ref);
final testController = TestController.query().watchOnPage(ref);
final testController = TestController.query().read(appRef);

状態管理

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

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

katana_scoped

https://pub.dev/packages/katana_model

翻訳

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

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

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

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

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

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

Text(l().success);

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

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

katana_localization

https://pub.dev/packages/katana_localization

テーマ管理

アセットの定義

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

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

// pubspec.yaml

flutter:
  assets:
    - assets/images/

フォントの定義

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

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

// 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

// 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.
/// ```

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。

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

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

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

katana_theme

https://pub.dev/packages/katana_theme

フォーム構築

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

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

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

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

formref.formに渡しますが、その際に元となるオブジェクト(TestModel)を引数に渡す必要があります。

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

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

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

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

katana code value [Value name]

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

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

// 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.



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.form(LoginValue.form(LoginValue()));    // Get the form controller.
  /// ```
  static const form = _$LoginValueFormQuery();
}

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

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

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

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

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

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

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

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

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

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

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

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

katana_form

https://pub.dev/packages/katana_form

UIサポート

ダイアログ

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

// 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を作成することができます。。


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),
          ],
        ),
      ],
    ),
  );
}

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

katana_ui

https://pub.dev/packages/katana_ui

masamune_universal_ui

https://pub.dev/packages/masamune_universal_ui

認証

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

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

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

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

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

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

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

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

katana_auth

https://pub.dev/packages/katana_auth

ファイルストレージ

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

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

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

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

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

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

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

katana_storage

https://pub.dev/packages/katana_storage

Firebase/Firestoreサポート

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

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

# 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等を利用して、Firebaseの初期設定を完了してください。

flutterfire configure

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

/// 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フレームワークにはその他の便利な機能が用意されています。

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

短縮記法の提供

katana_shorten

https://pub.dev/packages/katana_shorten

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

katana_indicator

https://pub.dev/packages/katana_indicator

おわりに

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

また、こちらにソースを公開しているのでissueやPullRequestをお待ちしてます!

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

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

https://zenn.dev/mathru/books/d219c9b7cdfd53

また仕事の依頼等ございましたら、私のTwitterWebサイトで直接ご連絡をお願いいたします!

https://mathru.net/ja/contact

GitHub Sponsors

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

https://github.com/sponsors/mathrunet

Discussion