【Flutter】Formウィジェットの仕組みとAutoValidationMode の罠 〜サンプルコードを添えて〜
はじめに
こんにちは、株式会社メンバーズ Cross Application カンパニーの田原です。
Flutter でフォームを扱う際に便利なのが Form ウィジェットです。フォームは、ユーザー入力を受け付け、バリデーションを行い、状態を管理するための重要なコンポーネントです。Form ウィジェットを使用することで、複数の入力フィールドを統合的に管理し、バリデーションや状態のリセットを容易に行うことができます。
本記事では、Flutter の Form ウィジェットの詳細と AutoValidationMode の動作について解説します。公式の Form の実装コードと、用意したサンプルコードをもとに、その構造や動作を深掘りし、より効果的なフォーム管理の方法を学びます。
- Form ウィジェット と FormState の基本的な役割
- FormField ウィジェット と FormState の仕組みとバリデーション処理
- AutoValidationMode の動作パターン
これらを学び、より柔軟な Form バリデーションの実装を身につけましょう!
▼▼▼ Form の公式ドキュメント ▼▼▼
▼▼▼ Form のソースコード ▼▼▼
Form ウィジェットの基本
Form ウィジェットは、Flutterの公式リポジトリの widgets/form.dart で定義されています。
Form は StatefulWidget として実装されており、その状態を管理する FormState クラスと連携して動作します。フォーム内のフィールドは FormField ウィジェットを介して登録され、バリデーションや状態変更の通知が可能になります。
Form の役割
Form ウィジェットは、複数のフォームフィールド(TextFormField、FormField など)を統合的に管理するためのコンテナです。FormState を通じて、バリデーションや状態のリセット、入力内容の保存などを一括で制御できます。
参考:Form class
基本的な使い方
以下は、Form ウィジェットの基本的な使い方の例です。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flutter Form Example')),
body: const Padding(padding: EdgeInsets.all(16.0), child: MyForm()),
),
);
}
}
class MyForm extends StatefulWidget {
const MyForm({super.key});
@override
State<MyForm> createState() => _MyFormState();
}
class _MyFormState extends State<MyForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String _email = '';
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print('Submitted: $_email');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Emailを入力してください';
}
return null;
},
onSaved: (String? value) {
_email = value!;
},
),
const SizedBox(height: 16),
ElevatedButton(onPressed: _submit, child: const Text('送信')),
],
),
);
}
}

この例では、Form ウィジェットを利用して、メールアドレスの入力とバリデーションを行っています。ボタンを押すと、バリデーションが適用され、問題がなければ入力内容が保存されます。
バリデーション処理の流れ
Form ウィジェットにおけるバリデーション処理は、以下の流れで行われます。
- 送信ボタンが押される (_submit メソッドの呼び出し)
- ユーザーが ElevatedButton を押すと、_submit メソッドが呼び出されます。
- validate() メソッドの実行
- _formKey.currentState!.validate() を呼び出すと、Form に含まれるすべての FormField(この場合は TextFormField)の validator が順番に実行されます。
- 各 FormField の validator は、入力値をチェックし、エラーがあればエラーメッセージを返し、問題がなければ null を返します。
- バリデーション結果の処理
- validate() の戻り値は bool で、
- すべての FormField が null を返せば true になり、フォームは有効と判断されます。
- どれか1つでもエラーがあれば false になり、フォームは無効と判断されます。
- false の場合、Flutter は TextFormField に設定されたエラーメッセージを UI に表示します。
- validate() の戻り値は bool で、
- save() メソッドの実行(バリデーション成功時のみ)
- validate() が true の場合、_formKey.currentState!.save() を呼び出し、各 FormField の onSaved コールバックを実行します。
- onSaved コールバック内では、入力値を _email などの変数に保存する処理を行います。
- データの処理
- save() 実行後、データが _email に格納されるので、それを利用してフォームデータを処理(実装例ではコンソール出力)します。
FormState の役割
FormState は、Form ウィジェットの状態を管理するクラスです。フォーム全体の状態を取得したり、バリデーションを実行したり、データを保存・リセットするために利用されます。
基本的な使い方
Form ウィジェットの例に示したコードを元に使い方を解説します。
GlobalKey<FormState> の生成
FormStateは、GlobalKey<FormState> を通じてアクセスします。
class _MyFormState extends State<MyForm> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); // キーを生成・保持
GlobalKey<FormState> を Form ウィジェットに渡す
この _formKey を Form ウィジェットに渡すことで、フォームの状態を管理できます。
@override
Widget build(BuildContext context) {
return Form(
key: _formKey, // Form に生成したキーを渡す
GlobalKey<FormState> を 使用して FormState にアクセスする
この _formKey の currentState プロパティを使用して FormState の validate や save メソッドにアクセスすることができます。
void _submit() {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
print('Submitted: $_email');
}
}
FormStateの主要なメソッド
- save()
- FormField に設定された onSaved コールバックを実行し、入力データを保存します。
- reset()
- すべての FormField の状態をリセットします。
- validate()
- FormField に設定されたバリデーション関数を実行し、フォーム全体のバリデーションを行います。
FormFieldの詳細とバリデーション処理
Form ウィジェットは、FormField とその派生クラス(TextFormField など)を通じて、入力値の管理やバリデーションを行います。FormField は、フォームの個々の入力要素を表し、状態管理やバリデーション機能を提供する重要なコンポーネントです。
FormFieldの基本構造
FormFieldの定義を見てみましょう。
class FormField<T> extends StatefulWidget {
const FormField({
super.key,
required this.builder,
this.onSaved,
this.forceErrorText,
this.validator,
this.initialValue,
this.enabled = true,
AutovalidateMode? autovalidateMode,
this.restorationId,
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
final FormFieldSetter<T>? onSaved;
final String? forceErrorText;
final FormFieldValidator<T>? validator;
final FormFieldBuilder<T> builder;
final T? initialValue;
final bool enabled;
final AutovalidateMode autovalidateMode;
final String? restorationId;
@override
FormFieldState<T> createState() => FormFieldState<T>();
}
主要なプロパティ
- builder
- FormFieldState を受け取り、実際のウィジェットを構築する関数。
- typedef FormFieldBuilder<T> = Widget Function(FormFieldState<T> field);
- FormFieldState を受け取り、実際のウィジェットを構築する関数。
- validator
- 入力値を検証する関数。エラーがあればエラーメッセージを返す。
- typedef FormFieldValidator<T> = String? Function(T? value);
- 入力値を検証する関数。エラーがあればエラーメッセージを返す。
- onSaved
- フォーム送信時に、入力値を保存する処理を実行する関数。
- typedef FormFieldSetter<T> = void Function(T? newValue);
- フォーム送信時に、入力値を保存する処理を実行する関数。
- initialValue
- 初期値を設定する。
- autovalidateMode
- 自動バリデーションの動作を制御する。
- forceErrorText
- 明示的にエラーメッセージを設定。
FormFieldStateの内部処理
各 FormField は、その状態を FormFieldState によって管理します。
その実装の詳細は以下のようになっています。
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
late T? _value = widget.initialValue;
late final RestorableStringN _errorText;
final RestorableBool _hasInteractedByUser = RestorableBool(false);
final FocusNode _focusNode = FocusNode();
T? get value => _value;
String? get errorText => _errorText.value;
bool get hasError => _errorText.value != null;
bool get hasInteractedByUser => _hasInteractedByUser.value;
bool get isValid => widget.forceErrorText == null && widget.validator?.call(_value) == null;
void save() {
widget.onSaved?.call(value);
}
void reset() {
setState(() {
_value = widget.initialValue;
_hasInteractedByUser.value = false;
_errorText.value = null;
});
Form.maybeOf(context)?._fieldDidChange();
}
bool validate() {
setState(() {
_validate();
});
return !hasError;
}
void _validate() {
if (widget.forceErrorText != null) {
_errorText.value = widget.forceErrorText;
return;
}
if (widget.validator != null) {
_errorText.value = widget.validator!(_value);
} else {
_errorText.value = null;
}
}
void didChange(T? value) {
setState(() {
_value = value;
_hasInteractedByUser.value = true;
});
Form.maybeOf(context)?._fieldDidChange();
}
@protected
void setValue(T? value) {
_value = value;
}
@override
String? get restorationId => widget.restorationId;
@protected
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_errorText, 'error_text');
registerForRestoration(_hasInteractedByUser, 'has_interacted_by_user');
}
@protected
@override
void deactivate() {
Form.maybeOf(context)?._unregister(this);
super.deactivate();
}
@protected
@override
void initState() {
super.initState();
_errorText = RestorableStringN(widget.forceErrorText);
}
@protected
@override
void didUpdateWidget(FormField<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.forceErrorText != oldWidget.forceErrorText) {
_errorText.value = widget.forceErrorText;
}
}
@protected
@override
void didChangeDependencies() {
super.didChangeDependencies();
switch (Form.maybeOf(context)?.widget.autovalidateMode) {
case AutovalidateMode.always:
WidgetsBinding.instance.addPostFrameCallback((_) {
// If the form is already validated, don't validate again.
if (widget.enabled && !hasError && !isValid) {
validate();
}
});
case AutovalidateMode.onUnfocus:
case AutovalidateMode.onUserInteraction:
case AutovalidateMode.disabled:
case null:
break;
}
}
@override
void dispose() {
_errorText.dispose();
_focusNode.dispose();
_hasInteractedByUser.dispose();
super.dispose();
}
@protected
@override
Widget build(BuildContext context) {
if (widget.enabled) {
switch (widget.autovalidateMode) {
case AutovalidateMode.always:
_validate();
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser.value) {
_validate();
}
case AutovalidateMode.onUnfocus:
case AutovalidateMode.disabled:
break;
}
}
Form.maybeOf(context)?._register(this);
if (Form.maybeOf(context)?.widget.autovalidateMode == AutovalidateMode.onUnfocus &&
widget.autovalidateMode != AutovalidateMode.always ||
widget.autovalidateMode == AutovalidateMode.onUnfocus) {
return Focus(
canRequestFocus: false,
skipTraversal: true,
onFocusChange: (bool value) {
if (!value) {
setState(() {
_validate();
});
}
},
focusNode: _focusNode,
child: widget.builder(this),
);
}
return widget.builder(this);
}
}
この FormFieldState は、フォームの各フィールドの状態を管理しています。主なメソッドの動作を解説します。
- save()
- FormField の onSave に value を渡して実行します。
- reset()
- value の値に FormField の initialValue をセットし、フォームの状態をリセットします。
- _hasInteractedByUser と errorText の値も初期状態に戻します。
- FormState の _fieldDidChange を呼び出し、値の変更があったことを通知します。
- validate()
- FormField の validator が null でなければ実行し、戻り値を _errorText にセットします。validator が null の場合は、_errorText に null をセットします。
- validate() の結果として、_errorText が non-null であれば true を、null の場合は false を戻り値に返します。
- didChange(T? value)
- _value に受け取った value をセットし、_hasInteractedByUser に true を代入します。
- FormState の _fieldDidChange を呼び出し、値の変更があったことを通知します。
- この時、通知を受け取った FormState は、Form の onChanged を実行します。その後、自身の管理するすべての FormField の _hasInteractedByUser をチェックして、FormState の _hasInteractedByUser の値を更新します。
AutoValidationMode
Form と FormState には AutoValidationMode を指定することができ、FormField の値の変更や、ユーザー操作に対して自動でバリデーションを実行することができます。
AutoValidationModeの種類
- disabled
- このモードは、自動バリデーションが実行されません。FormState の validate() を実行したときのみバリデーションが実行されます。
- always
- 入力中でなくても常に自動でバリデーション実行されます。初期値がバリデーション不正値であれば、初期状態でエラーが表示されます。
- onUserInteraction
- ユーザーが入力・操作中にのみ、操作対象の FormField は自動でバリデーションを実行し続けます。初期状態ではエラーが表示されず、入力を開始すると自動バリデーションが開始され、不正値であればエラーが表示されます。
- onUnfocus
- ユーザーが FormField の操作を行い、その FormField の操作を終えてフォーカスを外したタイミングで、操作対象だった FormField のバリデーションが実行されます。
AutoValidationMode の動作
AutoValidationMode は Form の autovalidateMode プロパティと FormField の autovalidateMode プロパティのそれぞれに設定することができます。
Form にのみ AutoValidationMode を設定した場合
Form 全体に統一した自動バリデーションの設定をしたい場合には、Form にのみ設定することで簡単にすべての FormField に同じ設定を適用することができます。
⚠️注意が必要な動作
- Form に onUserInteraction を設定した場合
- いずれかの FormField に操作を行ったときに、すべての FormField のバリデーションが実行されます。
FormField にのみ AutoValidationMode を設定した場合
FormField 毎に異なる自動バリデーションの仕組みが必要な場合には、FormField 毎に個別の設定をすることで実現できます。Form にはAutoValidationModeを設定せず、FormField のみに設定することで、それぞれのFormField は自身に設定されたモードに基づいて自動バリデーションが実行されます。
Form と FormField の両方に AutoValidationMode を設定した場合
Form と FormField の両方に設定した場合、AutoValidationMode は原則 Form の設定が優先されます。
⚠️注意が必要な動作
- Form に disabled を設定した場合
- FormField の設定が優先されます。FormField に disabled 以外のモードを設定していた場合、その設定が適用され、自動バリデーションが動作します。
- FormField に always を指定した場合
- その FormField は、Form の設定を無視して常に自動バリデーションが実行されるようになります。
AutoValidationMode サンプル
AutoValidationMode の複雑な動作を検証できるサンプル実装を以下に示します。実際にお手元でビルドして動かしてみることで、AutoValidationMode の動作法則の理解が深まると思います。
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('AutoValidation Behavior Demo')),
body: const Padding(
padding: EdgeInsets.all(16.0),
child: AutoValidationBehaviorDemo(),
),
),
),
);
}
class AutoValidationBehaviorDemo extends StatefulWidget {
const AutoValidationBehaviorDemo({super.key});
@override
State<AutoValidationBehaviorDemo> createState() =>
_AutoValidationBehaviorDemoState();
}
class _AutoValidationBehaviorDemoState
extends State<AutoValidationBehaviorDemo> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
bool validationStatus = false;
AutovalidateMode _selectedMode = AutovalidateMode.disabled;
void _validateAll() {
setState(() {
validationStatus = _formKey.currentState!.validate();
});
}
void _reset() {
_formKey.currentState!.reset();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
const Text('Form Validation Mode: '),
DropdownButton<AutovalidateMode>(
value: _selectedMode,
items: const <DropdownMenuItem<AutovalidateMode>>[
DropdownMenuItem<AutovalidateMode>(
value: AutovalidateMode.disabled,
child: Text('disabled'),
),
DropdownMenuItem<AutovalidateMode>(
value: AutovalidateMode.always,
child: Text('always'),
),
DropdownMenuItem<AutovalidateMode>(
value: AutovalidateMode.onUserInteraction,
child: Text('onUserInteraction'),
),
DropdownMenuItem<AutovalidateMode>(
value: AutovalidateMode.onUnfocus,
child: Text('onUnfocus'),
),
],
onChanged: (AutovalidateMode? mode) {
setState(() {
_selectedMode = mode!;
});
},
),
],
),
const SizedBox(height: 20),
Form(
key: _formKey,
autovalidateMode: _selectedMode,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('FormField Validation Mode: disabled'),
TextFormField(
autovalidateMode: AutovalidateMode.disabled,
validator: (String? value) {
if (value == null || value.isEmpty) {
return '必須です';
}
return null;
},
),
const SizedBox(height: 20),
const Text('FormField Validation Mode: always'),
TextFormField(
autovalidateMode: AutovalidateMode.always,
validator: (String? value) {
if (value == null || value.isEmpty) {
return '必須です';
}
return null;
},
),
const SizedBox(height: 20),
const Text('FormField Validation Mode: onUserInteraction'),
TextFormField(
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (String? value) {
if (value == null || value.isEmpty) {
return '必須です';
}
return null;
},
),
const SizedBox(height: 20),
const Text('onUnfocus'),
TextFormField(
autovalidateMode: AutovalidateMode.onUnfocus,
validator: (String? value) {
if (value == null || value.isEmpty) {
return '必須です';
}
return null;
},
),
const SizedBox(height: 40),
Row(
children: <Widget>[
const Text('Validation Status'),
const SizedBox(width: 10),
Icon(validationStatus ? Icons.verified : Icons.error),
],
),
const SizedBox(height: 10),
Row(
children: <Widget>[
ElevatedButton(
onPressed: _validateAll,
child: const Text('Validate All'),
),
const SizedBox(width: 20),
ElevatedButton(
onPressed: _reset,
child: const Text('Reset'),
),
],
),
],
),
),
],
);
}
}

まとめ
以上、Flutter の Form ウィジェットと、 AutoValidationMode の動作について解説しました。
AutoValidationMode の動作の仕組みを理解し、FormField と Form を適切に管理することで、より柔軟で堅牢な Form バリデーションの仕組みを実装できます!
最後までお読みいただき、ありがとうございました。
この記事を書いた人
関連記事
- 【Flutter】VSCodeで効率よく開発するためのおすす...
Nicolas Christopher
Advent Calendar!
Advent Calendar 2024