blob: 5ae7e57916f6158351cb69f4c930641c968b2091 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart';
import 'navigator.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
import 'will_pop_scope.dart';
// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
const Duration _kIOSAnnouncementDelayDuration = Duration(seconds: 1);
// Examples can assume:
// late BuildContext context;
/// An optional container for grouping together multiple form field widgets
/// (e.g. [TextField] widgets).
///
/// Each individual form field should be wrapped in a [FormField] widget, with
/// the [Form] widget as a common ancestor of all of those. Call methods on
/// [FormState] to save, reset, or validate each [FormField] that is a
/// descendant of this [Form]. To obtain the [FormState], you may use [Form.of]
/// with a context whose ancestor is the [Form], or pass a [GlobalKey] to the
/// [Form] constructor and call [GlobalKey.currentState].
///
/// {@tool dartpad}
/// This example shows a [Form] with one [TextFormField] to enter an email
/// address and an [ElevatedButton] to submit the form. A [GlobalKey] is used here
/// to identify the [Form] and validate input.
///
/// ![](https://flutter.github.io/assets-for-api-docs/assets/widgets/form.png)
///
/// ** See code in examples/api/lib/widgets/form/form.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [GlobalKey], a key that is unique across the entire app.
/// * [FormField], a single form field widget that maintains the current state.
/// * [TextFormField], a convenience widget that wraps a [TextField] widget in a [FormField].
class Form extends StatefulWidget {
/// Creates a container for form fields.
///
/// The [child] argument must not be null.
const Form({
super.key,
required this.child,
this.onWillPop,
this.onChanged,
AutovalidateMode? autovalidateMode,
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
/// Returns the [FormState] of the closest [Form] widget which encloses the
/// given context, or null if none is found.
///
/// Typical usage is as follows:
///
/// ```dart
/// FormState? form = Form.maybeOf(context);
/// form?.save();
/// ```
///
/// Calling this method will create a dependency on the closest [Form] in the
/// [context], if there is one.
///
/// See also:
///
/// * [Form.of], which is similar to this method, but asserts if no [Form]
/// ancestor is found.
static FormState? maybeOf(BuildContext context) {
final _FormScope? scope = context.dependOnInheritedWidgetOfExactType<_FormScope>();
return scope?._formState;
}
/// Returns the [FormState] of the closest [Form] widget which encloses the
/// given context.
///
/// Typical usage is as follows:
///
/// ```dart
/// FormState form = Form.of(context);
/// form.save();
/// ```
///
/// If no [Form] ancestor is found, this will assert in debug mode, and throw
/// an exception in release mode.
///
/// Calling this method will create a dependency on the closest [Form] in the
/// [context].
///
/// See also:
///
/// * [Form.maybeOf], which is similar to this method, but returns null if no
/// [Form] ancestor is found.
static FormState of(BuildContext context) {
final FormState? formState = maybeOf(context);
assert(() {
if (formState == null) {
throw FlutterError(
'Form.of() was called with a context that does not contain a Form widget.\n'
'No Form widget ancestor could be found starting from the context that '
'was passed to Form.of(). This can happen because you are using a widget '
'that looks for a Form ancestor, but no such ancestor exists.\n'
'The context used was:\n'
' $context',
);
}
return true;
}());
return formState!;
}
/// The widget below this widget in the tree.
///
/// This is the root of the widget hierarchy that contains this form.
///
/// {@macro flutter.widgets.ProxyWidget.child}
final Widget child;
/// Enables the form to veto attempts by the user to dismiss the [ModalRoute]
/// that contains the form.
///
/// If the callback returns a Future that resolves to false, the form's route
/// will not be popped.
///
/// See also:
///
/// * [WillPopScope], another widget that provides a way to intercept the
/// back button.
final WillPopCallback? onWillPop;
/// Called when one of the form fields changes.
///
/// In addition to this callback being invoked, all the form fields themselves
/// will rebuild.
final VoidCallback? onChanged;
/// Used to enable/disable form fields auto validation and update their error
/// text.
///
/// {@macro flutter.widgets.FormField.autovalidateMode}
final AutovalidateMode autovalidateMode;
@override
FormState createState() => FormState();
}
/// State associated with a [Form] widget.
///
/// A [FormState] object can be used to [save], [reset], and [validate] every
/// [FormField] that is a descendant of the associated [Form].
///
/// Typically obtained via [Form.of].
class FormState extends State<Form> {
int _generation = 0;
bool _hasInteractedByUser = false;
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();
}
void _forceRebuild() {
setState(() {
++_generation;
});
}
void _register(FormFieldState<dynamic> field) {
_fields.add(field);
}
void _unregister(FormFieldState<dynamic> field) {
_fields.remove(field);
}
@override
Widget build(BuildContext context) {
switch (widget.autovalidateMode) {
case AutovalidateMode.always:
_validate();
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser) {
_validate();
}
case AutovalidateMode.disabled:
break;
}
return WillPopScope(
onWillPop: widget.onWillPop,
child: _FormScope(
formState: this,
generation: _generation,
child: widget.child,
),
);
}
/// Saves every [FormField] that is a descendant of this [Form].
void save() {
for (final FormFieldState<dynamic> field in _fields) {
field.save();
}
}
/// Resets every [FormField] that is a descendant of this [Form] back to its
/// [FormField.initialValue].
///
/// The [Form.onChanged] callback will be called.
///
/// If the form's [Form.autovalidateMode] property is [AutovalidateMode.always],
/// the fields will all be revalidated after being reset.
void reset() {
for (final FormFieldState<dynamic> field in _fields) {
field.reset();
}
_hasInteractedByUser = false;
_fieldDidChange();
}
/// Validates every [FormField] that is a descendant of this [Form], and
/// returns true if there are no errors.
///
/// The form will rebuild to report the results.
bool validate() {
_hasInteractedByUser = true;
_forceRebuild();
return _validate();
}
bool _validate() {
bool hasError = false;
String errorMessage = '';
for (final FormFieldState<dynamic> field in _fields) {
hasError = !field.validate() || hasError;
errorMessage += field.errorText ?? '';
}
if(errorMessage.isNotEmpty) {
final TextDirection directionality = Directionality.of(context);
if (defaultTargetPlatform == TargetPlatform.iOS) {
unawaited(Future<void>(() async {
await Future<void>.delayed(_kIOSAnnouncementDelayDuration);
SemanticsService.announce(errorMessage, directionality, assertiveness: Assertiveness.assertive);
}));
} else {
SemanticsService.announce(errorMessage, directionality, assertiveness: Assertiveness.assertive);
}
}
return !hasError;
}
}
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;
@override
bool updateShouldNotify(_FormScope old) => _generation != old._generation;
}
/// Signature for validating a form field.
///
/// Returns an error string to display if the input is invalid, or null
/// otherwise.
///
/// Used by [FormField.validator].
typedef FormFieldValidator<T> = String? Function(T? value);
/// Signature for being notified when a form field changes value.
///
/// Used by [FormField.onSaved].
typedef FormFieldSetter<T> = void Function(T? newValue);
/// Signature for building the widget representing the form field.
///
/// Used by [FormField.builder].
typedef FormFieldBuilder<T> = Widget Function(FormFieldState<T> field);
/// A single form field.
///
/// This widget maintains the current state of the form field, so that updates
/// and validation errors are visually reflected in the UI.
///
/// When used inside a [Form], you can use methods on [FormState] to query or
/// manipulate the form data as a whole. For example, calling [FormState.save]
/// will invoke each [FormField]'s [onSaved] callback in turn.
///
/// Use a [GlobalKey] with [FormField] if you want to retrieve its current
/// state, for example if you want one form field to depend on another.
///
/// A [Form] ancestor is not required. The [Form] allows one to
/// save, reset, or validate multiple fields at once. To use without a [Form],
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
/// save or reset the form field.
///
/// See also:
///
/// * [Form], which is the widget that aggregates the form fields.
/// * [TextField], which is a commonly used form field for entering text.
class FormField<T> extends StatefulWidget {
/// Creates a single form field.
///
/// The [builder] argument must not be null.
const FormField({
super.key,
required this.builder,
this.onSaved,
this.validator,
this.initialValue,
this.enabled = true,
AutovalidateMode? autovalidateMode,
this.restorationId,
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
/// An optional method to call with the final value when the form is saved via
/// [FormState.save].
final FormFieldSetter<T>? onSaved;
/// An optional method that validates an input. Returns an error string to
/// display if the input is invalid, or null otherwise.
///
/// The returned value is exposed by the [FormFieldState.errorText] property.
/// The [TextFormField] uses this to override the [InputDecoration.errorText]
/// value.
///
/// Alternating between error and normal state can cause the height of the
/// [TextFormField] to change if no other subtext decoration is set on the
/// field. To create a field whose height is fixed regardless of whether or
/// not an error is displayed, either wrap the [TextFormField] in a fixed
/// height parent like [SizedBox], or set the [InputDecoration.helperText]
/// parameter to a space.
final FormFieldValidator<T>? validator;
/// Function that returns the widget representing this form field. It is
/// passed the form field state as input, containing the current value and
/// validation state of this field.
final FormFieldBuilder<T> builder;
/// An optional value to initialize the form field to, or null otherwise.
final T? initialValue;
/// Whether the form is able to receive user input.
///
/// Defaults to true. If [autovalidateMode] is not [AutovalidateMode.disabled],
/// the field will be auto validated. Likewise, if this field is false, the widget
/// will not be validated regardless of [autovalidateMode].
final bool enabled;
/// Used to enable/disable this form field auto validation and update its
/// error text.
///
/// {@template flutter.widgets.FormField.autovalidateMode}
/// If [AutovalidateMode.onUserInteraction], this FormField will only
/// auto-validate after its content changes. If [AutovalidateMode.always], it
/// will auto-validate even without user interaction. If
/// [AutovalidateMode.disabled], auto-validation will be disabled.
///
/// Defaults to [AutovalidateMode.disabled], cannot be null.
/// {@endtemplate}
final AutovalidateMode autovalidateMode;
/// Restoration ID to save and restore the state of the form field.
///
/// Setting the restoration ID to a non-null value results in whether or not
/// the form field validation persists.
///
/// The state of this widget is persisted in a [RestorationBucket] claimed
/// from the surrounding [RestorationScope] using the provided restoration ID.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationId;
@override
FormFieldState<T> createState() => FormFieldState<T>();
}
/// The current state of a [FormField]. Passed to the [FormFieldBuilder] method
/// for use in constructing the form field's widget.
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
late T? _value = widget.initialValue;
final RestorableStringN _errorText = RestorableStringN(null);
final RestorableBool _hasInteractedByUser = RestorableBool(false);
/// The current value of the form field.
T? get value => _value;
/// The current validation error returned by the [FormField.validator]
/// callback, or null if no errors have been triggered. This only updates when
/// [validate] is called.
String? get errorText => _errorText.value;
/// True if this field has any validation errors.
bool get hasError => _errorText.value != null;
/// True if the current value is valid.
///
/// This will not set [errorText] or [hasError] and it will not update
/// error display.
///
/// See also:
///
/// * [validate], which may update [errorText] and [hasError].
bool get isValid => widget.validator?.call(_value) == null;
/// Calls the [FormField]'s onSaved method with the current value.
void save() {
widget.onSaved?.call(value);
}
/// Resets the field to its initial value.
void reset() {
setState(() {
_value = widget.initialValue;
_hasInteractedByUser.value = false;
_errorText.value = null;
});
Form.maybeOf(context)?._fieldDidChange();
}
/// Calls [FormField.validator] to set the [errorText]. Returns true if there
/// were no errors.
///
/// See also:
///
/// * [isValid], which passively gets the validity without setting
/// [errorText] or [hasError].
bool validate() {
setState(() {
_validate();
});
return !hasError;
}
void _validate() {
if (widget.validator != null) {
_errorText.value = widget.validator!(_value);
}
}
/// Updates this field's state to the new value. Useful for responding to
/// child widget changes, e.g. [Slider]'s [Slider.onChanged] argument.
///
/// Triggers the [Form.onChanged] callback and, if [Form.autovalidateMode] is
/// [AutovalidateMode.always] or [AutovalidateMode.onUserInteraction],
/// revalidates all the fields of the form.
void didChange(T? value) {
setState(() {
_value = value;
_hasInteractedByUser.value = true;
});
Form.maybeOf(context)?._fieldDidChange();
}
/// Sets the value associated with this form field.
///
/// This method should only be called by subclasses that need to update
/// the form field value due to state changes identified during the widget
/// build phase, when calling `setState` is prohibited. In all other cases,
/// the value should be set by a call to [didChange], which ensures that
/// `setState` is called.
@protected
// ignore: use_setters_to_change_properties, (API predates enforcing the lint)
void setValue(T? value) {
_value = value;
}
@override
String? get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_errorText, 'error_text');
registerForRestoration(_hasInteractedByUser, 'has_interacted_by_user');
}
@override
void deactivate() {
Form.maybeOf(context)?._unregister(this);
super.deactivate();
}
@override
Widget build(BuildContext context) {
if (widget.enabled) {
switch (widget.autovalidateMode) {
case AutovalidateMode.always:
_validate();
case AutovalidateMode.onUserInteraction:
if (_hasInteractedByUser.value) {
_validate();
}
case AutovalidateMode.disabled:
break;
}
}
Form.maybeOf(context)?._register(this);
return widget.builder(this);
}
}
/// Used to configure the auto validation of [FormField] and [Form] widgets.
enum AutovalidateMode {
/// No auto validation will occur.
disabled,
/// Used to auto-validate [Form] and [FormField] even without user interaction.
always,
/// Used to auto-validate [Form] and [FormField] only after each user
/// interaction.
onUserInteraction,
}