SelectableText keep alive only when it has selection (#94493)
SelectableText defers to EditableText for wantKeepAlive, plus improved docs.
diff --git a/packages/flutter/lib/src/cupertino/text_field.dart b/packages/flutter/lib/src/cupertino/text_field.dart
index 3dd20a0..e797bc9 100644
--- a/packages/flutter/lib/src/cupertino/text_field.dart
+++ b/packages/flutter/lib/src/cupertino/text_field.dart
@@ -177,6 +177,8 @@
/// rounded rectangle border around the text field. If you set the [decoration]
/// property to null, the decoration will be removed entirely.
///
+/// {@macro flutter.material.textfield.wantKeepAlive}
+///
/// Remember to call [TextEditingController.dispose] when it is no longer
/// needed. This will ensure we discard any resources used by the object.
///
diff --git a/packages/flutter/lib/src/material/selectable_text.dart b/packages/flutter/lib/src/material/selectable_text.dart
index c8ad95b..bf09e04 100644
--- a/packages/flutter/lib/src/material/selectable_text.dart
+++ b/packages/flutter/lib/src/material/selectable_text.dart
@@ -123,6 +123,8 @@
/// behavior is useful, for example, to make the text bold while using the
/// default font family and size.
///
+/// {@macro flutter.material.textfield.wantKeepAlive}
+///
/// {@tool snippet}
///
/// ```dart
@@ -451,7 +453,7 @@
}
}
-class _SelectableTextState extends State<SelectableText> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
+class _SelectableTextState extends State<SelectableText> implements TextSelectionGestureDetectorBuilderDelegate {
EditableTextState? get _editableText => editableTextKey.currentState;
late _TextSpanEditingController _controller;
@@ -580,11 +582,7 @@
}
@override
- bool get wantKeepAlive => true;
-
- @override
Widget build(BuildContext context) {
- super.build(context); // See AutomaticKeepAliveClientMixin.
// TODO(garyq): Assert to block WidgetSpans from being used here are removed,
// but we still do not yet have nice handling of things like carets, clipboard,
// and other features. We should add proper support. Currently, caret handling
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index fc167cf..0257ea3 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -168,6 +168,13 @@
/// To integrate the [TextField] into a [Form] with other [FormField] widgets,
/// consider using [TextFormField].
///
+/// {@template flutter.material.textfield.wantKeepAlive}
+/// When the widget has focus, it will prevent itself from disposing via its
+/// underlying [EditableText]'s [AutomaticKeepAliveClientMixin.wantKeepAlive] in
+/// order to avoid losing the selection. Removing the focus will allow it to be
+/// disposed.
+/// {@endtemplate}
+///
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
/// when it is no longer needed. This will ensure we discard any resources used
/// by the object.
diff --git a/packages/flutter/lib/src/material/text_form_field.dart b/packages/flutter/lib/src/material/text_form_field.dart
index 3b1c810..d6fef5c 100644
--- a/packages/flutter/lib/src/material/text_form_field.dart
+++ b/packages/flutter/lib/src/material/text_form_field.dart
@@ -31,6 +31,8 @@
/// If a [controller] is not specified, [initialValue] can be used to give
/// the automatically generated controller an initial value.
///
+/// {@macro flutter.material.textfield.wantKeepAlive}
+///
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
/// when it is no longer needed. This will ensure we discard any resources used
/// by the object.
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index ae31e5d..3a0ac65 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -344,6 +344,10 @@
/// [onSubmitted] can be used to manually move focus to another input widget
/// when a user finishes with the currently focused input widget.
///
+/// When the widget has focus, it will prevent itself from disposing via
+/// [AutomaticKeepAliveClientMixin.wantKeepAlive] in order to avoid losing the
+/// selection. Removing the focus will allow it to be disposed.
+///
/// Rather than using this widget directly, consider using [TextField], which
/// is a full-featured, material-design text input field with placeholder text,
/// labels, and [Form] integration.
diff --git a/packages/flutter/test/widgets/selectable_text_test.dart b/packages/flutter/test/widgets/selectable_text_test.dart
index 6473cf5..e9f98b5 100644
--- a/packages/flutter/test/widgets/selectable_text_test.dart
+++ b/packages/flutter/test/widgets/selectable_text_test.dart
@@ -4869,4 +4869,91 @@
matchesGoldenFile('selectable_text_golden.TextSelectionStyle.2.png'),
);
});
+
+ testWidgets('keeps alive when has focus', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: DefaultTabController(
+ length: 2,
+ child: Scaffold(
+ body: NestedScrollView(
+ headerSliverBuilder:
+ (BuildContext context, bool innerBoxIsScrolled) {
+ return <Widget>[
+ SliverToBoxAdapter(
+ child: Container(
+ height: 200,
+ color: Colors.black12,
+ child: const Center(child: Text('Sliver 1')),
+ ),
+ ),
+ const SliverToBoxAdapter(
+ child: Center(
+ child: TabBar(
+ labelColor: Colors.black,
+ tabs: <Tab>[
+ Tab(text: 'Sliver Tab 1'),
+ Tab(text: 'Sliver Tab 2'),
+ ],
+ ),
+ )
+ ),
+ ];
+ },
+ body: const TabBarView(
+ children: <Widget>[
+ Padding(
+ padding: EdgeInsets.only(top: 100.0),
+ child: Text('Regular Text'),
+ ),
+ Padding(
+ padding: EdgeInsets.only(top: 100.0),
+ child: SelectableText('Selectable Text'),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ // Without any selection, the offscreen widget is disposed and can't be
+ // found, for both Text and SelectableText.
+ expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
+ expect(find.byType(SelectableText, skipOffstage: false), findsNothing);
+
+ await tester.tap(find.text('Sliver Tab 2'));
+ await tester.pumpAndSettle();
+ expect(find.text('Regular Text', skipOffstage: false), findsNothing);
+ expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
+
+ await tester.tap(find.text('Sliver Tab 1'));
+ await tester.pumpAndSettle();
+ expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
+ expect(find.byType(SelectableText, skipOffstage: false), findsNothing);
+
+ // Switch back to tab 2 and select some text in SelectableText.
+ await tester.tap(find.text('Sliver Tab 2'));
+ await tester.pumpAndSettle();
+ expect(find.text('Regular Text', skipOffstage: false), findsNothing);
+ expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
+
+ final EditableText editableText = tester.widget(find.byType(EditableText));
+ expect(editableText.controller.selection.isValid, isFalse);
+ await tester.tapAt(textOffsetToPosition(tester, 4));
+ await tester.pump(const Duration(milliseconds: 50));
+ await tester.tapAt(textOffsetToPosition(tester, 4));
+ await tester.pumpAndSettle();
+ expect(editableText.controller.selection.isValid, isTrue);
+ expect(editableText.controller.selection.baseOffset, 0);
+ expect(editableText.controller.selection.extentOffset, 'Selectable'.length);
+
+ // Switch back to tab 1. The SelectableText remains because it is preserving
+ // its selection.
+ await tester.tap(find.text('Sliver Tab 1'));
+ await tester.pumpAndSettle();
+ expect(find.text('Regular Text', skipOffstage: false), findsOneWidget);
+ expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
+ });
}