Unverified Commit 8363e782 authored by Taha Tesser's avatar Taha Tesser Committed by GitHub

Fix `SearchAnchor` triggers unnecessary suggestions builder calls (#143443)

fixes [`SearchAnchor` triggers extra search operations](https://github.com/flutter/flutter/issues/139880)

### Code sample

<details>
<summary>expand to view the code sample</summary> 

```dart
import 'package:flutter/material.dart';

Future<List<String>> createFuture() async {
  return List.generate(1000, (index) => "Hello World!");
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final SearchController controller = SearchController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SearchAnchor(
              searchController: controller,
              suggestionsBuilder: (suggestionsContext, controller) {
                final resultFuture = createFuture();
                return [
                  FutureBuilder(
                    future: resultFuture,
                    builder: ((context, snapshot) {
                      if (snapshot.connectionState != ConnectionState.done) {
                        return const LinearProgressIndicator();
                      }
                      final result = snapshot.data;
                      if (result == null) {
                        return const LinearProgressIndicator();
                      }
                      return ListView.builder(
                        shrinkWrap: true,
                        physics: const NeverScrollableScrollPhysics(),
                        itemCount: result.length,
                        itemBuilder: (BuildContext context, int index) {
                          final root = result[index];
                          return ListTile(
                            leading: const Icon(Icons.article),
                            title: Text(root),
                            subtitle: Text(
                              root,
                              overflow: TextOverflow.ellipsis,
                              style: TextStyle(
                                color: Theme.of(suggestionsContext)
                                    .colorScheme
                                    .onSurfaceVariant,
                              ),
                            ),
                            onTap: () {},
                          );
                        },
                      );
                    }),
                  ),
                ];
              },
              builder: (BuildContext context, SearchController controller) {
                return IconButton(
                  onPressed: () {
                    controller.openView();
                  },
                  icon: const Icon(Icons.search),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

```

</details>

### Before

https://github.com/flutter/flutter/assets/48603081/69f6dfdc-9f92-4d2e-8a3e-984fce25f9e4

### After

https://github.com/flutter/flutter/assets/48603081/be105e2c-51d8-4cb0-a75b-f5f41d948e5e
parent 2832611d
...@@ -740,6 +740,8 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -740,6 +740,8 @@ class _ViewContentState extends State<_ViewContent> {
late Rect _viewRect; late Rect _viewRect;
late final SearchController _controller; late final SearchController _controller;
Iterable<Widget> result = <Widget>[]; Iterable<Widget> result = <Widget>[];
String? searchValue;
Timer? _timer;
@override @override
void initState() { void initState() {
...@@ -770,12 +772,23 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -770,12 +772,23 @@ class _ViewContentState extends State<_ViewContent> {
_viewRect = Offset.zero & _screenSize!; _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 @override
void dispose() { void dispose() {
_controller.removeListener(updateSuggestions); _controller.removeListener(updateSuggestions);
_timer?.cancel();
_timer = null;
super.dispose(); super.dispose();
} }
...@@ -793,6 +806,8 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -793,6 +806,8 @@ class _ViewContentState extends State<_ViewContent> {
} }
Future<void> updateSuggestions() async { Future<void> updateSuggestions() async {
if (searchValue != _controller.text) {
searchValue = _controller.text;
final Iterable<Widget> suggestions = await widget.suggestionsBuilder(context, _controller); final Iterable<Widget> suggestions = await widget.suggestionsBuilder(context, _controller);
if (mounted) { if (mounted) {
setState(() { setState(() {
...@@ -800,6 +815,7 @@ class _ViewContentState extends State<_ViewContent> { ...@@ -800,6 +815,7 @@ class _ViewContentState extends State<_ViewContent> {
}); });
} }
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
......
...@@ -3131,6 +3131,104 @@ void main() { ...@@ -3131,6 +3131,104 @@ void main() {
await tester.pump(); await tester.pump();
expect(find.widgetWithIcon(IconButton, Icons.close), findsNothing); 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<List<String>> createListData() async {
return List<String>.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 <Widget>[
FutureBuilder<List<String>>(
future: createListData(),
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const LinearProgressIndicator();
}
final List<String>? 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 <Widget>[];
},
),
),
),
));
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<void> checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async { Future<void> checkSearchBarDefaults(WidgetTester tester, ColorScheme colorScheme, Material material) async {
......
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