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 {
this.scrollController,
this.scrollPhysics,
this.autofillHints,
this.restorationId,
}) : assert(textAlign != null),
assert(readOnly != null),
assert(autofocus != null),
......@@ -600,6 +601,9 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.services.autofill.autofillHints}
final Iterable<String> autofillHints;
/// {@macro flutter.material.textfield.restorationId}
final String restorationId;
@override
_CupertinoTextFieldState createState() => _CupertinoTextFieldState();
......@@ -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();
TextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller;
RestorableTextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller.value;
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
......@@ -670,8 +674,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
super.initState();
_selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null) {
_controller = TextEditingController();
_controller.addListener(updateKeepAlive);
_createLocalController();
}
}
......@@ -679,9 +682,10 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
void didUpdateWidget(CupertinoTextField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null) {
_controller = TextEditingController.fromValue(oldWidget.controller.value);
_controller.addListener(updateKeepAlive);
_createLocalController(oldWidget.controller.value);
} else if (widget.controller != null && oldWidget.controller == null) {
unregisterFromRestoration(_controller);
_controller.dispose();
_controller = null;
}
final bool isEnabled = widget.enabled ?? true;
......@@ -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
void dispose() {
_focusNode?.dispose();
_controller?.removeListener(updateKeepAlive);
_controller?.dispose();
super.dispose();
}
......@@ -736,7 +766,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
}
@override
bool get wantKeepAlive => _controller?.text?.isNotEmpty == true;
bool get wantKeepAlive => _controller?.value?.text?.isNotEmpty == true;
bool _shouldShowAttachment({
OverlayVisibilityMode attachment,
......@@ -927,57 +957,61 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
final Widget paddedEditable = Padding(
padding: widget.padding,
child: RepaintBoundary(
child: EditableText(
key: editableTextKey,
controller: controller,
readOnly: widget.readOnly,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
focusNode: _effectiveFocusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: textStyle,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: selectionColor,
selectionControls: widget.selectionEnabled
? cupertinoTextSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
inputFormatters: formatters,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorColor: cursorColor,
cursorOpacityAnimates: true,
cursorOffset: cursorOffset,
paintCursorAboveText: true,
autocorrectionTextRectColor: selectionColor,
backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context),
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection,
autofillHints: widget.autofillHints,
child: UnmanagedRestorationScope(
bucket: bucket,
child: EditableText(
key: editableTextKey,
controller: controller,
readOnly: widget.readOnly,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
focusNode: _effectiveFocusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: textStyle,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: selectionColor,
selectionControls: widget.selectionEnabled
? cupertinoTextSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
inputFormatters: formatters,
rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorColor: cursorColor,
cursorOpacityAnimates: true,
cursorOffset: cursorOffset,
paintCursorAboveText: true,
autocorrectionTextRectColor: selectionColor,
backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context),
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
enableInteractiveSelection: widget.enableInteractiveSelection,
autofillHints: widget.autofillHints,
restorationId: 'editable',
),
),
),
);
......
......@@ -372,6 +372,7 @@ class TextField extends StatefulWidget {
this.scrollController,
this.scrollPhysics,
this.autofillHints,
this.restorationId,
}) : assert(textAlign != null),
assert(readOnly != null),
assert(autofocus != null),
......@@ -787,6 +788,25 @@ class TextField extends StatefulWidget {
/// {@macro flutter.services.autofill.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
_TextFieldState createState() => _TextFieldState();
......@@ -828,9 +848,9 @@ class TextField extends StatefulWidget {
}
}
class _TextFieldState extends State<TextField> implements TextSelectionGestureDetectorBuilderDelegate {
TextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller;
class _TextFieldState extends State<TextField> with RestorationMixin implements TextSelectionGestureDetectorBuilderDelegate {
RestorableTextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller.value;
FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
......@@ -937,7 +957,7 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
super.initState();
_selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null) {
_controller = TextEditingController();
_createLocalController();
}
_effectiveFocusNode.canRequestFocus = _isEnabled;
}
......@@ -963,10 +983,13 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
@override
void didUpdateWidget(TextField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null)
_controller = TextEditingController.fromValue(oldWidget.controller.value);
else if (widget.controller != null && oldWidget.controller == null)
if (widget.controller == null && oldWidget.controller != null) {
_createLocalController(oldWidget.controller.value);
} else if (widget.controller != null && oldWidget.controller == null) {
unregisterFromRestoration(_controller);
_controller.dispose();
_controller = null;
}
_effectiveFocusNode.canRequestFocus = _canRequestFocus;
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) {
if(_effectiveController.selection.isCollapsed) {
......@@ -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
void dispose() {
_focusNode?.dispose();
_controller?.dispose();
super.dispose();
}
......@@ -1122,60 +1171,64 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
}
Widget child = RepaintBoundary(
child: EditableText(
key: editableTextKey,
readOnly: widget.readOnly || !_isEnabled,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
controller: controller,
focusNode: focusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: style,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
textDirection: widget.textDirection,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: selectionColor,
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
onAppPrivateCommand: widget.onAppPrivateCommand,
onSelectionHandleTapped: _handleSelectionHandleTapped,
inputFormatters: formatters,
rendererIgnoresPointer: true,
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
cursorOpacityAnimates: cursorOpacityAnimates,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,
backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
enableInteractiveSelection: widget.enableInteractiveSelection,
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints,
autocorrectionTextRectColor: autocorrectionTextRectColor,
child: UnmanagedRestorationScope(
bucket: bucket,
child: EditableText(
key: editableTextKey,
readOnly: widget.readOnly || !_isEnabled,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
showSelectionHandles: _showSelectionHandles,
controller: controller,
focusNode: focusNode,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: style,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
textDirection: widget.textDirection,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
selectionColor: selectionColor,
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
onChanged: widget.onChanged,
onSelectionChanged: _handleSelectionChanged,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
onAppPrivateCommand: widget.onAppPrivateCommand,
onSelectionHandleTapped: _handleSelectionHandleTapped,
inputFormatters: formatters,
rendererIgnoresPointer: true,
mouseCursor: MouseCursor.defer, // TextField will handle the cursor
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
cursorOpacityAnimates: cursorOpacityAnimates,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,
backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
enableInteractiveSelection: widget.enableInteractiveSelection,
dragStartBehavior: widget.dragStartBehavior,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints,
autocorrectionTextRectColor: autocorrectionTextRectColor,
restorationId: 'editable',
),
),
);
......
......@@ -445,6 +445,7 @@ class EditableText extends StatefulWidget {
),
this.autofillHints,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
......@@ -1218,6 +1219,25 @@ class EditableText extends StatefulWidget {
/// Defaults to [Clip.hardEdge].
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.
static TextInputType _inferKeyboardType({
@required Iterable<String> autofillHints,
......@@ -2323,6 +2343,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
controller: _scrollController,
physics: widget.scrollPhysics,
dragStartBehavior: widget.dragStartBehavior,
restorationId: widget.restorationId,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return CompositedTransformTarget(
link: _toolbarLayerLink,
......
......@@ -4,6 +4,8 @@
// @dart = 2.8
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
......@@ -257,11 +259,26 @@ class RestorableTextEditingController extends RestorableListenable<TextEditingCo
return value.text;
}
TextEditingController _controller;
@override
void initWithValue(TextEditingController value) {
_disposeControllerIfNecessary();
_controller = value;
super.initWithValue(value);
}
@override
void dispose() {
if (isRegistered) {
value.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