こんにちは。広瀬マサルです。
Formの実装のベストプラクティスがわかりません。
新しくデータを作るときやデータを更新するときなど効率的で簡単なフォームの実装方法に戸惑っていました。
なので、
フォームの実装を簡易的に定義するパッケージ
を作りました。
フォームのデータの取り扱いだけでなく、デザインも合わせて取り扱うことが可能です。
使い方をまとめたので興味ある方はぜひ使ってみてください!
katana_form
はじめに
Formの実装はアプリにとってとても重要な要素です。
アプリにユーザーの情報を入力させるインターフェースとして欠かせないものになっています。
Formの実装をシンプルにすることはアプリの実装速度や安全性を高めることに非常に役立ちます。
FlutterではForm
やTextFormField
などのFormField
系のWidgetが用意されています。
しかし、データの取扱には触れておらず、データの取得や保存を行うには各々の状態管理システムに対応した実装を行う必要があります。
また、InputDecoration
でのデザインを変更できるようになっていますが、設定項目が多いためButtonStyle
のように簡略化して利用したいです。
そのため下記のようなパッケージを作成しました。
-
FormController
にフォームに利用するための値を保存しそれを渡すことによりフォームを利用した入出力を可能にする -
すべてのフォームWidgetで
FormStyle
を利用可能にし、デザインの指定を統一。デザインを簡単に統一することが可能。
下記のように簡単に書けます。
final form = FormController(<String, dynamic>{});
return Scaffold(
appBar: AppBar(title: const Text("App Demo")),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 32),
children: [
const FormLabel("Name"),
FormTextField(
form: form,
initialValue: form.value["name"],
onSaved: (value) => {...form.value, "name": value},
),
const FormLabel("Description"),
FormTextField(
form: form,
minLines: 5,
initialValue: form.value["description"],
onSaved: (value) => {...form.value, "description": value},
),
FormButton(
"Submit",
icon: Icon(Icons.check),
onPressed: () {
final value = form.validate(); // Validate and get form values
if (value == null) {
return;
}
print(value);
// Save value as is.
},
),
]
)
);
またこのパッケージはfreezedを使うとにより安全にコードを書くことができます。
インストール
下記のパッケージをインポートします。
flutter pub add katana_form
実装
コントローラー作成
まずFormController
に初期値を入れて定義します。
新規データ作成の場合は空のオブジェクトを渡し、既存データの場合はデータベースから読み込んだ値を入れてください。
この例ではMap<String, dynamic>
でデータベース用のデータの取扱をしている場合のものです。
// 新規データ
final form = FormController(<String, dynamic>{});
// 既存データ
final Map<String, dynamic> data = getRepositoryData();
final form = FormController(data);
これをStatefulWidget
などの状態管理の仕組みで保持します。
FormControllerはChangeNotifier
を継承しているので、riverpodのChangeNotifierProvider
などと合わせて利用することも可能です。
フォーム実装
Form
Widgetの設置は必要ありません。
作成したFormController
を渡すだけです。FormController
を渡す場合はonSaved
も一緒に渡す必要があります。(onChangedのみを利用したい場合はFormController
を渡す必要はありません)
initialValue
に初期値を渡してください。初期値を渡す場合FormController.value
から取得した値をそのまま渡してください。
onSaved
は現在入力されている値がコールバックとして渡されるので変更したFormController.value
の値をそのまま返すようにしてください。
FormTextField(
form: form,
initialValue: form.value["description"],
onSaved: (value) => {...form.value, "description": value},
),
フォームのバリデーションと保存
FormController.validate
を実行することでフォームのバリデーションと保存を行うことができます。
まずバリデーションを行ない、失敗した場合はnull
が返されます。
成功した場合は各Form
WidgetのonSaved
で変更された値が返ってきます。
その値を元にデータベースの更新を行ってください。
final value = form.validate(); // Validate and get form values
if (value == null) {
return;
}
print(value);
// Save value as is.
サンプルコード
上記の一連の流れをまとめて書くと下記のようになります。
フォームのページを破棄する時FormController
もdispose
で一緒に破棄してください。
class FormPage extends StatefulWidget {
const FormPage({super.key});
@override
State<StatefulWidget> createState() => FormPageState();
}
class FormPageState extends State<FormPage> {
final form = FormController(<String, dynamic>{});
@override
void dispose() {
super.dispose();
form.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("App Demo")),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 32),
children: [
const FormLabel("Name"),
FormTextField(
form: form,
initialValue: form.value["name"],
onSaved: (value) => {...form.value, "name": value},
),
const FormLabel("Description"),
FormTextField(
form: form,
minLines: 5,
initialValue: form.value["description"],
onSaved: (value) => {...form.value, "description": value},
),
const SizedBox(height: 16),
FormButton(
"Submit",
icon: Icon(Icons.add),
onPressed: () {
final value = form.validate(); // Validate and get form values
if (value == null) {
return;
}
print(value);
// Save value as is.
},
),
],
),
);
}
}
freezedを使うとにより安全にコードを書くことができます。
@freezed
class FormValue with _$FormValue {
const factory FormValue({
String? name,
String? description,
}) = _FormValue;
}
class FormPage extends StatefulWidget {
const FormPage({super.key});
@override
State<StatefulWidget> createState() => FormPageState();
}
class FormPageState extends State<FormPage> {
final form = FormController(FormValue());
@override
void dispose() {
super.dispose();
form.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("App Demo")),
body: ListView(
padding: const EdgeInsets.symmetric(vertical: 32),
children: [
const FormLabel("Name"),
FormTextField(
form: form,
initialValue: form.value.name,
onSaved: (value) => form.value.copyWith(name: value),
),
const FormLabel("Description"),
FormTextField(
form: form,
minLines: 5,
initialValue: form.value.description,
onSaved: (value) => form.value.copyWith(description: value),
),
const SizedBox(height: 16),
FormButton(
"Submit",
icon: Icon(Icons.add),
onPressed: () {
final value = form.validate(); // Validate and get form values
if (value == null) {
return;
}
print(value);
// Save value as is.
},
),
],
),
);
}
}
スタイルの変更
各Form
WidgetのスタイルをFormStyle
でまとめて変更することが可能です。
デフォルトだとプレーンなスタイルになっていますが、下記のように指定するとMaterialデザインの枠付きのスタイルに変更されます。
FormTextField(
form: form,
initialValue: form.value["name"],
onSaved: (value) => {...form.value, "name": value},
style: FormStyle(
border: OutlineInputBorder(),
padding: const EdgeInsets.symmetric(horizontal: 16),
contentPadding: const EdgeInsets.all(16)),
),
FormWidgetの種類
現在利用可能なFormWidgetは以下です。
随時追加中。
-
FormTextField
- テキストを入力するためのフィールド
-
FormDateTimeField
- 日付と時間をFlutterのダイアログで選択・入力するフィールド
-
FormDateField
- 日付(月日)を選択肢から選択するフィールド
-
FormNumField
- 数値を選択肢から選択するフィールド。
-
FormEnumField
- Enumの定義から選択するフィールド。
-
FormMapField
- Mapを渡してその選択肢から選択するフィールド。
またFormを補助するWidgetは以下です。
-
FormLabel
- フォームのラベル部分を別途表示します。Dividerとしての役目もあります。
-
FormButton
- フォーム用の確定やキャンセルボタンに利用します。
- FormStyleを利用可能。
おわりに
自分で使う用途で作ったものですが実装の思想的に合ってそうならぜひぜひ使ってみてください!
また、こちらにソースを公開しているのでissueやPullRequestをお待ちしてます!
また仕事の依頼等ございましたら、私のTwitterやWebサイトで直接ご連絡をお願いいたします!
GitHub Sponsors
スポンサーを随時募集してます。ご支援お待ちしております!