💡

【Flutter】TextFormFieldのバリデーションでアイコンを使う方法

2024/03/15に公開

はじめに

FlutterのTextFormFieldでバリデーションを使用した際に、エラーメッセージにアイコンを付けたかったのですが、通常の方法ではアイコンを表示することができなかったので、その方法を調べました。

問題点

FlutterのTextFormFieldではvalidatorプロパティでフォームのバリデーションを行うことができます。
validatorは戻り値としてString型を返す必要があり、この文字列がエラーメッセージとして表示されます。
このエラーメッセージにアイコンを追加したいのですが、上述のように、返り値はString型であるため、Iconウィジェットなどを使用することができません。

解決方法

先に言ってしまうとあまりスマートな解決方法は見つけられませんでした。
以下では、Unicodeを使用してアイコンを表示する方法と、独自のウィジェットを作成してアイコンを表示する方法の2つについて説明していきます。

方法1

1つ目の方法としてはUnicodeを使ってアイコンを表示します。
シンプルなアイコンであればUnicodeを使って表示ができるため、validatorの返り値になる文字列にUnicodeでアイコンを追加するというものになります。
Unicodeで表示できるアイコンは限られており、デザインの調整などはできません。そのため、見た目にはこだわりなく、とにかくアイコンを表示できれば良いという場合には適していると思います。

  TextFormField(
    validator: (value) {
      if (value == null || value.isEmpty) {
        // unicodeを使ってアイコンを表示
        return '\u26A0 Please enter some text';
      }
      return null;
    },
  ),

方法2

2つ目の方法としては独自のウィジェットを作成してアイコンを表示します。
まず、Rowウィジェットを使ってアイコンとメッセージを表示するためのウィジェットを作成します。

class CustomErrorMessage extends StatelessWidget {
  const CustomErrorMessage({super.key, required this.message});

  final String message;

  
  Widget build(BuildContext context) {
    return Row(children: [
      const Icon(Icons.error, color: Colors.red),
      Text(
        message,
        style: const TextStyle(color: Colors.red),
      ),
    ]);
  }
}

次にTextFormFieldの下に作成したVisibleウィジェットと一緒に配置します。

class MyCustomForm extends StatefulWidget {
  const MyCustomForm({super.key});

  
  MyCustomFormState createState() {
    return MyCustomFormState();
  }
}

class MyCustomFormState extends State<MyCustomForm> {
    final _formKey = GlobalKey<FormState>();

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
             ...
          ),
          // TextFormFieldの下にVisibilityと作成したCustomErrorMessageを配置
          Visibility(
            child: const CustomErrorMessage(message: 'Please enter some text'),
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Processing Data')),
                );
              }
            },
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}

Visibilityウィジェットはvisibleプロパティの状態によって表示・非表示が切り替わるため、bool型の変数isValidを定義し、visibleに設定します。
バリデーション失敗時にisValidの状態を変化させるようにsetStateで状態を変更します。
このとき、バリデーション内で空の文字列を返却するようにしないと、バリデーションが正常に動作しないため注意が必要です。

class MyCustomFormState extends State<MyCustomForm> {
  final _formKey = GlobalKey<FormState>();
+ bool _isValid = true;

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
+            // validatorの追加
+            validator: (value) {
+              if (value == null || value.isEmpty) {
+                // バリデーション失敗時にメッセージを表示
+                setState(() {
+                  _isValid = true;
+                });
+                // 空文字を返さないとSubmitをタップした際に_formKey.currentState!.validate()がtrueになって、Submitの処理が実行されてしまうので注意
+                return '';
+              }
+              // バリデーション成功時はメッセージを非表示
+              setState(() {
+                _isValid = false;
+              });
+              return null;
+            },
          ),
          Visibility(
+           // メッセージの表示・非表示の条件を設定
+           visible: !_isValid,
            child: const CustomErrorMessage(message: 'Please enter some text'),
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Processing Data')),
                );
              }
            },
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}

これでSubmitを押した際に、アイコンとエラーメッセージが表示されるようになりました。

しかし、このままではデフォルトのエラーメッセージよりフォームとエラーメッセージの間が空きすぎてしまいます。
これを修正するためには、TextFormFieldのdecorationプロパティを使ってerrorStyleを指定することで調整が可能です。

...
 TextFormField(
+    decoration: const InputDecoration(
+      errorStyle: TextStyle(
+        fontSize: 1,
+      ),
+    ),
    ...

これでいい感じにアイコンとエラーメッセージを表示することができました。
メッセージ部分は単にIconとTextを使っているだけなので、アニメーションなどを追加することも自由にできます。

コード全文は以下になります。

コード全文
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    const appTitle = 'Form Validation Demo';

    return MaterialApp(
      title: appTitle,
      home: Scaffold(
        appBar: AppBar(
          title: const Text(appTitle),
        ),
        body: const MyCustomForm(),
      ),
    );
  }
}

class MyCustomForm extends StatefulWidget {
  const MyCustomForm({super.key});

  
  MyCustomFormState createState() {
    return MyCustomFormState();
  }
}

class MyCustomFormState extends State<MyCustomForm> {
  final _formKey = GlobalKey<FormState>();
  bool _isValid = true;

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(
              errorStyle: TextStyle(
                fontSize: 1,
              ),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                setState(() {
                  _isValid = false;
                });
                return '';
              }
              setState(() {
                _isValid = true;
              });
              return null;
            },
          ),
          Visibility(
            visible: !_isValid,
            child: const CustomErrorMessage(message: 'Please enter some text'),
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Processing Data')),
                );
              }
            },
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}

class CustomErrorMessage extends StatelessWidget {
  const CustomErrorMessage({super.key, required this.message});

  final String message;

  
  Widget build(BuildContext context) {
    return Row(children: [
      const Icon(Icons.error, color: Colors.red),
      Text(
        message,
        style: const TextStyle(color: Colors.red),
      ),
    ]);
  }
}

まとめ

2つの方法を確認しましたが、どちらもあまりスマートな感じがしませんでした。
デフォルトでアイコンが指定できると便利な気がします。

参考

https://docs.flutter.dev/cookbook/forms/validation
https://stackoverflow.com/questions/59376046/how-to-add-icon-to-errortext-below-textformfield
https://www.choge-blog.com/programming/fluttertextformfield-validationerrortext-size/

Discussion