【Flutter】FormにFormFieldでチェックボックスに検証機能

  • 2024年11月1日
  • 2024年11月1日
  • Widget

対象者

  • Flutterでフォームのバリデーションをカスタマイズしたい開発者
  • CheckBoxや他のウィジェットをフォーム内で検証したい人
  • Flutterのフォームフィールドの挙動を深く理解したいエンジニア

はじめに

Flutterでフォームを作成する際、TextFormFieldを使用すればテキスト入力のバリデーションは簡単に行えます。しかし、CheckBoxDropdownButtonなどの他のウィジェットをフォーム内でバリデーションする方法は少し複雑です。本記事では、3つのチェックボックスを例に取り、それぞれ異なるバリデーション方法を実装し、その違いと実装方法を詳しく解説します。また、FormFieldの標準的な使い方や主要なプロパティについても説明します。これにより、フォーム内で多様なウィジェットを扱う際の参考になります。

バリデーションの実行

フォームのバリデーションは、ユーザーが入力を完了した後に検証ボタンを押すことで行われます。以下のコードは、検証ボタンの処理を示しています。この部分は3つのチェックボックスに共通しています。

// 検証ボタンの処理
FilledButton(
  onPressed: () {
    final result =
        _formKey.currentState!.validate() ? '検証成功!' : '検証失敗!';
    setState(() {
      _isValidated = true;
      _label = result;
    });
  },
  child: Text(_label),
),

解説

  • _formKey: GlobalKey<FormState>のインスタンスで、フォーム全体の状態を管理します。

  • _formKey.currentState!.validate(): フォーム内のすべてのFormFieldvalidatorを呼び出し、バリデーションを実行します。すべてのフィールドが有効であればtrueを返します。

  • _isValidated フラグ: バリデーションが実行されたことを示すフラグで、3つめのチェックボックスでのエラーメッセージの表示制御に使用します。

  • _label: ボタンのラベルを更新し、バリデーションの結果(「検証成功!」または「検証失敗!」)をユーザーに表示します。

このボタンを押すことで、フォーム全体のバリデーションが実行され、各フィールドのvalidatorが評価されます。バリデーションの結果に応じて、エラーメッセージが表示されます。

FormFieldの基本的な使い方

FormFieldは、フォーム内で任意のウィジェットを扱い、バリデーションを適用するためのウィジェットです。FormField<T>のようにジェネリクスを使用して、扱う値の型を指定します。今回はチェックボックスを扱うため、FormField<bool>を使用しています。

主なプロパティとメソッド

  • builder: フォームフィールドのUIを構築するための関数を指定します。FormFieldState<T>を引数に取り、ウィジェットを返します。

  • validator: フィールドの値を検証するための関数を指定します。無効な場合はエラーメッセージを返し、有効な場合はnullを返します。

  • autovalidateMode: 自動バリデーションのタイミングを制御します。以下の値が使用できます:

    • AutovalidateMode.disabled: 自動バリデーションを行いません。
    • AutovalidateMode.always: 常にバリデーションを行います。
    • AutovalidateMode.onUserInteraction: ユーザーの操作時にバリデーションを行います。
  • state.didChange(value): フォームフィールドの状態が変更されたことを通知します。これを呼び出すことで、validatorが再評価され、UIが更新されます。

チェックボックスの場合の注意点

Checkboxウィジェット自体にはバリデーション機能がないため、FormField<bool>を使用してバリデーションを実装します。valueの型がboolであることを明示することで、適切なバリデーションと状態管理が可能になります。

チェックボックスのバリデーション方法の比較

本例では、3つのチェックボックスを使用して異なるバリデーション方法を実装しています。それぞれの違いとソースコードを以下に示します。

1. AutovalidateMode.onUserInteractionを使用する方法

多分、標準的な使用方法。
この方法では、ユーザーの操作に応じてバリデーションを行います。state.didChange(value)を呼び出すことで、フォームの状態を更新します。

FormField<bool>(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  builder: (state) {
    return Row(
      children: <Widget>[
        Checkbox(
          value: _checkboxValueFormal,
          onChanged: (value) {
            if (value == null) {
              return;
            }
            setState(() {
              _checkboxValueFormal = value;
              state.didChange(value);
            });
          },
        ),
        const Text('onUserInteractionを利用'),
        Text(
          state.errorText ?? '',
        ),
      ],
    );
  },
  validator: (value) {
    if (value == true) {
      return null;
    }
    return '(ユーザの入力で変化)';
  },
),

ポイント:

  • ジェネリクスの型指定: FormField<bool>として、bool型の値を扱うことを明示しています。

  • autovalidateModeの設定: AutovalidateMode.onUserInteractionを設定することで、ユーザーが操作したときに自動でバリデーションが行われます。

  • state.didChange(value)の呼び出し: onChanged内でstate.didChange(value)を呼び出し、フォームフィールドの状態を更新し、バリデーションを再評価します。

  • エラーメッセージの表示: state.errorTextを使用して、バリデーション結果に応じたエラーメッセージを表示します。

2. メンバー変数を使用して自前でデータを管理する方法

この方法では、state.didChange(value)を使用せず、メンバー変数を直接操作してバリデーションを行います。

FormField<bool>(
  builder: (state) {
    return Row(
      children: <Widget>[
        Checkbox(
          value: _checkboxValuePrivateValue,
          onChanged: (value) {
            if (value == null) {
              return;
            }
            setState(() {
              _checkboxValuePrivateValue = value;
            });
          },
        ),
        const Text('メンバー変数を使用'),
        Text(state.errorText ?? ''),
      ],
    );
  },
  validator: (_) {
    if (_checkboxValuePrivateValue == true) {
      return null;
    }
    return '(検証時のみ動作)';
  },
),

ポイント:

  • ジェネリクスの型指定: こちらもFormField<bool>を使用しています。

  • state.didChangeを使用しない: state.didChange(value)を呼び出さず、メンバー変数_checkboxValuePrivateValueを直接更新します。

  • バリデーションのタイミング: autovalidateModeを指定していないため、検証ボタンを押したとき(_formKey.currentState!.validate()が呼ばれたとき)のみバリデーションが行われます。

3. 検証直後のみエラーメッセージを表示する方法

この方法では、バリデーションフラグ_isJustAfterVerificationを使用し、検証ボタンを押した直後のみエラーメッセージが表示されるように制御します。

// フラグの初期化
var _checkboxValueOnlyAfterValidation = false;
var _isJustAfterVerification = false;


FormField<bool>(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  builder: (state) {
    return Row(
      children: <Widget>[
        Checkbox(
          value: _checkboxValueOnlyAfterValidation,
          onChanged: (value) {
            if (value == null) {
              return;
            }

            setState(() {
              _checkboxValueOnlyAfterValidation = value;
              state.didChange(value);
            });
            _isJustAfterVerification = false;
          },
        ),
        const Text('検証直後のみエラーを出す'),
        Text(
          state.errorText ?? '',
        ),
      ],
    );
  },
  validator: (value) {
    if (value == true || !_isJustAfterVerification) {
      return null;
    }
    return '(検証後)';
  },
),

FilledButton(
  onPressed: () {
    _isJustAfterVerification = true;

    final result =
        _formKey.currentState!.validate() ? '検証成功!' : '検証失敗!';
    setState(() {
      _label = result;
    });
  },
  child: Text(_label),
),

ポイント:

  • フラグによる制御: _isJustAfterVerificationフラグを使用して、バリデーションの結果を管理します。

  • autovalidateModeの設定: AutovalidateMode.onUserInteractionを設定していますが、フラグによってエラーメッセージの表示を制御します。

  • state.didChangeの呼び出し: state.didChange(value)を呼び出して、フォームフィールドの状態を更新します。

  • エラーメッセージの表示制御: validator内で_isJustAfterVerificationフラグをチェックし、必要な場合のみエラーメッセージを返します。

動作の流れ

以下は、チェックボックスのバリデーションが「検証直後のみエラーを出す」動作としてどのように機能しているかをわかりやすくまとめたものです。

  • 検証ボタンの押下

    • ユーザーが検証ボタンを押す
    • _isJustAfterVerification フラグを true に設定
    • フォーム全体のバリデーションを実行 (_formKey.currentState!.validate())
      • FormFieldvalidator が呼び出される
  • バリデーション処理

    • validator 内で以下の条件をチェック
      • チェックボックスがチェックされている (value == true) または
      • _isJustAfterVerificationfalse の場合
        • エラーメッセージは表示されない (null を返す)
      • それ以外の場合
        • エラーメッセージを返す ('(検証後)')
  • エラーメッセージの表示

    • 検証ボタン押下後、チェックボックスが未チェックの場合にエラーメッセージが表示される
    • チェックボックスがチェックされている場合はエラーなし
  • チェックボックスの変更時

    • ユーザーがチェックボックスを変更する (onChanged が呼び出される)
      • チェックボックスの値を更新 (_checkboxValueOnlyAfterValidation)
      • _isJustAfterVerification フラグを false にリセット
      • state.didChange(value) を呼び出してフォームフィールドの状態を更新
        • これにより、ユーザーの操作に基づいてバリデーションが再評価される
  • エラーメッセージの非表示

    • チェックボックスを変更して _isJustAfterVerificationfalse に設定されると、
      • 再度バリデーションを実行してもエラーメッセージは表示されない
      • エラーメッセージの表示は検証ボタン押下直後のみに限定される

このようにして、ユーザーが検証ボタンを押した直後のみエラーメッセージが表示され、その後のチェックボックスの変更ではエラーメッセージが表示されないように制御しています。これにより、ユーザーエクスペリエンスを向上させ、不要なエラーメッセージの表示を防ぐことができます。

Q&A

Q1: state.didChange(value)を呼び出すタイミングはいつが適切ですか?

A1: ユーザーがウィジェットを操作して値が変化したときに、state.didChange(value)を呼び出すことで、フォームの状態とバリデーションを更新できます。特に、autovalidateModeが設定されている場合は、これを呼び出すことで即時にバリデーション結果が反映されます。

Q2: バリデーションフラグ_isValidatedを使うメリットは何ですか?

A2: _isValidatedフラグを使うことで、ユーザーがチェックを外した際に不要なエラーメッセージが表示されるのを防ぎ、検証ボタンを押した直後のみエラーを表示することができます。これにより、ユーザーエクスペリエンスを向上させることができます。

Q3: autovalidateModeの違いは何ですか?

A3: autovalidateModeはバリデーションの自動実行タイミングを制御します。

  • AutovalidateMode.disabled: 自動バリデーションを行わず、Formvalidate()メソッドを呼び出したときのみバリデーションが実行されます。

  • AutovalidateMode.always: 常にバリデーションを行い、フィールドの値が変化するたびにバリデーションが実行されます。

  • AutovalidateMode.onUserInteraction: ユーザーがフィールドを操作したときにバリデーションを行います。

まとめ

チェックボックスをフォーム内でバリデーションする3つの方法を比較し、それぞれの特徴と実装方法を解説しました。FormFieldを使用することで、チェックボックスや他のウィジェットでもバリデーションを適用できます。特に、FormField<T>のジェネリクスを活用し、state.didChangevalidatorautovalidateModeを適切に組み合わせることで、柔軟なバリデーションが可能です。また、共通の検証ボタンを使用してフォーム全体のバリデーションを実行する方法も説明しました。用途に応じて最適な方法を選択し、ユーザーにとって使いやすいフォームを作成しましょう。

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _formKey = GlobalKey<FormState>();
  var _label = '検証';
  var _checkboxValueFormal = false;
  var _checkboxValuePrivateValue = false;
  var _checkboxValueOnlyAfterValidation = false;

  var _isJustAfterVerification = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Form(
        key: _formKey,
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FormField<bool>(
              autovalidateMode:
                  AutovalidateMode.onUserInteraction, // state.didChangeと連動
              builder: (state) {
                return Row(
                  children: <Widget>[
                    Checkbox(
                        value: _checkboxValueFormal,
                        onChanged: (value) {
                          if (value == null) {
                            return;
                          }
                          setState(() {
                            _checkboxValueFormal = value;
                            state.didChange(value);
                          });
                        }),
                    const Text('onUserInteractionを利用'),
                    Text(
                      state.errorText ?? '',
                    )
                  ],
                );
              },
              validator: (value) {
                if (value == true) {
                  return null;
                }
                return '(ユーザの入力で変化)';
              },
            ),
            FormField<bool>(
              builder: (state) {
                return Row(
                  children: <Widget>[
                    Checkbox(
                        value: _checkboxValuePrivateValue,
                        onChanged: (value) {
                          if (value == null) {
                            return;
                          }
                          setState(() {
                            _checkboxValuePrivateValue = value;
                          });
                        }),
                    const Text('メンバー変数を使用'),
                    Text(state.errorText ?? '')
                  ],
                );
              },
              validator: (_) {
                if (_checkboxValuePrivateValue == true) {
                  return null;
                }
                return '(検証時のみ動作)';
              },
            ),
            FormField<bool>(
              autovalidateMode: AutovalidateMode.onUserInteraction,
              builder: (state) {
                return Row(
                  children: <Widget>[
                    Checkbox(
                        value: _checkboxValueOnlyAfterValidation,
                        onChanged: (value) {
                          if (value == null) {
                            return;
                          }

                          setState(() {
                            _checkboxValueOnlyAfterValidation = value;
                            state.didChange(value);
                          });
                          _isJustAfterVerification = false;
                        }),
                    const Text('検証直後のみエラーを出す'),
                    Text(
                      state.errorText ?? '',
                    )
                  ],
                );
              },
              validator: (value) {
                if (value == true || !_isJustAfterVerification) {
                  return null;
                }
                return '(検証後)';
              },
            ),
            FilledButton(
                onPressed: () {
                  _isJustAfterVerification = true;

                  final result =
                      _formKey.currentState!.validate() ? '検証成功!' : '検証失敗!';
                  setState(() {
                    _label = result;
                  });
                },
                child: Text(_label)),
          ],
        ),
      ),
    );
  }
}