【Flutter】Form ウィジェットの構造ぜんぶ見て学ぶ、TextField と TextFormField の違い
TextField と TextFormField の違い
TextFormField は、内部で TextField を使用しつつ FormField<String> を継承したクラスです。
つまり、2つのウィジェットの関係は、ざっくり TextFormField = TextField + FormField って感じになります。
FormField
FormField の機能はざっくり以下の通りです。
enabledonSaved- 型
TのinitialValueと1つの値の管理 - バリデーション、エラーの管理
- 親の
Formウィジェットとの連携
enable はその名の通りで TextField にもあるのと、onSaved は使用機会があまりないので省きます。
よって以下の3つについて読み解いていきます。
- 型
TのinitialValueと1つの値の管理 - バリデーション、エラーの管理
- 親の
Formウィジェットとの連携
加えて FormField は、単なる StatefulWidget です。
なので、普段自分が作った StatefulWidget を見る感覚で内部構造を把握できるので、そこまで難易度は高くありません。
StatefulWidget の処理が書いてある箇所はほぼ State クラスです。
さらに、そこで管理されている 再代入可能な変数 の扱いを網羅できれば、その StatefulWidget のほぼ全てを知ったと言ってもいいでしょう。
FormField の State クラスである、FormFieldState の変数を見てみましょう。
T _valueString _errorTextbool _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 _errorTextbool _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