【Flutter】Formウィジェットの仕組みとAutoValidationMode の罠 〜サンプルコードを添えて〜

はじめに

こんにちは、株式会社メンバーズ Cross ApplicationOpen in new tab カンパニーの田原です。

Flutter でフォームを扱う際に便利なのが Form ウィジェットです。フォームは、ユーザー入力を受け付け、バリデーションを行い、状態を管理するための重要なコンポーネントです。Form ウィジェットを使用することで、複数の入力フィールドを統合的に管理し、バリデーションや状態のリセットを容易に行うことができます。

本記事では、Flutter の Form ウィジェットの詳細と AutoValidationMode の動作について解説します。公式の Form の実装コードと、用意したサンプルコードをもとに、その構造や動作を深掘りし、より効果的なフォーム管理の方法を学びます。

  • Form ウィジェット と FormState の基本的な役割
  • FormField ウィジェット と FormState の仕組みとバリデーション処理
  • AutoValidationMode の動作パターン

これらを学び、より柔軟な Form バリデーションの実装を身につけましょう!

▼▼▼ Form の公式ドキュメント ▼▼▼

▼▼▼ Form のソースコード ▼▼▼

Form ウィジェットの基本

Form ウィジェットは、Flutterの公式リポジトリの widgets/form.dartOpen in new tab で定義されています。

Form は StatefulWidget として実装されており、その状態を管理する FormState クラスと連携して動作します。フォーム内のフィールドは FormField ウィジェットを介して登録され、バリデーションや状態変更の通知が可能になります。

Form の役割

Form ウィジェットは、複数のフォームフィールド(TextFormField、FormField など)を統合的に管理するためのコンテナです。FormState を通じて、バリデーションや状態のリセット、入力内容の保存などを一括で制御できます。

参考:Form classOpen in new tab

基本的な使い方

以下は、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 ウィジェットにおけるバリデーション処理は、以下の流れで行われます。

  1. 送信ボタンが押される (_submit メソッドの呼び出し)
    • ユーザーが ElevatedButton を押すと、_submit メソッドが呼び出されます。
  2. validate() メソッドの実行
    • _formKey.currentState!.validate() を呼び出すと、Form に含まれるすべての FormField(この場合は TextFormField)の validator が順番に実行されます。
    • 各 FormField の validator は、入力値をチェックし、エラーがあればエラーメッセージを返し、問題がなければ null を返します。
  3. バリデーション結果の処理
    • validate() の戻り値は bool で、
      • すべての FormField が null を返せば true になり、フォームは有効と判断されます。
      • どれか1つでもエラーがあれば false になり、フォームは無効と判断されます。
    • false の場合、Flutter は TextFormField に設定されたエラーメッセージを UI に表示します。
  4. save() メソッドの実行(バリデーション成功時のみ)
    • validate() が true の場合、_formKey.currentState!.save() を呼び出し、各 FormField の onSaved コールバックを実行します。
    • onSaved コールバック内では、入力値を _email などの変数に保存する処理を行います。
  5. データの処理
    • save() 実行後、データが _email に格納されるので、それを利用してフォームデータを処理(実装例ではコンソール出力)します。

FormState の役割

FormState は、Form ウィジェットの状態を管理するクラスです。フォーム全体の状態を取得したり、バリデーションを実行したり、データを保存・リセットするために利用されます。

参考:FormState classOpen in new tab

基本的な使い方

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<T> classOpen in new tab

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);
  • validator
    • 入力値を検証する関数。エラーがあればエラーメッセージを返す。
      • typedef FormFieldValidator<T> = String? Function(T? value);
  • onSaved
    • フォーム送信時に、入力値を保存する処理を実行する関数。
      • typedef FormFieldSetter<T> = void Function(T? newValue);
  • initialValue
    • 初期値を設定する。
  • autovalidateMode
    • 自動バリデーションの動作を制御する。
  • forceErrorText
    • 明示的にエラーメッセージを設定。

FormFieldStateの内部処理

各 FormField は、その状態を FormFieldState によって管理します。

参考:FormFieldState<T> classOpen in new tab

その実装の詳細は以下のようになっています。

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 の値の変更や、ユーザー操作に対して自動でバリデーションを実行することができます。

参考:AutovalidateMode enumOpen in new tab

AutoValidationModeの種類

  • disabled
    • このモードは、自動バリデーションが実行されません。FormState の validate() を実行したときのみバリデーションが実行されます。

  • always
    • 入力中でなくても常に自動でバリデーション実行されます。初期値がバリデーション不正値であれば、初期状態でエラーが表示されます。

  • onUserInteraction
    • ユーザーが入力・操作中にのみ、操作対象の FormField は自動でバリデーションを実行し続けます。初期状態ではエラーが表示されず、入力を開始すると自動バリデーションが開始され、不正値であればエラーが表示されます。

  • onUnfocus
    • ユーザーが FormField の操作を行い、その FormField の操作を終えてフォーカスを外したタイミングで、操作対象だった FormField のバリデーションが実行されます。

AutoValidationMode の動作

AutoValidationMode は Form の autovalidateMode プロパティと FormField の autovalidateMode プロパティのそれぞれに設定することができます。

Form にのみ AutoValidationMode を設定した場合

Form 全体に統一した自動バリデーションの設定をしたい場合には、Form にのみ設定することで簡単にすべての FormField に同じ設定を適用することができます。

⚠️注意が必要な動作

  1. Form に onUserInteraction を設定した場合
    • いずれかの FormField に操作を行ったときに、すべての FormField のバリデーションが実行されます。

FormField にのみ AutoValidationMode を設定した場合

FormField 毎に異なる自動バリデーションの仕組みが必要な場合には、FormField 毎に個別の設定をすることで実現できます。Form にはAutoValidationModeを設定せず、FormField のみに設定することで、それぞれのFormField は自身に設定されたモードに基づいて自動バリデーションが実行されます。

Form と FormField の両方に AutoValidationMode を設定した場合

Form と FormField の両方に設定した場合、AutoValidationMode は原則 Form の設定が優先されます。

⚠️注意が必要な動作

  1. Form に disabled を設定した場合
    1. FormField の設定が優先されます。FormField に disabled 以外のモードを設定していた場合、その設定が適用され、自動バリデーションが動作します。
  2. 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 バリデーションの仕組みを実装できます!

最後までお読みいただき、ありがとうございました。

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

田原 葉
田原 葉
2024年にメンバーズに中途で入社。前職はiOSエンジニア。現在はCross ApplicationカンパニーでFlutter技術をメインにモバイルアプリ開発支援を担当。
詳しく見る
ページトップへ戻る