LoginSignup
11
7

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-08

モチベーション

実際のコード

@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を設定すべし

            TextFormField(
              autofillHints: [AutofillHints.username],
            ),

Chromeなどでパスワードの保存をしたい場合、アカウント名の方にautofillHints[AutofillHints.username]をセットする。
パスワードの方は[AutofillHints.password]にする

AutoFillGroupでくくるべし

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

maxLines=1をいじらない

  • maxLinesnullとか2以上だとinput要素でなくtextarea要素がレンダリングされる。
  • そのため、ブラウザのパスワードの管理が、期待通り動作しない

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

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

自然なオートバリデーションをするには?

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

標準Form Widgetのつらみ

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

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

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

結論

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

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

おまけ

  • Form Widgetを扱ってるblogとかでsubmit動作でこんな記述を紹介している
    TextFormField(
      onChanged: (value) => _email = value,
    ),
    TextFormField(
      onChanged: (value) => password = value,
    )

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

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

         // ...成功した時の処理
       }
     }
//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);
  }
// 

  • 最終的にFormFieldonSave()を呼んでるだけなので、TextFormFieldonSaveが書いて無ければ何の意味もない
  • 完全におまじないで
11
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7