mirror of
https://github.com/flutter/flutter
synced 2024-08-24 18:36:03 +00:00
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
This commit is contained in:
parent
2832611da8
commit
8363e78280
|
@ -740,6 +740,8 @@ class _ViewContentState extends State<_ViewContent> {
|
|||
late Rect _viewRect;
|
||||
late final SearchController _controller;
|
||||
Iterable<Widget> result = <Widget>[];
|
||||
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<void> updateSuggestions() async {
|
||||
final Iterable<Widget> suggestions = await widget.suggestionsBuilder(context, _controller);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
result = suggestions;
|
||||
});
|
||||
if (searchValue != _controller.text) {
|
||||
searchValue = _controller.text;
|
||||
final Iterable<Widget> suggestions = await widget.suggestionsBuilder(context, _controller);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
result = suggestions;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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 {
|
||||
|
|
Loading…
Reference in a new issue