Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix SearchAnchor disposing SearchController while it is still used #155219

Merged
merged 34 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d6b61c5
Update search_anchor_test.dart
PurplePolyhedron Sep 15, 2024
cdffd8f
Update search_anchor.dart
PurplePolyhedron Sep 15, 2024
cb30e0d
Update menu_anchor.dart
PurplePolyhedron Sep 15, 2024
6f15e12
revert Update menu_anchor.dart
PurplePolyhedron Sep 15, 2024
8b39e1b
fix search_anchor.dart error
PurplePolyhedron Sep 15, 2024
8622214
Update search_anchor_test.dart
PurplePolyhedron Sep 15, 2024
b018ce6
Update search_anchor_test.dart
PurplePolyhedron Sep 15, 2024
4b59559
Update search_anchor_test.dart
PurplePolyhedron Sep 16, 2024
3e445f5
Update search_anchor_test.dart
PurplePolyhedron Sep 16, 2024
10f7d2e
move disposal to ViewContent and update doc
PurplePolyhedron Sep 16, 2024
bdcb9b6
Update search_anchor.dart doc
PurplePolyhedron Sep 16, 2024
4c5f19a
Update search_anchor.dart doc
PurplePolyhedron Sep 16, 2024
7f143c6
Update search_anchor_test.dart
PurplePolyhedron Sep 17, 2024
e45cc99
Update search_anchor.dart
PurplePolyhedron Sep 17, 2024
226e28e
Update search_anchor.dart
PurplePolyhedron Sep 17, 2024
70bba6a
Update search_anchor.dart
PurplePolyhedron Sep 18, 2024
6a2cd13
Update search_anchor_test.dart
PurplePolyhedron Sep 18, 2024
1192390
search_anchor.dart format
PurplePolyhedron Sep 18, 2024
d12c58d
Update search_anchor_test.dart
PurplePolyhedron Sep 18, 2024
096a88a
Update search_anchor_test.dart
PurplePolyhedron Sep 18, 2024
f4b71e0
Update search_anchor.dart
PurplePolyhedron Sep 18, 2024
01897d1
Update search_anchor.dart
PurplePolyhedron Sep 18, 2024
2455974
Update search_anchor.dart
PurplePolyhedron Sep 19, 2024
bce5f65
Update search_anchor.dart
PurplePolyhedron Sep 19, 2024
0409816
Update search_anchor.dart
PurplePolyhedron Sep 19, 2024
2e33498
Update search_anchor.dart
PurplePolyhedron Sep 24, 2024
a9d914a
Update search_anchor_test.dart
PurplePolyhedron Sep 24, 2024
5e8d863
Update search_anchor_test format
PurplePolyhedron Sep 24, 2024
62cab33
Update search_anchor_test format
PurplePolyhedron Sep 24, 2024
150c6ca
Update search_anchor_test.dart format
PurplePolyhedron Sep 24, 2024
4e0f0b3
Update search_anchor_test.dart format
PurplePolyhedron Sep 24, 2024
7cbac61
Merge branch 'master' into search_anchor_dispose
PurplePolyhedron Sep 24, 2024
d205e8e
Merge branch 'master' into search_anchor_dispose
PurplePolyhedron Sep 24, 2024
e34910b
Update search_anchor_test.dart
PurplePolyhedron Sep 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions packages/flutter/lib/src/material/search_anchor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ class _SearchAnchorState extends State<SearchAnchor> {
bool get _viewIsOpen => !_anchorIsVisible;
SearchController? _internalSearchController;
SearchController get _searchController => widget.searchController ?? (_internalSearchController ??= SearchController());
_SearchViewRoute? _route;
nate-thegrate marked this conversation as resolved.
Show resolved Hide resolved

@override
void initState() {
Expand Down Expand Up @@ -401,15 +402,17 @@ class _SearchAnchorState extends State<SearchAnchor> {

@override
void dispose() {
super.dispose();
widget.searchController?._detach(this);
_internalSearchController?._detach(this);
_internalSearchController?.dispose();
}
_route?._dismiss(
disposeController: widget.searchController == null,
);
nate-thegrate marked this conversation as resolved.
Show resolved Hide resolved
super.dispose();
}

void _openView() {
final NavigatorState navigator = Navigator.of(context);
navigator.push(_SearchViewRoute(
_route = _SearchViewRoute(
viewOnChanged: widget.viewOnChanged,
viewOnSubmitted: widget.viewOnSubmitted,
viewLeading: widget.viewLeading,
Expand All @@ -436,7 +439,8 @@ class _SearchAnchorState extends State<SearchAnchor> {
capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
textInputAction: widget.textInputAction,
keyboardType: widget.keyboardType,
));
);
navigator.push(_route!);
}

void _closeView(String? selectedText) {
Expand Down Expand Up @@ -542,6 +546,7 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
final TextInputType? keyboardType;
CurvedAnimation? curvedAnimation;
CurvedAnimation? viewFadeOnIntervalCurve;
bool _disposeController = false;

@override
Color? get barrierColor => Colors.transparent;
Expand Down Expand Up @@ -585,10 +590,20 @@ class _SearchViewRoute extends PopupRoute<_SearchViewRoute> {
return super.didPop(result);
}

void _dismiss({required bool disposeController}) {
_disposeController = disposeController;
if (isActive) {
navigator?.removeRoute(this);
}
}

@override
void dispose() {
curvedAnimation?.dispose();
viewFadeOnIntervalCurve?.dispose();
if (_disposeController) {
searchController.dispose();
}
super.dispose();
}

Expand Down
141 changes: 141 additions & 0 deletions packages/flutter/test/material/search_anchor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3419,6 +3419,147 @@ void main() {
expect(find.byType(Placeholder), findsOneWidget);
});
});

testWidgets('SearchAnchor does not dispose external SeachController', (WidgetTester tester) async {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
testWidgets('SearchAnchor does not dispose external SeachController', (WidgetTester tester) async {
testWidgets('SearchAnchor does not dispose external SearchController', (WidgetTester tester) async {

final SearchController controller = SearchController();
addTearDown(controller.dispose);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SearchAnchor(
searchController: controller,
builder: (BuildContext context, SearchController controller) {
return IconButton(
onPressed: () async {
controller.openView();
},
icon: const Icon(Icons.search),
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[];
},
),
),
));

await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Text('disposed'),
),
));
expect(tester.takeException(), isNull);
ChangeNotifier.debugAssertNotDisposed(controller);
});

testWidgets('SearchAnchor gracefully closes its search view when disposed', (WidgetTester tester) async {
bool disposed = false;
late StateSetter setState;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
if (disposed) {
return const Text('disposed');
}
return SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return IconButton(
onPressed: () async {
controller.openView();
},
icon: const Icon(Icons.search),
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[
const Text('suggestion'),
];
},
);
}
),
),
));

await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
setState(() {
disposed = true;
});
await tester.pump();
// The search menu starts to close but is not disposed yet.
final EditableText editableText = tester.widget(find.byType(EditableText));
final TextEditingController controller = editableText.controller;
ChangeNotifier.debugAssertNotDisposed(controller);

await tester.pumpAndSettle();
// The search menu and the internal search controller are now disposed.
expect(tester.takeException(), isNull);
expect(find.byType(TextField), findsNothing);
FlutterError? error;
try {
ChangeNotifier.debugAssertNotDisposed(controller);
} on FlutterError catch (e) {
error = e;
}
expect(error, isNotNull);
expect(error, isFlutterError);
expect(
error!.toStringDeep(),
equalsIgnoringHashCodes(
'FlutterError\n'
' A SearchController was used after being disposed.\n'
' Once you have called dispose() on a SearchController, it can no\n'
' longer be used.\n',
),
);
});

// Regression test for https://github.com/flutter/flutter/issues/155180.
testWidgets('disposing SearchAnchor during search view exit animation does not crash',
(WidgetTester tester) async {
final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: key,
home: Material(
child: SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return IconButton(
onPressed: () async {
controller.openView();
},
icon: const Icon(Icons.search),
);
},
suggestionsBuilder: (BuildContext context, SearchController controller) {
return <Widget>[
const Text('suggestion'),
];
},
),
),
));

await tester.tap(find.byIcon(Icons.search));
await tester.pumpAndSettle();
key.currentState!.pop();
await tester.pump();
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Text('disposed'),
),
));
await tester.pump();
expect(tester.takeException(), isNull);
});
}

Future<void> checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async {
Expand Down