【Flutter】Form ウィジェットの構造ぜんぶ見て学ぶ、TextField と TextFormField の違い
TextField と TextFormField の違い
TextFormField
は、内部で TextField
を使用しつつ FormField<String>
を継承したクラスです。
つまり、2つのウィジェットの関係は、ざっくり TextFormField
= TextField
+ FormField
って感じになります。
FormField
FormField
の機能はざっくり以下の通りです。
enabled
onSaved
- 型
T
のinitialValue
と1つの値の管理 - バリデーション、エラーの管理
- 親の
Form
ウィジェットとの連携
enable
はその名の通りで TextField
にもあるのと、onSaved
は使用機会があまりないので省きます。
よって以下の3つについて読み解いていきます。
- 型
T
のinitialValue
と1つの値の管理 - バリデーション、エラーの管理
- 親の
Form
ウィジェットとの連携
加えて FormField
は、単なる StatefulWidget
です。
なので、普段自分が作った StatefulWidget
を見る感覚で内部構造を把握できるので、そこまで難易度は高くありません。
StatefulWidget
の処理が書いてある箇所はほぼ State
クラスです。
さらに、そこで管理されている 再代入可能な変数 の扱いを網羅できれば、その StatefulWidget
のほぼ全てを知ったと言ってもいいでしょう。
FormField
の State
クラスである、FormFieldState
の変数を見てみましょう。
T _value
String _errorText
bool _hasInteractedByUser
T
の initialValue
と1つの値の管理
型 先ほど挙げた通り、FormFieldState
は _value
によって1つの値を管理しています。
むしろこの1つの値を管理することがこの StatefulWidget
目的で、それを色々便利に扱う周辺の機能が用意されています。
以下が変数 _value
の抜粋です。
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
late T? _value = widget.initialValue;
T? get value => _value;
初期化処理として widget.initialValue
が入って、public なゲッター value
も提供されています。
この _value
の正体を探る唯一の方法は、この変数に代入されている処理を探ることです。
以下が _value
に代入している関数を抜粋したものです(コメントなどは削除しています)。
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
late T? _value = widget.initialValue;
T? get value => _value;
void reset() {
setState(() {
_value = widget.initialValue;
_hasInteractedByUser.value = false;
_errorText.value = null;
});
Form.maybeOf(context)?._fieldDidChange();
}
void didChange(T? value) {
setState(() {
_value = value;
_hasInteractedByUser.value = true;
});
Form.maybeOf(context)?._fieldDidChange();
}
void setValue(T? value) {
_value = value;
}
これで、一旦 _value
について分かった事だけをまとめると
- 初期化段階で
widget.initialValue
が代入されている。 - 変数を代入して
setState
を行う、didChange
関数が用意されている。 -
widget.initialValue
を再代入するreset
関数が用意されている。 - 単に値を代入するだけの
setValue
関数が用意されている。
didUpdateWidget がない
加えて、_value
の再代入されている箇所を網羅したことで、didUpdateWidget
がないことも分かります。
よって、TextFormField
で追加されていなければ、以下のようにinitialValue
に渡す値を変更したところで、値が変わらないことが分かります。
Widget build(BuildContext context, WidgetRef ref) {
// このウィジェットをビルドした時にローディングで hasValue == false であれば、
// 取得しても初期値として入らない
final user = ref.watch(asyncUserProvider).valueOrNull;
return TextFormField(
initialValue: user.name,
);
}
では、TextFormField
でどう使われているか見てみましょう。
FormField
の build
関数で最終的に返されるウィジェットは、引数として渡される builder
に依存します。
builder
のコールバックの引数には this
が渡され、FormFieldState
の public な機能が全て使えます。
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
Widget build(BuildContext context) {
// ...
return widget.builder(this);
}
}
TextFormField
でさっき見た didChange
、reset
が使われている箇所を確認してみましょう。
class TextFormField extends FormField<String> {
TextFormField({
}) : // ...
super(
builder: (FormFieldState<String> field) {
// ① TextField.onChanged と同期して didChange が発火するようになっている
void onChangedHandler(String value) {
field.didChange(value);
onChanged?.call(value);
}
return UnmanagedRestorationScope(
bucket: field.bucket,
child: TextField(
// ...
onChanged: onChangedHandler
// ...
class _TextFormFieldState extends FormFieldState<String> {
void initState() {
super.initState();
if (_textFormField.controller == null) {
_createLocalController(
widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null,
);
} else {
// ② controller が渡されている場合、_handleControllerChanged が発火するように
_textFormField.controller!.addListener(_handleControllerChanged);
}
}
// ③ didChange を実行した時にも
void didChange(String? value) {
super.didChange(value);
// 無限ループ対策
if (_effectiveController.text != value) {
_effectiveController.value = TextEditingValue(text: value ?? '');
}
}
// reset 実行時に TextEditingController も初期値(widget.initialValue)にする
void reset() {
_effectiveController.value = TextEditingValue(text: widget.initialValue ?? '');
super.reset();
_textFormField.onChanged?.call(_effectiveController.text);
}
// ② TextEditingController と 同期して didChange を発火
void _handleControllerChanged() {
// 無限ループ対策
if (_effectiveController.text != value) {
didChange(_effectiveController.text);
}
}
このように TextFormField
では、FormField
で管理している値 _value
をTextField
と同期させる処理が書かれていることが分かります。
少し TextFormField
の解像度が上がったのではないでしょうか。
これで FormField
と TextField
が同期されていることがわかりました。
ですが、これだけでは苦労して同期させたのに、追加された機能は reset
くらいです。
続いて、これで管理した値を使って更なるやりたい事を実現させます。
バリデーション、エラーの管理
バリデーションは、現代に求められるフォームのユーザー体験において必須の機能です。
ここで登場するのがこの2つの変数です。
String _errorText
bool _hasInteractedByUser
_hasInteractedByUser
は、ある1つの機能を実現するだけの細かい実装なので、ほとんど _errorText
の話になります。
上項と同様、_errorText
とその再代入されている箇所を、少しづつ見ていきます。
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
String? _errorText;
String? get errorText => _errorText;
bool get hasError => _errorText != null;
void initState() {
super.initState();
_errorText = widget.forceErrorText;
}
void didUpdateWidget(FormField<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.forceErrorText != oldWidget.forceErrorText) {
_errorText = widget.forceErrorText;
}
}
ここでわかるのは以下の2つです。
- エラーがあるかどうか(
hasError
)は、条件式_errorText != null
で表される。 -
FormField.forceErrorText
が用意されていて、これが更新されれば_errorText
に毎回代入される。
👇 バリデーションを実行している箇所を見ると、これらの関数や変数が利用されているのがわかります。
bool get isValid => widget.forceErrorText == null && widget.validator?.call(_value) == null;
bool validate() {
setState(() {
_validate();
});
return !hasError;
}
void _validate() {
if (widget.forceErrorText != null) {
_errorText.value = widget.forceErrorText;
// Skip validating if error is forced.
return;
}
if (widget.validator != null) {
_errorText.value = widget.validator!(_value);
} else {
_errorText.value = null;
}
}
`errorText` の表示
TextField
と TextFormField
の違いは、バリデーションの有無(errorText
の有無)というのを見てきました。
よって、エラー表示の有無も TextField
と TextFormField
の違いともいえます。
この errorText
は、TextField
に渡す InputDecoration
に代入することで実現しています。
class TextFormField extends FormField<String> {
TextFormField({
super(
builder: (FormFieldState<String> field) {
final String? errorText = field.errorText;
if (errorText != null) {
effectiveDecoration =
errorBuilder != null
? effectiveDecoration.copyWith(error: errorBuilder(state.context, errorText))
: effectiveDecoration.copyWith(errorText: errorText);
}
TextField(
decoration: effectiveDecoration,
_hasInteractedByUser
と AutovalidateMode
続いて、_hasInteractedByUser
変数の使われている箇所を見てみましょう。
まずは代入されている箇所です。
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
var _hasInteractedByUser = false;
bool get hasInteractedByUser => _hasInteractedByUser;
void reset() {
setState(() {
_value = widget.initialValue;
_hasInteractedByUser = false;
_errorText = null;
});
Form.maybeOf(context)?._fieldDidChange();
}
void didChange(T? value) {
setState(() {
_value = value;
_hasInteractedByUser = true;
});
Form.maybeOf(context)?._fieldDidChange();
}
_hasInteractedByUser
で代入している箇所は2つだけです。
-
reset
関数でfalse
が代入されている -
didChange
関数でtrue
が代入されている。
では、参照されている箇所を見てみましょう。
Widget build(BuildContext context) {
if (widget.enabled) {
switch (widget.autovalidateMode) {
case AutovalidateMode.always:
_validate();
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate();
}
case AutovalidateMode.onUnfocus:
case AutovalidateMode.disabled:
break;
}
}
FormField
には、リビルドするたびにバリデーションを実行する(_errorText
変数にバリデーション結果が代入される) AutovalidateMode
が用意されています。
AutovalidateMode.always
を設定すれば、その名の通りリビルドされるたびにバリデーションが実行されます。
似たような名前に AutovalidateMode.onUserInteraction
があり、その中で _hasInteractedByUser
は使われます。
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate();
}
つまり、_hasInteractedByUser
の代入される整理するとこうなります。
-
AutovalidateMode.always
=> リビルドされるたびにバリデーションを実行 -
AutovalidateMode.onUserInteraction
=> リビルドされるたびにバリデーションを実行(reset
関数の実行後を除く)
これで全ての 再代入可能な変数 を読んだので、このウィジェットの主要な機能をほとんどの機能をみることができました。
最後は、Form
ウィジェットとの連携部分を見ていきましょう。
Form
ウィジェットとの連携
親の 一般的に TextFormField
のバリデーションを実行するときは、親を Form
ウィジェットで囲み、そこに GlobalKey
を渡すなどして複数のバリデーションを一気に実行します。
class _MyWdgetState extends State<MyWidget> {
final _formKey = GlobalKey<FormState>();
void _validate() {
_formKey.currentState.validate();
}
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
validator: (String? value) {
if (value!.isEmpty) {
return "必須項目です";
}
return null;
}
),
TextFormField(
validator: (String? value) {
if (value!.isEmpty) {
return "必須項目です";
}
if (int.tryParge(value) == null) {
return "整数で入力してください";
}
return null;
}
),
],
)
);
}
Form
ウィジェットは、子孫の FormField
ウィジェットの State クラスを、Set
で保持することで、これらのバリデーションを行っています。
(リンク)
class FormState extends State<Form> {
final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{};
void _register(FormFieldState<dynamic> field) {
_fields.add(field);
}
void _unregister(FormFieldState<dynamic> field) {
_fields.remove(field);
}
これを実現するためには、親である Form
ウィジェットから子孫のウィジェットを判別する必要があります。
親から子孫をチェックしてもいいのですが、それでは全ての子孫をチェックすることになってしまいます。
こういう場合、Flutter では InheritedWidget
で子孫から特定の親にアクセスすることができます。
Form.of(context)
Form
/FormField
が実装されているファイルでは、プライベートで FormState
を保持した InheritedWidget
が実装されています。
class _FormScope extends InheritedWidget {
const _FormScope({required super.child, required FormState formState, required int generation})
: _formState = formState,
_generation = generation;
final FormState _formState;
/// Incremented every time a form field has changed. This lets us know when
/// to rebuild the form.
final int _generation;
/// The [Form] associated with this widget.
Form get form => _formState.widget;
bool updateShouldNotify(_FormScope old) => _generation != old._generation;
}
Form.of
/Form.maybeOf
で子孫からアクセスできるようになっています。
class Form extends StatefulWidget {
static FormState? maybeOf(BuildContext context) {
final _FormScope? scope = context.dependOnInheritedWidgetOfExactType<_FormScope>();
return scope?._formState;
}
static FormState of(BuildContext context) {
final FormState? formState = maybeOf(context);
return formState!;
}
FormState createState() => FormState();
}
class FormState extends State<Form> {
int _generation = 0;
void _forceRebuild() {
setState(() {
++_generation;
});
}
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: widget.onWillPop,
child: _FormScope(formState: this, generation: _generation, child: widget.child),
);
}
FormState._register
あとは、FormField
側で親の FormState
にアクセス、自身の FormFieldState
のインスタンスを登録します。
Form.maybeOf(context)?._register
は build 関数で実行されています。
(setState
さえしなければ、build 関数でこういうことしていいんだみたいな気持ちになります)
class FormField<T> extends StatefulWidget {
FormFieldState<T> createState() => FormFieldState<T>();
}
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
Widget build(BuildContext context) {
Form.maybeOf(context)?._register(this);
}
}
そのほか、この Form.mabeOf
を使って、Form
の onChanged
を発火させたりもしています。
class FormState extends State<Form> {
final Set<FormFieldState<dynamic>> _fields = <FormFieldState<dynamic>>{};
// Called when a form field has changed. This will cause all form fields
// to rebuild, useful if form fields have interdependencies.
void _fieldDidChange() {
widget.onChanged?.call();
_hasInteractedByUser = _fields.any(
(FormFieldState<dynamic> field) => field._hasInteractedByUser.value,
);
_forceRebuild();
}
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
void didChange(T? value) {
setState(() {
_value = value;
_hasInteractedByUser.value = true;
});
Form.maybeOf(context)?._fieldDidChange();
}
まとめ
TextFormField
は、 TextField
に FormField
をプラスしたウィジェットです。
- バリデーション、エラーの管理
- 初期値(
initialValue
)へ変更するreset
関数 - 親の
Form
ウィジェットとの連携- 複数
FormField
のバリデーションの発火 - 値が変わるたび、
Form.onChanged
の発火
- 複数
逆に言えば、これらを使わないのであれば、TextField
で良さそうです。
Discussion