モチベーション
- Flutter Web+Firebase Authシリーズの続き(前回→https://qiita.com/hummer/items/65b296803f8b200838bd)
- ログインフォームをまじめに作ったら結構大変だったのでここに記す
実際のコード
@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
でくくるべし
- アカウント名とパスワードの
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の配列を持ってるくせにアクセサがない - 一応、**「全部のFormFieldにkeyを指定」**しまくれば
_fieldKey.currentState.value
と書くことで、それぞれのFormField
が保持している値の取得はできる
_formKey.currentState.validate()
がbool
しか返さないのでどこのフォームがエラーなのかわからない
- 前節の**「全部のFormFieldにkeyを指定」**を配列に持てば一応エラーが発生したフィールドはわかる
[emailKey, passwordKey, firstNameKey, secondNameKey].firstWhere((e) => !e.isValid);
/// つかfirstWhere()は値が無いとthrowすんの勘弁してほしい...null返して...
- しかし、エラーが発生しているフォームにフォーカスを当てるのがまた面倒。
- しょうがないので**「全部のFormFieldにfocusNodeを指定」**して、
requestFocus()
するしかなさそう
- しょうがないので**「全部のFormFieldにfocusNodeを指定」**して、
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(); // ←これ
// ...成功した時の処理
}
}
-
実際のところ
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);
}
//
- 最終的に
FormField
のonSave()
を呼んでるだけなので、TextFormField
にonSave
が書いて無ければ何の意味もない - 完全におまじないで草