Unverified Commit 37ddad61 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Make textfields restorable (#63401)

parent 03dcd1bd
...@@ -273,6 +273,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -273,6 +273,7 @@ class CupertinoTextField extends StatefulWidget {
this.scrollController, this.scrollController,
this.scrollPhysics, this.scrollPhysics,
this.autofillHints, this.autofillHints,
this.restorationId,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(readOnly != null), assert(readOnly != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -600,6 +601,9 @@ class CupertinoTextField extends StatefulWidget { ...@@ -600,6 +601,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.services.autofill.autofillHints} /// {@macro flutter.services.autofill.autofillHints}
final Iterable<String> autofillHints; final Iterable<String> autofillHints;
/// {@macro flutter.material.textfield.restorationId}
final String restorationId;
@override @override
_CupertinoTextFieldState createState() => _CupertinoTextFieldState(); _CupertinoTextFieldState createState() => _CupertinoTextFieldState();
...@@ -641,11 +645,11 @@ class CupertinoTextField extends StatefulWidget { ...@@ -641,11 +645,11 @@ class CupertinoTextField extends StatefulWidget {
} }
} }
class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
final GlobalKey _clearGlobalKey = GlobalKey(); final GlobalKey _clearGlobalKey = GlobalKey();
TextEditingController _controller; RestorableTextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller; TextEditingController get _effectiveController => widget.controller ?? _controller.value;
FocusNode _focusNode; FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
...@@ -670,8 +674,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -670,8 +674,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
super.initState(); super.initState();
_selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this); _selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null) { if (widget.controller == null) {
_controller = TextEditingController(); _createLocalController();
_controller.addListener(updateKeepAlive);
} }
} }
...@@ -679,9 +682,10 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -679,9 +682,10 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
void didUpdateWidget(CupertinoTextField oldWidget) { void didUpdateWidget(CupertinoTextField oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null) { if (widget.controller == null && oldWidget.controller != null) {
_controller = TextEditingController.fromValue(oldWidget.controller.value); _createLocalController(oldWidget.controller.value);
_controller.addListener(updateKeepAlive);
} else if (widget.controller != null && oldWidget.controller == null) { } else if (widget.controller != null && oldWidget.controller == null) {
unregisterFromRestoration(_controller);
_controller.dispose();
_controller = null; _controller = null;
} }
final bool isEnabled = widget.enabled ?? true; final bool isEnabled = widget.enabled ?? true;
...@@ -691,10 +695,36 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -691,10 +695,36 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
} }
} }
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
if (_controller != null) {
_registerController();
}
}
void _registerController() {
assert(_controller != null);
registerForRestoration(_controller, 'controller');
_controller.value.addListener(updateKeepAlive);
}
void _createLocalController([TextEditingValue value]) {
assert(_controller == null);
_controller = value == null
? RestorableTextEditingController()
: RestorableTextEditingController.fromValue(value);
if (!restorePending) {
_registerController();
}
}
@override
String get restorationId => widget.restorationId;
@override @override
void dispose() { void dispose() {
_focusNode?.dispose(); _focusNode?.dispose();
_controller?.removeListener(updateKeepAlive); _controller?.dispose();
super.dispose(); super.dispose();
} }
...@@ -736,7 +766,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -736,7 +766,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
} }
@override @override
bool get wantKeepAlive => _controller?.text?.isNotEmpty == true; bool get wantKeepAlive => _controller?.value?.text?.isNotEmpty == true;
bool _shouldShowAttachment({ bool _shouldShowAttachment({
OverlayVisibilityMode attachment, OverlayVisibilityMode attachment,
...@@ -927,57 +957,61 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -927,57 +957,61 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
final Widget paddedEditable = Padding( final Widget paddedEditable = Padding(
padding: widget.padding, padding: widget.padding,
child: RepaintBoundary( child: RepaintBoundary(
child: EditableText( child: UnmanagedRestorationScope(
key: editableTextKey, bucket: bucket,
controller: controller, child: EditableText(
readOnly: widget.readOnly, key: editableTextKey,
toolbarOptions: widget.toolbarOptions, controller: controller,
showCursor: widget.showCursor, readOnly: widget.readOnly,
showSelectionHandles: _showSelectionHandles, toolbarOptions: widget.toolbarOptions,
focusNode: _effectiveFocusNode, showCursor: widget.showCursor,
keyboardType: widget.keyboardType, showSelectionHandles: _showSelectionHandles,
textInputAction: widget.textInputAction, focusNode: _effectiveFocusNode,
textCapitalization: widget.textCapitalization, keyboardType: widget.keyboardType,
style: textStyle, textInputAction: widget.textInputAction,
strutStyle: widget.strutStyle, textCapitalization: widget.textCapitalization,
textAlign: widget.textAlign, style: textStyle,
autofocus: widget.autofocus, strutStyle: widget.strutStyle,
obscuringCharacter: widget.obscuringCharacter, textAlign: widget.textAlign,
obscureText: widget.obscureText, autofocus: widget.autofocus,
autocorrect: widget.autocorrect, obscuringCharacter: widget.obscuringCharacter,
smartDashesType: widget.smartDashesType, obscureText: widget.obscureText,
smartQuotesType: widget.smartQuotesType, autocorrect: widget.autocorrect,
enableSuggestions: widget.enableSuggestions, smartDashesType: widget.smartDashesType,
maxLines: widget.maxLines, smartQuotesType: widget.smartQuotesType,
minLines: widget.minLines, enableSuggestions: widget.enableSuggestions,
expands: widget.expands, maxLines: widget.maxLines,
selectionColor: selectionColor, minLines: widget.minLines,
selectionControls: widget.selectionEnabled expands: widget.expands,
? cupertinoTextSelectionControls : null, selectionColor: selectionColor,
onChanged: widget.onChanged, selectionControls: widget.selectionEnabled
onSelectionChanged: _handleSelectionChanged, ? cupertinoTextSelectionControls : null,
onEditingComplete: widget.onEditingComplete, onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted, onSelectionChanged: _handleSelectionChanged,
inputFormatters: formatters, onEditingComplete: widget.onEditingComplete,
rendererIgnoresPointer: true, onSubmitted: widget.onSubmitted,
cursorWidth: widget.cursorWidth, inputFormatters: formatters,
cursorHeight: widget.cursorHeight, rendererIgnoresPointer: true,
cursorRadius: widget.cursorRadius, cursorWidth: widget.cursorWidth,
cursorColor: cursorColor, cursorHeight: widget.cursorHeight,
cursorOpacityAnimates: true, cursorRadius: widget.cursorRadius,
cursorOffset: cursorOffset, cursorColor: cursorColor,
paintCursorAboveText: true, cursorOpacityAnimates: true,
autocorrectionTextRectColor: selectionColor, cursorOffset: cursorOffset,
backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context), paintCursorAboveText: true,
selectionHeightStyle: widget.selectionHeightStyle, autocorrectionTextRectColor: selectionColor,
selectionWidthStyle: widget.selectionWidthStyle, backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context),
scrollPadding: widget.scrollPadding, selectionHeightStyle: widget.selectionHeightStyle,
keyboardAppearance: keyboardAppearance, selectionWidthStyle: widget.selectionWidthStyle,
dragStartBehavior: widget.dragStartBehavior, scrollPadding: widget.scrollPadding,
scrollController: widget.scrollController, keyboardAppearance: keyboardAppearance,
scrollPhysics: widget.scrollPhysics, dragStartBehavior: widget.dragStartBehavior,
enableInteractiveSelection: widget.enableInteractiveSelection, scrollController: widget.scrollController,
autofillHints: widget.autofillHints, scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection,
autofillHints: widget.autofillHints,
restorationId: 'editable',
),
), ),
), ),
); );
......
...@@ -372,6 +372,7 @@ class TextField extends StatefulWidget { ...@@ -372,6 +372,7 @@ class TextField extends StatefulWidget {
this.scrollController, this.scrollController,
this.scrollPhysics, this.scrollPhysics,
this.autofillHints, this.autofillHints,
this.restorationId,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(readOnly != null), assert(readOnly != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -787,6 +788,25 @@ class TextField extends StatefulWidget { ...@@ -787,6 +788,25 @@ class TextField extends StatefulWidget {
/// {@macro flutter.services.autofill.autofillHints} /// {@macro flutter.services.autofill.autofillHints}
final Iterable<String> autofillHints; final Iterable<String> autofillHints;
/// {@template flutter.material.textfield.restorationId}
/// Restoration ID to save and restore the state of the text field.
///
/// If non-null, the text field will persist and restore its current scroll
/// offset and - if no [controller] has been provided - the content of the
/// text field. If a [controller] has been provided, it is the responsibility
/// of the owner of that controller to persist and restore it, e.g. by using
/// a [RestorableTextEditingController].
///
/// 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.
/// {@endtemplate}
final String restorationId;
@override @override
_TextFieldState createState() => _TextFieldState(); _TextFieldState createState() => _TextFieldState();
...@@ -828,9 +848,9 @@ class TextField extends StatefulWidget { ...@@ -828,9 +848,9 @@ class TextField extends StatefulWidget {
} }
} }
class _TextFieldState extends State<TextField> implements TextSelectionGestureDetectorBuilderDelegate { class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
TextEditingController _controller; RestorableTextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller; TextEditingController get _effectiveController => widget.controller ?? _controller.value;
FocusNode _focusNode; FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
...@@ -937,7 +957,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -937,7 +957,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
super.initState(); super.initState();
_selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this); _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null) { if (widget.controller == null) {
_controller = TextEditingController(); _createLocalController();
} }
_effectiveFocusNode.canRequestFocus = _isEnabled; _effectiveFocusNode.canRequestFocus = _isEnabled;
} }
...@@ -963,10 +983,13 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -963,10 +983,13 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
@override @override
void didUpdateWidget(TextField oldWidget) { void didUpdateWidget(TextField oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null) if (widget.controller == null && oldWidget.controller != null) {
_controller = TextEditingController.fromValue(oldWidget.controller.value); _createLocalController(oldWidget.controller.value);
else if (widget.controller != null && oldWidget.controller == null) } else if (widget.controller != null && oldWidget.controller == null) {
unregisterFromRestoration(_controller);
_controller.dispose();
_controller = null; _controller = null;
}
_effectiveFocusNode.canRequestFocus = _canRequestFocus; _effectiveFocusNode.canRequestFocus = _canRequestFocus;
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) { if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
if(_effectiveController.selection.isCollapsed) { if(_effectiveController.selection.isCollapsed) {
...@@ -975,9 +998,35 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -975,9 +998,35 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
} }
} }
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
if (_controller != null) {
_registerController();
}
}
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
String get restorationId => widget.restorationId;
@override @override
void dispose() { void dispose() {
_focusNode?.dispose(); _focusNode?.dispose();
_controller?.dispose();
super.dispose(); super.dispose();
} }
...@@ -1122,60 +1171,64 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe ...@@ -1122,60 +1171,64 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
} }
Widget child = RepaintBoundary( Widget child = RepaintBoundary(
child: EditableText( child: UnmanagedRestorationScope(
key: editableTextKey, bucket: bucket,
readOnly: widget.readOnly || !_isEnabled, child: EditableText(
toolbarOptions: widget.toolbarOptions, key: editableTextKey,
showCursor: widget.showCursor, readOnly: widget.readOnly || !_isEnabled,
showSelectionHandles: _showSelectionHandles, toolbarOptions: widget.toolbarOptions,
controller: controller, showCursor: widget.showCursor,
focusNode: focusNode, showSelectionHandles: _showSelectionHandles,
keyboardType: widget.keyboardType, controller: controller,
textInputAction: widget.textInputAction, focusNode: focusNode,
textCapitalization: widget.textCapitalization, keyboardType: widget.keyboardType,
style: style, textInputAction: widget.textInputAction,
strutStyle: widget.strutStyle, textCapitalization: widget.textCapitalization,
textAlign: widget.textAlign, style: style,
textDirection: widget.textDirection, strutStyle: widget.strutStyle,
autofocus: widget.autofocus, textAlign: widget.textAlign,
obscuringCharacter: widget.obscuringCharacter, textDirection: widget.textDirection,
obscureText: widget.obscureText, autofocus: widget.autofocus,
autocorrect: widget.autocorrect, obscuringCharacter: widget.obscuringCharacter,
smartDashesType: widget.smartDashesType, obscureText: widget.obscureText,
smartQuotesType: widget.smartQuotesType, autocorrect: widget.autocorrect,
enableSuggestions: widget.enableSuggestions, smartDashesType: widget.smartDashesType,
maxLines: widget.maxLines, smartQuotesType: widget.smartQuotesType,
minLines: widget.minLines, enableSuggestions: widget.enableSuggestions,
expands: widget.expands, maxLines: widget.maxLines,
selectionColor: selectionColor, minLines: widget.minLines,
selectionControls: widget.selectionEnabled ? textSelectionControls : null, expands: widget.expands,
onChanged: widget.onChanged, selectionColor: selectionColor,
onSelectionChanged: _handleSelectionChanged, selectionControls: widget.selectionEnabled ? textSelectionControls : null,
onEditingComplete: widget.onEditingComplete, onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted, onSelectionChanged: _handleSelectionChanged,
onAppPrivateCommand: widget.onAppPrivateCommand, onEditingComplete: widget.onEditingComplete,
onSelectionHandleTapped: _handleSelectionHandleTapped, onSubmitted: widget.onSubmitted,
inputFormatters: formatters, onAppPrivateCommand: widget.onAppPrivateCommand,
rendererIgnoresPointer: true, onSelectionHandleTapped: _handleSelectionHandleTapped,
mouseCursor: MouseCursor.defer, // TextField will handle the cursor inputFormatters: formatters,
cursorWidth: widget.cursorWidth, rendererIgnoresPointer: true,
cursorHeight: widget.cursorHeight, mouseCursor: MouseCursor.defer, // TextField will handle the cursor
cursorRadius: cursorRadius, cursorWidth: widget.cursorWidth,
cursorColor: cursorColor, cursorHeight: widget.cursorHeight,
selectionHeightStyle: widget.selectionHeightStyle, cursorRadius: cursorRadius,
selectionWidthStyle: widget.selectionWidthStyle, cursorColor: cursorColor,
cursorOpacityAnimates: cursorOpacityAnimates, selectionHeightStyle: widget.selectionHeightStyle,
cursorOffset: cursorOffset, selectionWidthStyle: widget.selectionWidthStyle,
paintCursorAboveText: paintCursorAboveText, cursorOpacityAnimates: cursorOpacityAnimates,
backgroundCursorColor: CupertinoColors.inactiveGray, cursorOffset: cursorOffset,
scrollPadding: widget.scrollPadding, paintCursorAboveText: paintCursorAboveText,
keyboardAppearance: keyboardAppearance, backgroundCursorColor: CupertinoColors.inactiveGray,
enableInteractiveSelection: widget.enableInteractiveSelection, scrollPadding: widget.scrollPadding,
dragStartBehavior: widget.dragStartBehavior, keyboardAppearance: keyboardAppearance,
scrollController: widget.scrollController, enableInteractiveSelection: widget.enableInteractiveSelection,
scrollPhysics: widget.scrollPhysics, dragStartBehavior: widget.dragStartBehavior,
autofillHints: widget.autofillHints, scrollController: widget.scrollController,
autocorrectionTextRectColor: autocorrectionTextRectColor, scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints,
autocorrectionTextRectColor: autocorrectionTextRectColor,
restorationId: 'editable',
),
), ),
); );
......
...@@ -445,6 +445,7 @@ class EditableText extends StatefulWidget { ...@@ -445,6 +445,7 @@ class EditableText extends StatefulWidget {
), ),
this.autofillHints, this.autofillHints,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
this.restorationId,
}) : assert(controller != null), }) : assert(controller != null),
assert(focusNode != null), assert(focusNode != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscuringCharacter != null && obscuringCharacter.length == 1),
...@@ -1218,6 +1219,25 @@ class EditableText extends StatefulWidget { ...@@ -1218,6 +1219,25 @@ class EditableText extends StatefulWidget {
/// Defaults to [Clip.hardEdge]. /// Defaults to [Clip.hardEdge].
final Clip clipBehavior; final Clip clipBehavior;
/// Restoration ID to save and restore the scroll offset of the
/// [EditableText].
///
/// If a restoration id is provided, the [EditableText] will persist its
/// current scroll offset and restore it during state restoration.
///
/// The scroll offset is persisted in a [RestorationBucket] claimed from
/// the surrounding [RestorationScope] using the provided restoration ID.
///
/// Persisting and restoring the content of the [EditableText] is the
/// responsibilility of the owner of the [controller], who may use a
/// [RestorableTextEditingController] for that purpose.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String restorationId;
// Infer the keyboard type of an `EditableText` if it's not specified. // Infer the keyboard type of an `EditableText` if it's not specified.
static TextInputType _inferKeyboardType({ static TextInputType _inferKeyboardType({
@required Iterable<String> autofillHints, @required Iterable<String> autofillHints,
...@@ -2323,6 +2343,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2323,6 +2343,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
controller: _scrollController, controller: _scrollController,
physics: widget.scrollPhysics, physics: widget.scrollPhysics,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
restorationId: widget.restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return CompositedTransformTarget( return CompositedTransformTarget(
link: _toolbarLayerLink, link: _toolbarLayerLink,
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
// @dart = 2.8 // @dart = 2.8
import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -257,11 +259,26 @@ class RestorableTextEditingController extends RestorableListenable<TextEditingCo ...@@ -257,11 +259,26 @@ class RestorableTextEditingController extends RestorableListenable<TextEditingCo
return value.text; return value.text;
} }
TextEditingController _controller;
@override
void initWithValue(TextEditingController value) {
_disposeControllerIfNecessary();
_controller = value;
super.initWithValue(value);
}
@override @override
void dispose() { void dispose() {
if (isRegistered) {
value.dispose();
}
super.dispose(); super.dispose();
_disposeControllerIfNecessary();
}
void _disposeControllerIfNecessary() {
if (_controller != null) {
// Scheduling a microtask for dispose to give other entities a chance
// to remove their listeners first.
scheduleMicrotask(_controller.dispose);
}
} }
} }
// 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.
// @dart = 2.8
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
const String text = 'Hello World! How are you? Life is good!';
const String alternativeText = 'Everything is awesome!!';
void main() {
testWidgets('CupertinoTextField restoration', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
child: TestWidget(),
restorationId: 'root',
),
);
await restoreAndVerify(tester);
});
testWidgets('CupertinoTextField restoration with external controller', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
child: TestWidget(
useExternal: true,
),
restorationId: 'root',
),
);
await restoreAndVerify(tester);
});
}
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(CupertinoTextField), 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(CupertinoTextField), 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 MaterialApp(
home: Material(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 50,
child: CupertinoTextField(
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));
}
// 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.
// @dart = 2.8
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.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 RootRestorationScope(
child: TestWidget(),
restorationId: 'root',
),
);
await restoreAndVerify(tester);
});
testWidgets('TextField restoration with external controller', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
child: TestWidget(
useExternal: true,
),
restorationId: 'root',
),
);
await restoreAndVerify(tester);
});
}
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(TextField), 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(TextField), 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 MaterialApp(
home: Material(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 50,
child: TextField(
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));
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment