From e6b67587844a24fef7b6fb09cfe9ee96cd0555bc Mon Sep 17 00:00:00 2001 From: cuishuang Date: Wed, 29 Jul 2026 18:21:22 +0800 Subject: [PATCH] Prevent stale async suggestions in SearchAnchor --- .../material_ui/lib/src/search_anchor.dart | 27 +++++++------ .../change_2026_08_16_search_anchor.yaml | 3 ++ .../material_ui/test/search_anchor_test.dart | 40 +++++++++++++++++++ 3 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 packages/material_ui/pending_changelogs/change_2026_08_16_search_anchor.yaml diff --git a/packages/material_ui/lib/src/search_anchor.dart b/packages/material_ui/lib/src/search_anchor.dart index c08eccafa7e4..bc0578517d78 100644 --- a/packages/material_ui/lib/src/search_anchor.dart +++ b/packages/material_ui/lib/src/search_anchor.dart @@ -972,6 +972,8 @@ class _ViewContentState extends State<_ViewContent> { Iterable result = []; String? searchValue; Timer? _timer; + // Identifies the latest call so that older async results cannot replace newer ones. + int _suggestionsCallId = 0; @override void initState() { @@ -1008,14 +1010,9 @@ class _ViewContentState extends State<_ViewContent> { _timer?.cancel(); _timer = Timer(Duration.zero, () async { searchValue = _controller.text; - final Iterable suggestions = await widget.suggestionsBuilder(context, _controller); + await _buildSuggestions(); _timer?.cancel(); _timer = null; - if (mounted) { - setState(() { - result = suggestions; - }); - } }); } } @@ -1056,15 +1053,21 @@ class _ViewContentState extends State<_ViewContent> { Future updateSuggestions() async { if (searchValue != _controller.text) { searchValue = _controller.text; - final Iterable suggestions = await widget.suggestionsBuilder(context, _controller); - if (mounted) { - setState(() { - result = suggestions; - }); - } + await _buildSuggestions(); } } + Future _buildSuggestions() async { + final int callId = ++_suggestionsCallId; + final Iterable suggestions = await widget.suggestionsBuilder(context, _controller); + if (!mounted || callId != _suggestionsCallId) { + return; + } + setState(() { + result = suggestions; + }); + } + @override Widget build(BuildContext context) { final Widget defaultLeading = BackButton( diff --git a/packages/material_ui/pending_changelogs/change_2026_08_16_search_anchor.yaml b/packages/material_ui/pending_changelogs/change_2026_08_16_search_anchor.yaml new file mode 100644 index 000000000000..cec507da7926 --- /dev/null +++ b/packages/material_ui/pending_changelogs/change_2026_08_16_search_anchor.yaml @@ -0,0 +1,3 @@ +changelog: | + - Prevents stale asynchronous suggestions from replacing newer `SearchAnchor` results. +version: patch diff --git a/packages/material_ui/test/search_anchor_test.dart b/packages/material_ui/test/search_anchor_test.dart index 675a4e93950f..568be6c7d914 100644 --- a/packages/material_ui/test/search_anchor_test.dart +++ b/packages/material_ui/test/search_anchor_test.dart @@ -2261,6 +2261,46 @@ void main() { expect(controller.value.text, suggestion); }); + testWidgets('SearchAnchor ignores out-of-order async suggestions', (WidgetTester tester) async { + final requests = >>{}; + + await tester.pumpWidget( + MaterialApp( + home: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return const Icon(Icons.search); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + final String query = controller.text; + if (query.isEmpty) { + return []; + } + final request = Completer>(); + requests[query] = request; + return request.future; + }, + ), + ), + ); + + await tester.tap(find.byIcon(Icons.search)); + await tester.pumpAndSettle(); + + await tester.enterText(findTextField(), 'a'); + await tester.pump(); + await tester.enterText(findTextField(), 'ab'); + await tester.pump(); + + requests['ab']!.complete([const Text('ab-result')]); + await tester.pumpAndSettle(); + expect(find.text('ab-result'), findsOneWidget); + + requests['a']!.complete([const Text('a-result')]); + await tester.pumpAndSettle(); + expect(find.text('ab-result'), findsOneWidget); + expect(find.text('a-result'), findsNothing); + }); + testWidgets('SearchAnchor.bar has a default search bar as the anchor', ( WidgetTester tester, ) async {