[State Restoration] Restorable FormField and TextFormField (#78835)
* Restorable FormField and TextFormField
This commit is contained in:
parent
10d5ec875d
commit
3eeadc28f3
@ -195,6 +195,7 @@ class TextFormField extends FormField<String> {
|
|||||||
Iterable<String>? autofillHints,
|
Iterable<String>? autofillHints,
|
||||||
AutovalidateMode? autovalidateMode,
|
AutovalidateMode? autovalidateMode,
|
||||||
ScrollController? scrollController,
|
ScrollController? scrollController,
|
||||||
|
String? restorationId,
|
||||||
}) : assert(initialValue == null || controller == null),
|
}) : assert(initialValue == null || controller == null),
|
||||||
assert(textAlign != null),
|
assert(textAlign != null),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
@ -230,73 +231,78 @@ class TextFormField extends FormField<String> {
|
|||||||
assert(maxLength == null || maxLength > 0),
|
assert(maxLength == null || maxLength > 0),
|
||||||
assert(enableInteractiveSelection != null),
|
assert(enableInteractiveSelection != null),
|
||||||
super(
|
super(
|
||||||
key: key,
|
key: key,
|
||||||
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
restorationId: restorationId,
|
||||||
onSaved: onSaved,
|
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
||||||
validator: validator,
|
onSaved: onSaved,
|
||||||
enabled: enabled ?? decoration?.enabled ?? true,
|
validator: validator,
|
||||||
autovalidateMode: autovalidate
|
enabled: enabled ?? decoration?.enabled ?? true,
|
||||||
? AutovalidateMode.always
|
autovalidateMode: autovalidate
|
||||||
: (autovalidateMode ?? AutovalidateMode.disabled),
|
? AutovalidateMode.always
|
||||||
builder: (FormFieldState<String> field) {
|
: (autovalidateMode ?? AutovalidateMode.disabled),
|
||||||
final _TextFormFieldState state = field as _TextFormFieldState;
|
builder: (FormFieldState<String> field) {
|
||||||
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
final _TextFormFieldState state = field as _TextFormFieldState;
|
||||||
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||||
void onChangedHandler(String value) {
|
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
||||||
field.didChange(value);
|
void onChangedHandler(String value) {
|
||||||
if (onChanged != null) {
|
field.didChange(value);
|
||||||
onChanged(value);
|
if (onChanged != null) {
|
||||||
|
onChanged(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return UnmanagedRestorationScope(
|
||||||
return TextField(
|
bucket: field.bucket,
|
||||||
controller: state._effectiveController,
|
child: TextField(
|
||||||
focusNode: focusNode,
|
restorationId: restorationId,
|
||||||
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
controller: state._effectiveController,
|
||||||
keyboardType: keyboardType,
|
focusNode: focusNode,
|
||||||
textInputAction: textInputAction,
|
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
||||||
style: style,
|
keyboardType: keyboardType,
|
||||||
strutStyle: strutStyle,
|
textInputAction: textInputAction,
|
||||||
textAlign: textAlign,
|
style: style,
|
||||||
textAlignVertical: textAlignVertical,
|
strutStyle: strutStyle,
|
||||||
textDirection: textDirection,
|
textAlign: textAlign,
|
||||||
textCapitalization: textCapitalization,
|
textAlignVertical: textAlignVertical,
|
||||||
autofocus: autofocus,
|
textDirection: textDirection,
|
||||||
toolbarOptions: toolbarOptions,
|
textCapitalization: textCapitalization,
|
||||||
readOnly: readOnly,
|
autofocus: autofocus,
|
||||||
showCursor: showCursor,
|
toolbarOptions: toolbarOptions,
|
||||||
obscuringCharacter: obscuringCharacter,
|
readOnly: readOnly,
|
||||||
obscureText: obscureText,
|
showCursor: showCursor,
|
||||||
autocorrect: autocorrect,
|
obscuringCharacter: obscuringCharacter,
|
||||||
smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
obscureText: obscureText,
|
||||||
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
autocorrect: autocorrect,
|
||||||
enableSuggestions: enableSuggestions,
|
smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||||
maxLengthEnforced: maxLengthEnforced,
|
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||||
maxLengthEnforcement: maxLengthEnforcement,
|
enableSuggestions: enableSuggestions,
|
||||||
maxLines: maxLines,
|
maxLengthEnforced: maxLengthEnforced,
|
||||||
minLines: minLines,
|
maxLengthEnforcement: maxLengthEnforcement,
|
||||||
expands: expands,
|
maxLines: maxLines,
|
||||||
maxLength: maxLength,
|
minLines: minLines,
|
||||||
onChanged: onChangedHandler,
|
expands: expands,
|
||||||
onTap: onTap,
|
maxLength: maxLength,
|
||||||
onEditingComplete: onEditingComplete,
|
onChanged: onChangedHandler,
|
||||||
onSubmitted: onFieldSubmitted,
|
onTap: onTap,
|
||||||
inputFormatters: inputFormatters,
|
onEditingComplete: onEditingComplete,
|
||||||
enabled: enabled ?? decoration?.enabled ?? true,
|
onSubmitted: onFieldSubmitted,
|
||||||
cursorWidth: cursorWidth,
|
inputFormatters: inputFormatters,
|
||||||
cursorHeight: cursorHeight,
|
enabled: enabled ?? decoration?.enabled ?? true,
|
||||||
cursorRadius: cursorRadius,
|
cursorWidth: cursorWidth,
|
||||||
cursorColor: cursorColor,
|
cursorHeight: cursorHeight,
|
||||||
scrollPadding: scrollPadding,
|
cursorRadius: cursorRadius,
|
||||||
scrollPhysics: scrollPhysics,
|
cursorColor: cursorColor,
|
||||||
keyboardAppearance: keyboardAppearance,
|
scrollPadding: scrollPadding,
|
||||||
enableInteractiveSelection: enableInteractiveSelection,
|
scrollPhysics: scrollPhysics,
|
||||||
selectionControls: selectionControls,
|
keyboardAppearance: keyboardAppearance,
|
||||||
buildCounter: buildCounter,
|
enableInteractiveSelection: enableInteractiveSelection,
|
||||||
autofillHints: autofillHints,
|
selectionControls: selectionControls,
|
||||||
scrollController: scrollController,
|
buildCounter: buildCounter,
|
||||||
);
|
autofillHints: autofillHints,
|
||||||
},
|
scrollController: scrollController,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/// Controls the text being edited.
|
/// Controls the text being edited.
|
||||||
///
|
///
|
||||||
@ -309,18 +315,44 @@ class TextFormField extends FormField<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _TextFormFieldState extends FormFieldState<String> {
|
class _TextFormFieldState extends FormFieldState<String> {
|
||||||
TextEditingController? _controller;
|
RestorableTextEditingController? _controller;
|
||||||
|
|
||||||
TextEditingController? get _effectiveController => widget.controller ?? _controller;
|
TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextFormField get widget => super.widget as TextFormField;
|
TextFormField get widget => super.widget as TextFormField;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
super.restoreState(oldBucket, initialRestore);
|
||||||
|
if (_controller != null) {
|
||||||
|
_registerController();
|
||||||
|
}
|
||||||
|
// Make sure to update the internal [FormFieldState] value to sync up with
|
||||||
|
// text editing controller value.
|
||||||
|
setValue(_effectiveController.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerController() {
|
||||||
|
assert(_controller != null);
|
||||||
|
registerForRestoration(_controller!, 'controller');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createLocalController([TextEditingValue? value]) {
|
||||||
|
assert(_controller == null);
|
||||||
|
_controller = value == null
|
||||||
|
? RestorableTextEditingController()
|
||||||
|
: RestorableTextEditingController.fromValue(value);
|
||||||
|
if (!restorePending) {
|
||||||
|
_registerController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
if (widget.controller == null) {
|
if (widget.controller == null) {
|
||||||
_controller = TextEditingController(text: widget.initialValue);
|
_createLocalController(widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null);
|
||||||
} else {
|
} else {
|
||||||
widget.controller!.addListener(_handleControllerChanged);
|
widget.controller!.addListener(_handleControllerChanged);
|
||||||
}
|
}
|
||||||
@ -333,12 +365,17 @@ class _TextFormFieldState extends FormFieldState<String> {
|
|||||||
oldWidget.controller?.removeListener(_handleControllerChanged);
|
oldWidget.controller?.removeListener(_handleControllerChanged);
|
||||||
widget.controller?.addListener(_handleControllerChanged);
|
widget.controller?.addListener(_handleControllerChanged);
|
||||||
|
|
||||||
if (oldWidget.controller != null && widget.controller == null)
|
if (oldWidget.controller != null && widget.controller == null) {
|
||||||
_controller = TextEditingController.fromValue(oldWidget.controller!.value);
|
_createLocalController(oldWidget.controller!.value);
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.controller != null) {
|
if (widget.controller != null) {
|
||||||
setValue(widget.controller!.text);
|
setValue(widget.controller!.text);
|
||||||
if (oldWidget.controller == null)
|
if (oldWidget.controller == null) {
|
||||||
|
unregisterFromRestoration(_controller!);
|
||||||
|
_controller!.dispose();
|
||||||
_controller = null;
|
_controller = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -346,6 +383,7 @@ class _TextFormFieldState extends FormFieldState<String> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.controller?.removeListener(_handleControllerChanged);
|
widget.controller?.removeListener(_handleControllerChanged);
|
||||||
|
_controller?.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,15 +391,15 @@ class _TextFormFieldState extends FormFieldState<String> {
|
|||||||
void didChange(String? value) {
|
void didChange(String? value) {
|
||||||
super.didChange(value);
|
super.didChange(value);
|
||||||
|
|
||||||
if (_effectiveController!.text != value)
|
if (_effectiveController.text != value)
|
||||||
_effectiveController!.text = value ?? '';
|
_effectiveController.text = value ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void reset() {
|
void reset() {
|
||||||
// setState will be called in the superclass, so even though state is being
|
// setState will be called in the superclass, so even though state is being
|
||||||
// manipulated, no setState call is needed here.
|
// manipulated, no setState call is needed here.
|
||||||
_effectiveController!.text = widget.initialValue ?? '';
|
_effectiveController.text = widget.initialValue ?? '';
|
||||||
super.reset();
|
super.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -373,7 +411,7 @@ class _TextFormFieldState extends FormFieldState<String> {
|
|||||||
// notifications for changes originating from within this class -- for
|
// notifications for changes originating from within this class -- for
|
||||||
// example, the reset() method. In such cases, the FormField value will
|
// example, the reset() method. In such cases, the FormField value will
|
||||||
// already have been set.
|
// already have been set.
|
||||||
if (_effectiveController!.text != value)
|
if (_effectiveController.text != value)
|
||||||
didChange(_effectiveController!.text);
|
didChange(_effectiveController.text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'navigator.dart';
|
import 'navigator.dart';
|
||||||
|
import 'restoration.dart';
|
||||||
|
import 'restoration_properties.dart';
|
||||||
import 'will_pop_scope.dart';
|
import 'will_pop_scope.dart';
|
||||||
|
|
||||||
/// An optional container for grouping together multiple form field widgets
|
/// An optional container for grouping together multiple form field widgets
|
||||||
@ -170,7 +172,7 @@ class FormState extends State<Form> {
|
|||||||
widget.onChanged?.call();
|
widget.onChanged?.call();
|
||||||
|
|
||||||
_hasInteractedByUser = _fields
|
_hasInteractedByUser = _fields
|
||||||
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser);
|
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser.value);
|
||||||
_forceRebuild();
|
_forceRebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,14 +333,15 @@ class FormField<T> extends StatefulWidget {
|
|||||||
this.autovalidate = false,
|
this.autovalidate = false,
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
AutovalidateMode? autovalidateMode,
|
AutovalidateMode? autovalidateMode,
|
||||||
|
this.restorationId,
|
||||||
}) : assert(builder != null),
|
}) : assert(builder != null),
|
||||||
assert(
|
assert(
|
||||||
autovalidate == false ||
|
autovalidate == false ||
|
||||||
autovalidate == true && autovalidateMode == null,
|
autovalidate == true && autovalidateMode == null,
|
||||||
'autovalidate and autovalidateMode should not be used together.',
|
'autovalidate and autovalidateMode should not be used together.',
|
||||||
),
|
),
|
||||||
autovalidateMode = autovalidateMode ??
|
autovalidateMode = autovalidateMode ??
|
||||||
(autovalidate ? AutovalidateMode.always : AutovalidateMode.disabled),
|
(autovalidate ? AutovalidateMode.always : AutovalidateMode.disabled),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
/// An optional method to call with the final value when the form is saved via
|
/// An optional method to call with the final value when the form is saved via
|
||||||
@ -399,16 +402,30 @@ class FormField<T> extends StatefulWidget {
|
|||||||
)
|
)
|
||||||
final bool autovalidate;
|
final bool autovalidate;
|
||||||
|
|
||||||
|
/// 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
|
@override
|
||||||
FormFieldState<T> createState() => FormFieldState<T>();
|
FormFieldState<T> createState() => FormFieldState<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current state of a [FormField]. Passed to the [FormFieldBuilder] method
|
/// The current state of a [FormField]. Passed to the [FormFieldBuilder] method
|
||||||
/// for use in constructing the form field's widget.
|
/// for use in constructing the form field's widget.
|
||||||
class FormFieldState<T> extends State<FormField<T>> {
|
class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||||
T? _value;
|
late T? _value = widget.initialValue;
|
||||||
String? _errorText;
|
final RestorableStringN _errorText = RestorableStringN(null);
|
||||||
bool _hasInteractedByUser = false;
|
final RestorableBool _hasInteractedByUser = RestorableBool(false);
|
||||||
|
|
||||||
/// The current value of the form field.
|
/// The current value of the form field.
|
||||||
T? get value => _value;
|
T? get value => _value;
|
||||||
@ -416,10 +433,10 @@ class FormFieldState<T> extends State<FormField<T>> {
|
|||||||
/// The current validation error returned by the [FormField.validator]
|
/// The current validation error returned by the [FormField.validator]
|
||||||
/// callback, or null if no errors have been triggered. This only updates when
|
/// callback, or null if no errors have been triggered. This only updates when
|
||||||
/// [validate] is called.
|
/// [validate] is called.
|
||||||
String? get errorText => _errorText;
|
String? get errorText => _errorText.value;
|
||||||
|
|
||||||
/// True if this field has any validation errors.
|
/// True if this field has any validation errors.
|
||||||
bool get hasError => _errorText != null;
|
bool get hasError => _errorText.value != null;
|
||||||
|
|
||||||
/// True if the current value is valid.
|
/// True if the current value is valid.
|
||||||
///
|
///
|
||||||
@ -440,8 +457,8 @@ class FormFieldState<T> extends State<FormField<T>> {
|
|||||||
void reset() {
|
void reset() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_value = widget.initialValue;
|
_value = widget.initialValue;
|
||||||
_hasInteractedByUser = false;
|
_hasInteractedByUser.value = false;
|
||||||
_errorText = null;
|
_errorText.value = null;
|
||||||
});
|
});
|
||||||
Form.of(context)?._fieldDidChange();
|
Form.of(context)?._fieldDidChange();
|
||||||
}
|
}
|
||||||
@ -462,7 +479,7 @@ class FormFieldState<T> extends State<FormField<T>> {
|
|||||||
|
|
||||||
void _validate() {
|
void _validate() {
|
||||||
if (widget.validator != null)
|
if (widget.validator != null)
|
||||||
_errorText = widget.validator!(_value);
|
_errorText.value = widget.validator!(_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates this field's state to the new value. Useful for responding to
|
/// Updates this field's state to the new value. Useful for responding to
|
||||||
@ -474,7 +491,7 @@ class FormFieldState<T> extends State<FormField<T>> {
|
|||||||
void didChange(T? value) {
|
void didChange(T? value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_value = value;
|
_value = value;
|
||||||
_hasInteractedByUser = true;
|
_hasInteractedByUser.value = true;
|
||||||
});
|
});
|
||||||
Form.of(context)?._fieldDidChange();
|
Form.of(context)?._fieldDidChange();
|
||||||
}
|
}
|
||||||
@ -492,9 +509,12 @@ class FormFieldState<T> extends State<FormField<T>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
String? get restorationId => widget.restorationId;
|
||||||
super.initState();
|
|
||||||
_value = widget.initialValue;
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
registerForRestoration(_errorText, 'error_text');
|
||||||
|
registerForRestoration(_hasInteractedByUser, 'has_interacted_by_user');
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -511,7 +531,7 @@ class FormFieldState<T> extends State<FormField<T>> {
|
|||||||
_validate();
|
_validate();
|
||||||
break;
|
break;
|
||||||
case AutovalidateMode.onUserInteraction:
|
case AutovalidateMode.onUserInteraction:
|
||||||
if (_hasInteractedByUser) {
|
if (_hasInteractedByUser.value) {
|
||||||
_validate();
|
_validate();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -0,0 +1,251 @@
|
|||||||
|
// 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 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
const String text = 'Hello World! How are you? Life is good!';
|
||||||
|
const String alternativeText = 'Everything is awesome!!';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('TextField restoration', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
restorationScopeId: 'app',
|
||||||
|
home: TestWidget(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await restoreAndVerify(tester);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TextField restoration with external controller', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
restorationScopeId: 'root',
|
||||||
|
home: TestWidget(
|
||||||
|
useExternal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await restoreAndVerify(tester);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('State restoration (No Form ancestor) - onUserInteraction error text validation', (WidgetTester tester) async {
|
||||||
|
String? errorText(String? value) => '$value/error';
|
||||||
|
late GlobalKey<FormFieldState<String>> formState;
|
||||||
|
|
||||||
|
Widget builder() {
|
||||||
|
return MaterialApp(
|
||||||
|
restorationScopeId: 'app',
|
||||||
|
home: MediaQuery(
|
||||||
|
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Center(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter state) {
|
||||||
|
formState = GlobalKey<FormFieldState<String>>();
|
||||||
|
return Material(
|
||||||
|
child: TextFormField(
|
||||||
|
key: formState,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
restorationId: 'text_form_field',
|
||||||
|
initialValue: 'foo',
|
||||||
|
validator: errorText,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(builder());
|
||||||
|
|
||||||
|
// No error text is visible yet.
|
||||||
|
expect(find.text(errorText('foo')!), findsNothing);
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextFormField), 'bar');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(errorText('bar')!), findsOneWidget);
|
||||||
|
|
||||||
|
final TestRestorationData data = await tester.getRestorationData();
|
||||||
|
await tester.restartAndRestore();
|
||||||
|
// Error text should be present after restart and restore.
|
||||||
|
expect(find.text(errorText('bar')!), findsOneWidget);
|
||||||
|
|
||||||
|
// Resetting the form state should remove the error text.
|
||||||
|
formState.currentState!.reset();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(errorText('bar')!), findsNothing);
|
||||||
|
await tester.restartAndRestore();
|
||||||
|
// Error text should still be removed after restart and restore.
|
||||||
|
expect(find.text(errorText('bar')!), findsNothing);
|
||||||
|
|
||||||
|
await tester.restoreFrom(data);
|
||||||
|
expect(find.text(errorText('bar')!), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('State Restoration (No Form ancestor) - validator sets the error text only when validate is called', (WidgetTester tester) async {
|
||||||
|
String? errorText(String? value) => '$value/error';
|
||||||
|
late GlobalKey<FormFieldState<String>> formState;
|
||||||
|
|
||||||
|
Widget builder(AutovalidateMode mode) {
|
||||||
|
return MaterialApp(
|
||||||
|
restorationScopeId: 'app',
|
||||||
|
home: MediaQuery(
|
||||||
|
data: const MediaQueryData(devicePixelRatio: 1.0),
|
||||||
|
child: Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: Center(
|
||||||
|
child: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter state) {
|
||||||
|
formState = GlobalKey<FormFieldState<String>>();
|
||||||
|
return Material(
|
||||||
|
child: TextFormField(
|
||||||
|
key: formState,
|
||||||
|
restorationId: 'form_field',
|
||||||
|
autovalidateMode: mode,
|
||||||
|
initialValue: 'foo',
|
||||||
|
validator: errorText,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start off not autovalidating.
|
||||||
|
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
||||||
|
|
||||||
|
Future<void> checkErrorText(String testValue) async {
|
||||||
|
formState.currentState!.reset();
|
||||||
|
await tester.pumpWidget(builder(AutovalidateMode.disabled));
|
||||||
|
await tester.enterText(find.byType(TextFormField), testValue);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// We have to manually validate if we're not autovalidating.
|
||||||
|
expect(find.text(errorText(testValue)!), findsNothing);
|
||||||
|
formState.currentState!.validate();
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text(errorText(testValue)!), findsOneWidget);
|
||||||
|
final TestRestorationData data = await tester.getRestorationData();
|
||||||
|
await tester.restartAndRestore();
|
||||||
|
// Error text should be present after restart and restore.
|
||||||
|
expect(find.text(errorText(testValue)!), findsOneWidget);
|
||||||
|
|
||||||
|
formState.currentState!.reset();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text(errorText(testValue)!), findsNothing);
|
||||||
|
|
||||||
|
await tester.restoreFrom(data);
|
||||||
|
expect(find.text(errorText(testValue)!), findsOneWidget);
|
||||||
|
|
||||||
|
// Try again with autovalidation. Should validate immediately.
|
||||||
|
formState.currentState!.reset();
|
||||||
|
await tester.pumpWidget(builder(AutovalidateMode.always));
|
||||||
|
await tester.enterText(find.byType(TextFormField), testValue);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text(errorText(testValue)!), findsOneWidget);
|
||||||
|
await tester.restartAndRestore();
|
||||||
|
// Error text should be present after restart and restore.
|
||||||
|
expect(find.text(errorText(testValue)!), findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkErrorText('Test');
|
||||||
|
await checkErrorText('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> restoreAndVerify(WidgetTester tester) async {
|
||||||
|
expect(find.text(text), findsNothing);
|
||||||
|
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextFormField), text);
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
|
||||||
|
|
||||||
|
await tester.drag(find.byType(Scrollable), const Offset(0, -80));
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
|
||||||
|
expect(find.text(text), findsOneWidget);
|
||||||
|
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||||
|
|
||||||
|
await tester.restartAndRestore();
|
||||||
|
|
||||||
|
expect(find.text(text), findsOneWidget);
|
||||||
|
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||||
|
|
||||||
|
final TestRestorationData data = await tester.getRestorationData();
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextFormField), alternativeText);
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
await tester.drag(find.byType(Scrollable), const Offset(0, 80));
|
||||||
|
await skipPastScrollingAnimation(tester);
|
||||||
|
|
||||||
|
expect(find.text(text), findsNothing);
|
||||||
|
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, isNot(60));
|
||||||
|
|
||||||
|
await tester.restoreFrom(data);
|
||||||
|
|
||||||
|
expect(find.text(text), findsOneWidget);
|
||||||
|
expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestWidget extends StatefulWidget {
|
||||||
|
const TestWidget({Key? key, this.useExternal = false}) : super(key: key);
|
||||||
|
|
||||||
|
final bool useExternal;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TestWidgetState createState() => TestWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestWidgetState extends State<TestWidget> with RestorationMixin {
|
||||||
|
final RestorableTextEditingController controller = RestorableTextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get restorationId => 'widget';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||||
|
registerForRestoration(controller, 'controller');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Material(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 50,
|
||||||
|
child: TextFormField(
|
||||||
|
restorationId: 'text',
|
||||||
|
maxLines: 3,
|
||||||
|
controller: widget.useExternal ? controller.value : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user