【Flutter】フォーム項目にエラーをValidation以外で設定する方法

## 概要
Flutterで入力フォームのチェックを実装する場合、[[Flutter]Validationを用いたformを作る](https://zenn.dev/t_fukuyama/articles/57192adf2b182b)の記事にある通り、Validationの機能を使って実装すると思います。画面側のチェックはこれで良いのですが、サーバサイドでのチェックがNGになった時も、バリデーションと同じ形式でフォーム項目にエラーを設定したいです。その方法を今回はメモ書きします。

## 対応方針
[Manually setting Flutter Validation Error](https://stackoverflow.com/questions/58243997/manually-setting-flutter-validation-error)のstackoverflowの記事に、対応方法がいくつか紹介されていますが、今回は`errorText`の項目にstateで管理したメッセージを設定する方針とします。

## 前提
* 使用したFlutterのバージョンは`3.3.10`です。
* Webで動作確認を行いました。

## 実装イメージ
入力したユーザIDが、サーバサイドでチェックして登録済みの場合に、
以下のようにフォーム項目にエラーを表示します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32098/3257eab3-bfd4-da9b-1c34-581cbde6a2de.png" width=250/>

## 実装サンプル
以下のサンプルでは、Stateは`hooks_riverpod`を使用して管理していますが、普通にStatefulWidgetを使っても問題ないと思います。

```dart
class UserAccountRegisterWidget extends StatefulHookConsumerWidget {
  const UserAccountRegisterWidget({super.key});

  @override
  UserAccountRegisterState createState() => UserAccountRegisterState();
}

class UserAccountRegisterState
    extends ConsumerState<UserAccountRegisterWidget> {
  final _formKey = GlobalKey<FormState>();
  final userIdRegisterController = TextEditingController();
  final userNameRegisterController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    // サーバサイドのエラー用メッセージstate
    final userIdServerErrorMsgState = useState<String?>(null);

    Future<void> submitUserAccount() async {
      // 登録のAPIをサーバサイドに投げる
      var res = await UserAccountApiService.createUser(
        userIdRegisterController.text,
        userNameRegisterController.text,
      );
      // サーバサイドの結果を判定してチェックNGの場合はメッセージをセット
      if (res.error == "userId") {
        userIdServerErrorMsgState.value = "登録済みのユーザIDです";
      }
    }

    return Center(
      child: Form(
        key: _formKey,
        child: Column(
          children: [
            const SizedBox(height: 20),
            ConstrainedBox(
                constraints: const BoxConstraints(maxWidth: 300),
                child: TextFormField(
                  controller: userIdRegisterController,
                  decoration: InputDecoration(
                    label: Row(children: const [
                      Text('*', style: TextStyle(color: Colors.red)),
                      Padding(
                        padding: EdgeInsets.all(3.0),
                      ),
                      Text("ユーザID")
                    ]),
                    border: const OutlineInputBorder(),
                    errorText: userIdServerErrorMsgState.value,
                  ),
                  validator: (value) {
                    // 一度サーバのエラーMSGを初期化する。
                    userIdServerErrorMsgState.value = null;
                    final regUserId = RegExp(r'^[a-zA-Z0-9_-]+$');
                    if (value != null && regUserId.hasMatch(value)) {
                      return null;
                    }
                    return '半角英数字、「_」、「-」で入力してください';
                  },
                )),
            const SizedBox(height: 20),
            ConstrainedBox(
                constraints: const BoxConstraints(maxWidth: 300),
                child: TextFormField(
                  controller: userNameRegisterController,
                  decoration: InputDecoration(
                    label: Row(children: const [
                      Text('*', style: TextStyle(color: Colors.red)),
                      Padding(
                        padding: EdgeInsets.all(3.0),
                      ),
                      Text("ユーザ名")
                    ]),
                    border: const OutlineInputBorder(),
                  ),
                  validator: (value) {
                    if (value == null || value.isEmpty) {
                      return '入力必須です';
                    }
                    return null;
                  },
                )),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () async => {
                if (_formKey.currentState!.validate())
                  {
                    // ユーザ登録処理
                    await submitUserAccount()
                  }
              },
              style: ElevatedButton.styleFrom(fixedSize: const Size(250, 50)),
              child: const Text('会員登録'),
            )
          ],
        ),
      ),
    );
  }
}
```