Skip to content

Commit

Permalink
Fix SearchAnchor triggers unnecessary suggestions builder calls (fl…
Browse files Browse the repository at this point in the history
…utter#143443)

fixes [`SearchAnchor` triggers extra search operations](flutter#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
  • Loading branch information
TahaTesser committed Mar 25, 2024
1 parent 2832611 commit 8363e78
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 6 deletions.
28 changes: 22 additions & 6 deletions packages/flutter/lib/src/material/search_anchor.dart
Expand Up @@ -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() {
Expand Down Expand Up @@ -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();
}

Expand All @@ -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;
});
}
}
}

Expand Down
98 changes: 98 additions & 0 deletions packages/flutter/test/material/search_anchor_test.dart
Expand Up @@ -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 {
Expand Down

0 comments on commit 8363e78

Please sign in to comment.