Flutter Web+標準のFormでまじめにログインフォーム作ったら結構大変だった件

# モチベーション
* **Flutter Web**+**Firebase Auth**シリーズの続き（前回→https://qiita.com/hummer/items/65b296803f8b200838bd）
* ログインフォームをまじめに作ったら結構大変だったのでここに記す

# 実際のコード
```dart
@freezed
abstract class LoginState with _$LoginState {
  const factory LoginState({
    @Default('') String email,
    @Default('') String password,
  }) = _LoginState;
}

final _formKey = GlobalKey<FormState>();

void _onSubmit() {
  if( _formKey.currentState.validate()){
    // 注: ここに_formKey.currentState.save()なんか書くな（後述)

    // ...成功時の処理
  }
}

// 以下いい感じのScaffoldの下に表示してね
Widget _buildForm(ValueNotifier<LoginState> state) {
    return Form(
      key: _formKey,
      child: AutofillGroup(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextFormField(
              initialValue: state.value.email,
              autofocus: true,
              autovalidateMode: AutovalidateMode.onUserInteraction,
              autofillHints: [AutofillHints.username],
              onFieldSubmitted: (_) => _onSubmit(),
              onChanged: (value) => state.value = state.value.copyWith(email: value),
              validator: (value) => (value.isEmpty) ? 'メールアドレスを入力してください' : null,
            ),
            const SizedBox(height: 16),
            TextFormField(
              initialValue: state.value.password,
              autovalidateMode: AutovalidateMode.onUserInteraction,
              autofillHints: [AutofillHints.password],
              onFieldSubmitted: (_) => _onSubmit(),
              obscureText: true,
              onChanged: (value) => state.value = state.value.copyWith(password: value),
              validator: (value) => (value.isEmpty) ? 'パスワードを入力してください' : null,
            ),
            const SizedBox(height: 16),
            SizedBox(
              height: 44,
              child: RaisedButton.icon(
                icon: const Icon(Icons.login),
                label: const Text('サインイン'),
                onPressed: _onSubmit,
              ),
            ),
          ],
        ),
      ),
    );
  }
```

# 解説

## `autofillHints`を設定すべし
```dart
            TextFormField(
              autofillHints: [AutofillHints.username],
            ),
```
Chromeなどでパスワードの保存をしたい場合、アカウント名の方に`autofillHints`に`[AutofillHints.username]`をセットする。
パスワードの方は`[AutofillHints.password]`にする

## `AutoFillGroup`でくくるべし

* アカウント名とパスワードの`TextFormField`を`AutofillGroup`で囲っておく。
* さもないとパスワードだけ保存しようとしたりする。

## `maxLines=1`をいじらない
* `maxLines`が`null`とか`2以上`だと`input要素`でなく`textarea要素`がレンダリングされる。
* そのため、ブラウザのパスワードの管理が、期待通り動作しない

## EnterでSubmitしたいなら各フィールドで`onFieldSubmitted`

* `onFieldSubmitted: (_) => _onSubmit()`と書いておくと、ボタンを押したのと同じ処理が走るので、見た目EnterでSubmitしたように見える。
* Keyイベント拾って頑張る方法もあるようだが、とりあえずこれでも動いてる。

## 自然なオートバリデーションをするには？
* 各`TextFormField`に`autovalidateMode: AutovalidateMode.onUserInteraction`を指定する
  * 実は`Form`にも`autovalidateMode`はあるのだが「Formのいずれかを書き換えるとすべてのフィールドでバリデーションが走る」動作になるので**「違う、そうじゃない」**という気分にさせられるのでやってみても良い
* 画面遷移時には`autofocus: true`のフィールドにフォーカスが当たるが、`onUserInteraction`だと、この時バリデーションは走らない
* ユーザーの入力に応じて、**「入力中のフィールドのみ」**バリデーションが走るようになる

# 標準`Form Widget`のつらみ

## Formから値が取り出しにくい

* 実は`FormState`は`_fields`という値で、この[FormFieldの配列を持ってるくせ](https://github.com/flutter/flutter/blob/ee66a50162d4d97ea60affa5a6e70124eb565e59/packages/flutter/lib/src/widgets/form.dart#L165)に***アクセサがない***
* 一応、**「全部のFormFieldにkeyを指定」**しまくれば`_fieldKey.currentState.value`と書くことで、それぞれの`FormField`が保持している値の取得はできる
  * https://github.com/flutter/flutter/blob/ee66a50162d4d97ea60affa5a6e70124eb565e59/packages/flutter/lib/src/widgets/form.dart#L416 

## `_formKey.currentState.validate()`が`bool`しか返さないのでどこのフォームがエラーなのかわからない

* 前節の**「全部のFormFieldにkeyを指定」**を配列に持てば一応エラーが発生したフィールドはわかる

```dart
   [emailKey, passwordKey, firstNameKey, secondNameKey].firstWhere((e) => !e.isValid);
   /// つかfirstWhere()は値が無いとthrowすんの勘弁してほしい...null返して...
```
* しかし、**エラーが発生しているフォームにフォーカスを当てる**のがまた面倒。
  * しょうがないので**「全部のFormFieldにfocusNodeを指定」**して、`requestFocus()`するしかなさそう
  
```dart
   final index = [emailKey, passwordKey, firstNameKey, secondNameKey].firstWhere((e) => !e.isValid);
   [emailNode, passwordNode, firstNameNode, secondNode][index]?.requestFocus();
```

# 結論
### まじめに作ると、めちゃめちゃめんどくさいので、フォーム系のライブラリ使ったほうが100倍楽

* なんでこんな使いにくい仕様になってるのか、理由がわかる方がいたら是非おしえてください。

# おまけ

* `Form Widget`を扱ってるblogとかで`submit動作`でこんな記述を紹介している

```dart
    TextFormField(
      onChanged: (value) => _email = value,
    ),
    TextFormField(
      onChanged: (value) => password = value,
    )

    // ...
   RaisedButton(
     onPressed: () {
       if(_formKey.currentState.valdate()) {

         _formKey.currentState.save(); // ←これ

         // ...成功した時の処理
       }
     }
```

* 実際のところ`save()`が何しているかというと...

  * https://github.com/flutter/flutter/blob/ee66a50162d4d97ea60affa5a6e70124eb565e59/packages/flutter/lib/src/widgets/form.dart#L218-L222 より抜粋

```dart
//FormStateのsave()

/// Saves every [FormField] that is a descendant of this [Form].
  void save() {
    for (final FormFieldState<dynamic> field in _fields)
      field.save();
  }

// FormFieldStateのsave()
/// Calls the [FormField]'s onSaved method with the current value.
  void save() {
    if (widget.onSaved != null)
      widget.onSaved!(value);
  }
// 

```

* 最終的に`FormField`の`onSave()`を呼んでるだけなので、`TextFormField`に`onSave`が書いて無ければ***何の意味もない***
* 完全におまじないで***草***