簡単に複雑なフォームを作れるFlash Formについて紹介する
はじめに
ひっそりとFlutterでフォームを簡単に作成するためのパッケージであるFlashFormを開発しており、OSSとして公開していました。
ドキュメント等を書くのが億劫だったので、ずっと放置していたのですが、ChatGPTのo1がリリースされたので、o1にやらせたら楽に書けるんじゃないかということで、まずはFlash Formの紹介記事を書いてみようと思いました。
フォーム関連のパッケージとしては、Reactive Formsなどが有名で、私自身もそれらを使用していました。しかし、管理画面のような単純なフォームが多数ある場合、UI側のウィジェットを一つ一つ手動で書くのは面倒に感じていました。そこで、より素早く簡単にフォームを作成できるものを作りたいと考え、FlashFormというパッケージを作成しました。
できること
FlashFormは、以下のような特徴を持つフォーム作成パッケージです。
- 自動的なフォーム生成: モデルの構造を定義するだけで、対応するフォームを自動生成します。これにより、手動でウィジェットを作成する手間を省くことができます。
- カスタマイズ性: 各フィールドに対して、バリデーション、ラベル、デコレーター、入力形式などを柔軟に設定できます。
- ネストされたフォームのサポート: 複雑な入れ子構造を持つモデルでも、それに対応したフォームを簡単に作成できます。
- リストや動的フィールドのサポート: リスト形式のフィールドや、動的に追加・削除可能なフィールドをサポートしています。
- データモデルとの連携: フォームの入力値をデータモデルに変換したり、既存のデータモデルからフォームを初期化することができます。
デモ
今回の記事で紹介する使い方に関するコードのデモを作成したみたので、こちらをチェックしてみてください。
使い方
FlashFormの使い方は次のとおりです。
- フォームのモデルである
ModelSchema
を定義する。 -
FlashModelField
ウィジェットにModelSchema
のインスタンスを渡す。
これだけでフォームを作成することができます。それぞれのステップについて、実際のコードを使って詳しく説明していきます。
1. ModelSchemaを定義する
まず、フォーム専用のモデルクラスであるModelSchema
を定義します。このModelSchema
にフォームに必要な設定を書き込むことで、フォームを作成できます。
Personクラスの定義
今回は、次のようなPerson
というクラスに関するフォームを作成します。
class Person {
String name;
int age;
Person({
required this.name,
required this.age,
});
}
Person
クラスは、名前と年齢を持つシンプルなデータモデルです。
PersonSchemaの定義
Person
クラスのModelSchema
として、PersonSchema
を作成します。
class PersonSchema extends ModelSchema<Person> {
PersonSchema() : super(parent: null);
// フィールドの定義は後述
}
PersonSchema
はModelSchema
を継承して作成します。ModelSchema
のジェネリックパラメータには、相互に変換したいデータモデルのクラスを指定します。今回はPerson
クラスのフォームを作成したいので、Person
を指定しています。
Note:
ModelSchema
は木構造になっており、parent
は親のSchema
を指定します。PersonSchema
はトップレベルのため、parent
にはnull
を渡します。
名前フィールドの追加
次に、名前入力用のフィールドを定義します。
class PersonSchema extends ModelSchema<Person> {
PersonSchema() : super(parent: null);
late final nameField = ValueSchema<String, String>(
fieldFormat: TextFieldFormat(),
decorators: [
const DefaultValueDecorator(label: '名前'),
],
validators: [
RequiredValidator(),
],
value: null,
parent: this,
);
// 年齢フィールドの定義は後述
}
nameField
はValueSchema
として定義します。ValueSchema
は単一の値を持つフィールドを表現するクラスです。
-
fieldFormat
: フィールドの入力形式を指定します。ここではTextFieldFormat()
を指定し、テキスト入力フィールドであることを示します。 -
decorators
: フィールドのラベルや装飾を指定します。DefaultValueDecorator
を使って、ラベルを「名前」に設定しています。 -
validators
: フィールドのバリデーションルールを指定します。RequiredValidator()
を追加し、必須入力フィールドであることを示しています。 -
value
: フィールドの初期値を指定します。ここではnull
としています。 -
parent
: 親のSchema
を指定します。ここではthis
(PersonSchema
のインスタンス)を指定します。
年齢フィールドの追加
同様に、年齢入力用のフィールドを定義します。
late final ageField = ValueSchema<num, String>(
fieldFormat: NumberFieldFormat(),
decorators: [
const DefaultValueDecorator(label: '年齢'),
],
validators: [
RequiredValidator(),
RangeValidator(min: 0, max: 120),
],
value: null,
parent: this,
);
-
NumberFieldFormat()
を指定し、数値入力フィールドであることを示します。 - バリデーションとして
RangeValidator
を追加し、0から120の範囲に制限しています。
フィールドリストの定義
ModelSchema
では、fields
プロパティをオーバーライドして、含まれるフィールドのリストを返します。
このfields
に設定されたものが、フォームとして表示されます。
List<FieldSchema> get fields => [
nameField,
ageField,
];
データモデルとの変換
ModelSchema
では、フォームの値とデータモデルとの相互変換を行うために、fromModel
とtoModel
メソッドを実装します。
この変換を定義しておくことで、Personモデルの更新をしたい場合などに、既存のデータからフォームを初期化することができます。
void fromModel(Person model) {
nameField.updateValue(model.name);
ageField.updateValue(model.age);
}
Person toModel() {
return Person(
name: nameField.value ?? '',
age: ageField.value?.toInt() ?? 0,
);
}
-
fromModel
: 既存のPerson
オブジェクトから、フォームのフィールドを初期化します。 -
toModel
: フォームの入力値から、Person
オブジェクトを生成します。
2. FlashModelFieldウィジェットにModelSchemaを渡す
次に、定義したPersonSchema
を使ってフォームを表示します。
メインのウィジェットでフォームを表示
重要なのはFlashModelField(form: form)
で定義したフォームをFlashModelField
に渡すだけでWidgetを構築することができます。
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final form = PersonSchema();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Person Form'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
FlashModelField(form: form),
Padding(
padding: const EdgeInsets.all(32.0),
child: ElevatedButton(
onPressed: () {
if (form.validate()) {
final person = form.toModel();
// personオブジェクトを使って何かを行う
}
},
child: const Text('送信'),
),
),
],
),
),
);
}
}
-
FlashModelField
:ModelSchema
を受け取り、それに対応するフォームを自動的に生成するウィジェットです。 -
form.validate()
: フォームのバリデーションを行います。 -
form.toModel()
: フォームの入力値からデータモデルを生成します。
これで、名前と年齢を入力できるシンプルなフォームが完成しました。
応用編: 複雑なフィールドの追加
FlashFormは、リスト形式のフィールドやネストされたモデルもサポートしています。ここでは、いくつかの応用的なフィールドを追加してみます。
スキルの複数選択フィールドを追加
Person
クラスにスキルのリストを追加し、それに対応するフォームフィールドを作成します。
スキルは得意な技術のことで、チェックボックスから選択する仕様です。
skillsフィールドの定義
チェックボックスから複数の値を選択して、リストを返す場合、MultiSelectFieldFormat.checkbox
を使います。
late final skillsField = ValueSchema<List<String>, List<String>>(
fieldFormat: MultiSelectFieldFormat.checkbox(
options: ['Dart', 'Flutter', 'Java', 'Kotlin'],
toDisplay: (value) => value,
),
decorators: [
const DefaultValueDecorator(label: 'スキル'),
],
value: [],
parent: this,
);
-
MultiSelectFieldFormat.checkbox
: 複数選択可能なチェックボックス形式の入力フィールドを定義します。 -
options
: 選択肢を指定します。
フィールドリストへの追加とデータモデルとの変換の箇所も更新する必要があります。
趣味のリストフィールドを追加
次にユーザーが複数の趣味を入力できるように、リスト形式のフィールドを追加します。
趣味は、自由入力形式でテキストフィールドを、自由に追加したり削除したりすることができるような仕様です。
hobbiesフィールドの定義
この場合、ValueSchemaではなくListSchemaを利用します。
ListSchemaはリスト形式のフィールドを追加したり削除したりするフォーム専用のSchemaになります。
特徴としてはchildFactoryという、リスト形式のフィールドの要素として、どんなフォームを使うかを指定するための関数の設定が必要ということです。
late final hobbiesField = ListSchema<String, String>(
children: [],
decorators: [
const DefaultListDecorator(label: '趣味'),
],
childFactory: (value, parent) {
return ValueSchema<String, String>(
fieldFormat: TextFieldFormat(),
value: value,
parent: parent,
);
},
parent: this,
);
-
ListSchema
: リスト形式のフィールドを定義します。 -
childFactory
: リスト内の各アイテムのフィールドを生成するための関数を指定します。
まとめ
これで、FlashFormを使って複雑なフォームも簡単に作成できることがわかりました。ModelSchema
を定義し、それをFlashModelField
ウィジェットに渡すだけで、対応するフォームが自動的に生成されます。
さらに、リスト形式のフィールドやネストされたモデル、複数選択フィールドなど、さまざまな入力形式にも対応しています。まだまだ、入力形式が少ないのでこれから充実させていく予定です。もしも追加して欲しい、形式のフォームがあれば教えてください。
詳細や他の機能については、GitHubリポジトリを参照してください。これから、ドキュメント等を追加していこうとおもいます。
今後もFlashFormの機能拡張や改善を行っていく予定です。ご意見やフィードバックがありましたら、ぜひお寄せください。
サンプルの全体的なコード
最後に、今回使用したサンプルコードの全体を掲載します。
モデルクラス
class Person {
String name;
int age;
List<String> skills;
List<String> hobbies;
Person({
required this.name,
required this.age,
required this.skills,
required this.hobbies,
});
}
フォームスキーマ
class PersonSchema extends ModelSchema<Person> {
PersonSchema() : super(parent: null);
late final nameField = ValueSchema<String, String>(
fieldFormat: TextFieldFormat(),
decorators: [
const DefaultValueDecorator(label: '名前'),
],
validators: [
RequiredValidator(),
],
value: null,
parent: this,
);
late final ageField = ValueSchema<num, String>(
fieldFormat: NumberFieldFormat(),
decorators: [
const DefaultValueDecorator(label: '年齢'),
],
validators: [
RequiredValidator(),
RangeValidator(min: 0, max: 120),
],
value: null,
parent: this,
);
late final skillsField = ValueSchema<List<String>, List<String>>(
fieldFormat: MultiSelectFieldFormat.checkbox(
options: ['Dart', 'Flutter', 'Java', 'Kotlin'],
toDisplay: (value) => value,
),
decorators: [
const DefaultValueDecorator(label: 'スキル'),
],
value: [],
parent: this,
);
late final hobbiesField = ListSchema<String, String>(
children: [],
decorators: [
const DefaultListDecorator(label: '趣味'),
],
childFactory: (value, parent) {
return ValueSchema<String, String>(
fieldFormat: TextFieldFormat(),
value: value,
parent: parent,
);
},
parent: this,
);
List<FieldSchema> get fields => [
nameField,
ageField,
skillsField,
hobbiesField,
];
void fromModel(Person model) {
nameField.updateValue(model.name);
ageField.updateValue(model.age);
skillsField.updateValue(model.skills);
hobbiesField.updateValue(model.hobbies);
}
Person toModel() {
return Person(
name: nameField.value ?? '',
age: ageField.value?.toInt() ?? 0,
skills: skillsField.value ?? [],
hobbies: hobbiesField.value ?? [],
);
}
}
メインファイル
import 'package:flutter/material.dart';
import 'package:flash_form/flash_form.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// アプリケーションのルートウィジェット
Widget build(BuildContext context) {
return MaterialApp(
title: 'FlashForm Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final form = PersonSchema();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Person Form'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
FlashModelField(form: form),
Padding(
padding: const EdgeInsets.all(32.0),
child: ElevatedButton(
onPressed: () {
if (form.validate()) {
final person = form.toModel();
// personオブジェクトを使って何かを行う
print('Name: ${person.name}');
print('Age: ${person.age}');
print('Skills: ${person.skills}');
print('Hobbies: ${person.hobbies}');
}
},
child: const Text('送信'),
),
),
],
),
),
);
}
}
以上で、FlashFormを使ったフォームの作成方法についての解説を終わります。
Discussion