[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,
|
||||
AutovalidateMode? autovalidateMode,
|
||||
ScrollController? scrollController,
|
||||
String? restorationId,
|
||||
}) : assert(initialValue == null || controller == null),
|
||||
assert(textAlign != null),
|
||||
assert(autofocus != null),
|
||||
@ -230,73 +231,78 @@ class TextFormField extends FormField<String> {
|
||||
assert(maxLength == null || maxLength > 0),
|
||||
assert(enableInteractiveSelection != null),
|
||||
super(
|
||||
key: key,
|
||||
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
||||
onSaved: onSaved,
|
||||
validator: validator,
|
||||
enabled: enabled ?? decoration?.enabled ?? true,
|
||||
autovalidateMode: autovalidate
|
||||
? AutovalidateMode.always
|
||||
: (autovalidateMode ?? AutovalidateMode.disabled),
|
||||
builder: (FormFieldState<String> field) {
|
||||
final _TextFormFieldState state = field as _TextFormFieldState;
|
||||
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
||||
void onChangedHandler(String value) {
|
||||
field.didChange(value);
|
||||
if (onChanged != null) {
|
||||
onChanged(value);
|
||||
key: key,
|
||||
restorationId: restorationId,
|
||||
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
||||
onSaved: onSaved,
|
||||
validator: validator,
|
||||
enabled: enabled ?? decoration?.enabled ?? true,
|
||||
autovalidateMode: autovalidate
|
||||
? AutovalidateMode.always
|
||||
: (autovalidateMode ?? AutovalidateMode.disabled),
|
||||
builder: (FormFieldState<String> field) {
|
||||
final _TextFormFieldState state = field as _TextFormFieldState;
|
||||
final InputDecoration effectiveDecoration = (decoration ?? const InputDecoration())
|
||||
.applyDefaults(Theme.of(field.context).inputDecorationTheme);
|
||||
void onChangedHandler(String value) {
|
||||
field.didChange(value);
|
||||
if (onChanged != null) {
|
||||
onChanged(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return TextField(
|
||||
controller: state._effectiveController,
|
||||
focusNode: focusNode,
|
||||
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
style: style,
|
||||
strutStyle: strutStyle,
|
||||
textAlign: textAlign,
|
||||
textAlignVertical: textAlignVertical,
|
||||
textDirection: textDirection,
|
||||
textCapitalization: textCapitalization,
|
||||
autofocus: autofocus,
|
||||
toolbarOptions: toolbarOptions,
|
||||
readOnly: readOnly,
|
||||
showCursor: showCursor,
|
||||
obscuringCharacter: obscuringCharacter,
|
||||
obscureText: obscureText,
|
||||
autocorrect: autocorrect,
|
||||
smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
enableSuggestions: enableSuggestions,
|
||||
maxLengthEnforced: maxLengthEnforced,
|
||||
maxLengthEnforcement: maxLengthEnforcement,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
expands: expands,
|
||||
maxLength: maxLength,
|
||||
onChanged: onChangedHandler,
|
||||
onTap: onTap,
|
||||
onEditingComplete: onEditingComplete,
|
||||
onSubmitted: onFieldSubmitted,
|
||||
inputFormatters: inputFormatters,
|
||||
enabled: enabled ?? decoration?.enabled ?? true,
|
||||
cursorWidth: cursorWidth,
|
||||
cursorHeight: cursorHeight,
|
||||
cursorRadius: cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
scrollPadding: scrollPadding,
|
||||
scrollPhysics: scrollPhysics,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: enableInteractiveSelection,
|
||||
selectionControls: selectionControls,
|
||||
buildCounter: buildCounter,
|
||||
autofillHints: autofillHints,
|
||||
scrollController: scrollController,
|
||||
);
|
||||
},
|
||||
);
|
||||
return UnmanagedRestorationScope(
|
||||
bucket: field.bucket,
|
||||
child: TextField(
|
||||
restorationId: restorationId,
|
||||
controller: state._effectiveController,
|
||||
focusNode: focusNode,
|
||||
decoration: effectiveDecoration.copyWith(errorText: field.errorText),
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
style: style,
|
||||
strutStyle: strutStyle,
|
||||
textAlign: textAlign,
|
||||
textAlignVertical: textAlignVertical,
|
||||
textDirection: textDirection,
|
||||
textCapitalization: textCapitalization,
|
||||
autofocus: autofocus,
|
||||
toolbarOptions: toolbarOptions,
|
||||
readOnly: readOnly,
|
||||
showCursor: showCursor,
|
||||
obscuringCharacter: obscuringCharacter,
|
||||
obscureText: obscureText,
|
||||
autocorrect: autocorrect,
|
||||
smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
|
||||
smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
|
||||
enableSuggestions: enableSuggestions,
|
||||
maxLengthEnforced: maxLengthEnforced,
|
||||
maxLengthEnforcement: maxLengthEnforcement,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
expands: expands,
|
||||
maxLength: maxLength,
|
||||
onChanged: onChangedHandler,
|
||||
onTap: onTap,
|
||||
onEditingComplete: onEditingComplete,
|
||||
onSubmitted: onFieldSubmitted,
|
||||
inputFormatters: inputFormatters,
|
||||
enabled: enabled ?? decoration?.enabled ?? true,
|
||||
cursorWidth: cursorWidth,
|
||||
cursorHeight: cursorHeight,
|
||||
cursorRadius: cursorRadius,
|
||||
cursorColor: cursorColor,
|
||||
scrollPadding: scrollPadding,
|
||||
scrollPhysics: scrollPhysics,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: enableInteractiveSelection,
|
||||
selectionControls: selectionControls,
|
||||
buildCounter: buildCounter,
|
||||
autofillHints: autofillHints,
|
||||
scrollController: scrollController,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// Controls the text being edited.
|
||||
///
|
||||
@ -309,18 +315,44 @@ class TextFormField extends FormField<String> {
|
||||
}
|
||||
|
||||
class _TextFormFieldState extends FormFieldState<String> {
|
||||
TextEditingController? _controller;
|
||||
RestorableTextEditingController? _controller;
|
||||
|
||||
TextEditingController? get _effectiveController => widget.controller ?? _controller;
|
||||
TextEditingController get _effectiveController => widget.controller ?? _controller!.value;
|
||||
|
||||
@override
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller == null) {
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
_createLocalController(widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null);
|
||||
} else {
|
||||
widget.controller!.addListener(_handleControllerChanged);
|
||||
}
|
||||
@ -333,12 +365,17 @@ class _TextFormFieldState extends FormFieldState<String> {
|
||||
oldWidget.controller?.removeListener(_handleControllerChanged);
|
||||
widget.controller?.addListener(_handleControllerChanged);
|
||||
|
||||
if (oldWidget.controller != null && widget.controller == null)
|
||||
_controller = TextEditingController.fromValue(oldWidget.controller!.value);
|
||||
if (oldWidget.controller != null && widget.controller == null) {
|
||||
_createLocalController(oldWidget.controller!.value);
|
||||
}
|
||||
|
||||
if (widget.controller != null) {
|
||||
setValue(widget.controller!.text);
|
||||
if (oldWidget.controller == null)
|
||||
if (oldWidget.controller == null) {
|
||||
unregisterFromRestoration(_controller!);
|
||||
_controller!.dispose();
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -346,6 +383,7 @@ class _TextFormFieldState extends FormFieldState<String> {
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller?.removeListener(_handleControllerChanged);
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -353,15 +391,15 @@ class _TextFormFieldState extends FormFieldState<String> {
|
||||
void didChange(String? value) {
|
||||
super.didChange(value);
|
||||
|
||||
if (_effectiveController!.text != value)
|
||||
_effectiveController!.text = value ?? '';
|
||||
if (_effectiveController.text != value)
|
||||
_effectiveController.text = value ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
// setState will be called in the superclass, so even though state is being
|
||||
// manipulated, no setState call is needed here.
|
||||
_effectiveController!.text = widget.initialValue ?? '';
|
||||
_effectiveController.text = widget.initialValue ?? '';
|
||||
super.reset();
|
||||
}
|
||||
|
||||
@ -373,7 +411,7 @@ class _TextFormFieldState extends FormFieldState<String> {
|
||||
// notifications for changes originating from within this class -- for
|
||||
// example, the reset() method. In such cases, the FormField value will
|
||||
// already have been set.
|
||||
if (_effectiveController!.text != value)
|
||||
didChange(_effectiveController!.text);
|
||||
if (_effectiveController.text != value)
|
||||
didChange(_effectiveController.text);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
import 'framework.dart';
|
||||
import 'navigator.dart';
|
||||
import 'restoration.dart';
|
||||
import 'restoration_properties.dart';
|
||||
import 'will_pop_scope.dart';
|
||||
|
||||
/// An optional container for grouping together multiple form field widgets
|
||||
@ -170,7 +172,7 @@ class FormState extends State<Form> {
|
||||
widget.onChanged?.call();
|
||||
|
||||
_hasInteractedByUser = _fields
|
||||
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser);
|
||||
.any((FormFieldState<dynamic> field) => field._hasInteractedByUser.value);
|
||||
_forceRebuild();
|
||||
}
|
||||
|
||||
@ -331,14 +333,15 @@ class FormField<T> extends StatefulWidget {
|
||||
this.autovalidate = false,
|
||||
this.enabled = true,
|
||||
AutovalidateMode? autovalidateMode,
|
||||
this.restorationId,
|
||||
}) : assert(builder != null),
|
||||
assert(
|
||||
autovalidate == false ||
|
||||
autovalidate == true && autovalidateMode == null,
|
||||
'autovalidate and autovalidateMode should not be used together.',
|
||||
),
|
||||
autovalidateMode = autovalidateMode ??
|
||||
(autovalidate ? AutovalidateMode.always : AutovalidateMode.disabled),
|
||||
autovalidateMode = autovalidateMode ??
|
||||
(autovalidate ? AutovalidateMode.always : AutovalidateMode.disabled),
|
||||
super(key: key);
|
||||
|
||||
/// 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;
|
||||
|
||||
/// 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>> {
|
||||
T? _value;
|
||||
String? _errorText;
|
||||
bool _hasInteractedByUser = false;
|
||||
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;
|
||||
@ -416,10 +433,10 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
/// 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;
|
||||
String? get errorText => _errorText.value;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
@ -440,8 +457,8 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
void reset() {
|
||||
setState(() {
|
||||
_value = widget.initialValue;
|
||||
_hasInteractedByUser = false;
|
||||
_errorText = null;
|
||||
_hasInteractedByUser.value = false;
|
||||
_errorText.value = null;
|
||||
});
|
||||
Form.of(context)?._fieldDidChange();
|
||||
}
|
||||
@ -462,7 +479,7 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
|
||||
void _validate() {
|
||||
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
|
||||
@ -474,7 +491,7 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
void didChange(T? value) {
|
||||
setState(() {
|
||||
_value = value;
|
||||
_hasInteractedByUser = true;
|
||||
_hasInteractedByUser.value = true;
|
||||
});
|
||||
Form.of(context)?._fieldDidChange();
|
||||
}
|
||||
@ -492,9 +509,12 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_value = widget.initialValue;
|
||||
String? get restorationId => widget.restorationId;
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
registerForRestoration(_errorText, 'error_text');
|
||||
registerForRestoration(_hasInteractedByUser, 'has_interacted_by_user');
|
||||
}
|
||||
|
||||
@override
|
||||
@ -511,7 +531,7 @@ class FormFieldState<T> extends State<FormField<T>> {
|
||||
_validate();
|
||||
break;
|
||||
case AutovalidateMode.onUserInteraction:
|
||||
if (_hasInteractedByUser) {
|
||||
if (_hasInteractedByUser.value) {
|
||||
_validate();
|
||||
}
|
||||
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