diff --git a/packages/flutter/lib/src/material/search_anchor.dart b/packages/flutter/lib/src/material/search_anchor.dart index 90187b84bd3..cf7d6c07f16 100644 --- a/packages/flutter/lib/src/material/search_anchor.dart +++ b/packages/flutter/lib/src/material/search_anchor.dart @@ -740,6 +740,8 @@ class _ViewContentState extends State<_ViewContent> { late Rect _viewRect; late final SearchController _controller; Iterable result = []; + String? searchValue; + Timer? _timer; @override void initState() { @@ -770,12 +772,23 @@ class _ViewContentState extends State<_ViewContent> { _viewRect = Offset.zero & _screenSize!; } } - unawaited(updateSuggestions()); + + if (searchValue != _controller.text) { + _timer?.cancel(); + _timer = Timer(Duration.zero, () async { + searchValue = _controller.text; + result = await widget.suggestionsBuilder(context, _controller); + _timer?.cancel(); + _timer = null; + }); + } } @override void dispose() { _controller.removeListener(updateSuggestions); + _timer?.cancel(); + _timer = null; super.dispose(); } @@ -793,11 +806,14 @@ class _ViewContentState extends State<_ViewContent> { } Future updateSuggestions() async { - final Iterable suggestions = await widget.suggestionsBuilder(context, _controller); - if (mounted) { - setState(() { - result = suggestions; - }); + if (searchValue != _controller.text) { + searchValue = _controller.text; + final Iterable suggestions = await widget.suggestionsBuilder(context, _controller); + if (mounted) { + setState(() { + result = suggestions; + }); + } } } diff --git a/packages/flutter/test/material/search_anchor_test.dart b/packages/flutter/test/material/search_anchor_test.dart index 4775e858dcd..76ed139616d 100644 --- a/packages/flutter/test/material/search_anchor_test.dart +++ b/packages/flutter/test/material/search_anchor_test.dart @@ -3131,6 +3131,104 @@ void main() { await tester.pump(); expect(find.widgetWithIcon(IconButton, Icons.close), findsNothing); }); + + // This is a regression test for https://github.com/flutter/flutter/issues/139880. + testWidgets('suggestionsBuilder with Future is not called twice on layout resize', (WidgetTester tester) async { + int suggestionsLoadingCount = 0; + + Future> createListData() async { + return List.generate(1000, (int index) { + return 'Hello World - $index'; + }); + } + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Center( + child: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return const Icon(Icons.search); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + return [ + FutureBuilder>( + future: createListData(), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const LinearProgressIndicator(); + } + final List? result = snapshot.data; + if (result == null) { + return const LinearProgressIndicator(); + } + suggestionsLoadingCount++; + return SingleChildScrollView( + child: Column( + children: result.map((String text) { + return ListTile(title: Text(text)); + }).toList(), + ), + ); + }, + ), + ]; + }, + ), + ), + ), + )); + await tester.pump(); + await tester.tap(find.byIcon(Icons.search)); // Open search view. + await tester.pumpAndSettle(); + + // Simulate the keyboard opening resizing the view. + tester.view.viewInsets = const FakeViewPadding(bottom: 500.0); + addTearDown(tester.view.reset); + await tester.pumpAndSettle(); + + expect(suggestionsLoadingCount, 1); + }); + + // This is a regression test for https://github.com/flutter/flutter/issues/139880. + testWidgets('suggestionsBuilder is not called when the search value does not change', (WidgetTester tester) async { + int suggestionsBuilderCalledCount = 0; + + await tester.pumpWidget(MaterialApp( + home: Scaffold( + body: Center( + child: SearchAnchor( + builder: (BuildContext context, SearchController controller) { + return const Icon(Icons.search); + }, + suggestionsBuilder: (BuildContext context, SearchController controller) { + suggestionsBuilderCalledCount++; + return []; + }, + ), + ), + ), + )); + await tester.pump(); + await tester.tap(find.byIcon(Icons.search)); // Open search view. + await tester.pumpAndSettle(); + + // Simulate the keyboard opening resizing the view. + tester.view.viewInsets = const FakeViewPadding(bottom: 500.0); + addTearDown(tester.view.reset); + // Show the keyboard. + await tester.showKeyboard(find.byType(TextField)); + await tester.pumpAndSettle(); + + expect(suggestionsBuilderCalledCount, 2); + + // Remove the viewInset, as if the keyboard were hidden. + tester.view.resetViewInsets(); + // Hide the keyboard. + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + expect(suggestionsBuilderCalledCount, 2); + }); } Future checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async {