SearchAnchor class

Manages a "search view" route that allows the user to select one of the suggested completions for a search query.

The search view's route can either be shown by creating a SearchController and then calling SearchController.openView or by tapping on an anchor. When the anchor is tapped or SearchController.openView is called, the search view either grows to a specific size, or grows to fill the entire screen. By default, the search view only shows full screen on mobile platforms. Use SearchAnchor.isFullScreen to override the default setting.

The search view is usually opened by a SearchBar, an IconButton or an Icon. If builder returns an Icon, or any un-tappable widgets, we don't have to explicitly call SearchController.openView.

The search view route will be popped if the window size is changed and the search view route is not in full-screen mode. However, if the search view route is in full-screen mode, changing the window size, such as rotating a mobile device from portrait mode to landscape mode, will not close the search view.

This example shows how to use an IconButton to open a search view in a SearchAnchor. It also shows how to use SearchController to open or close the search view route.

To see it in action, copy and run this code snippet on DartPad.

import 'package:material_ui/material_ui.dart';

/// Flutter code sample for [SearchAnchor].

void main() => runApp(const SearchBarApp());

class SearchBarApp extends StatefulWidget {
  const SearchBarApp({super.key});

  @override
  State<SearchBarApp> createState() => _SearchBarAppState();
}

class _SearchBarAppState extends State<SearchBarApp> {
  final SearchController controller = SearchController();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Search Anchor Sample')),
        body: Column(
          children: <Widget>[
            SearchAnchor(
              searchController: controller,
              builder: (BuildContext context, SearchController controller) {
                return IconButton(
                  icon: const Icon(Icons.search),
                  onPressed: () {
                    controller.openView();
                  },
                );
              },
              suggestionsBuilder:
                  (BuildContext context, SearchController controller) {
                    return List<ListTile>.generate(5, (int index) {
                      final String item = 'item $index';
                      return ListTile(
                        title: Text(item),
                        onTap: () {
                          setState(() {
                            controller.closeView(item);
                          });
                        },
                      );
                    });
                  },
            ),
            Center(
              child: controller.text.isEmpty
                  ? const Text('No item selected')
                  : Text('Selected item: ${controller.value.text}'),
            ),
          ],
        ),
      ),
    );
  }
}

This example shows how to set up a floating (or pinned) AppBar with a SearchAnchor for a title.

To see it in action, copy and run this code snippet on DartPad.

import 'package:material_ui/material_ui.dart';

/// Flutter code sample for pinned [SearchAnchor] while scrolling.

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

class PinnedSearchBarApp extends StatefulWidget {
  const PinnedSearchBarApp({super.key});

  @override
  State<PinnedSearchBarApp> createState() => _PinnedSearchBarAppState();
}

class _PinnedSearchBarAppState extends State<PinnedSearchBarApp> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4)),
      home: Scaffold(
        body: SafeArea(
          child: CustomScrollView(
            slivers: <Widget>[
              SliverAppBar(
                clipBehavior: .none,
                shape: const StadiumBorder(),
                scrolledUnderElevation: 0.0,
                titleSpacing: 0.0,
                backgroundColor: Colors.transparent,
                floating:
                    true, // We can also uncomment this line and set `pinned` to true to see a pinned search bar.
                title: SearchAnchor.bar(
                  suggestionsBuilder:
                      (BuildContext context, SearchController controller) {
                        return List<Widget>.generate(5, (int index) {
                          return ListTile(
                            titleAlignment: .center,
                            title: Text('Initial list item $index'),
                          );
                        });
                      },
                ),
              ),
              // The listed items below are just for filling the screen
              // so we can see the scrolling effect.
              SliverToBoxAdapter(
                child: Padding(
                  padding: const .all(20),
                  child: SizedBox(
                    height: 100.0,
                    child: ListView.builder(
                      scrollDirection: Axis.horizontal,
                      itemCount: 10,
                      itemBuilder: (BuildContext context, int index) {
                        return SizedBox(
                          width: 100.0,
                          child: Card(
                            child: Center(child: Text('Card $index')),
                          ),
                        );
                      },
                    ),
                  ),
                ),
              ),
              SliverToBoxAdapter(
                child: Padding(
                  padding: const .symmetric(horizontal: 20),
                  child: Container(
                    height: 1000,
                    color: Colors.deepPurple.withValues(alpha: 0.5),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

This example shows how to fetch the search suggestions from a remote API.

To see it in action, copy and run this code snippet on DartPad.

import 'package:material_ui/material_ui.dart';

/// Flutter code sample for [SearchAnchor].

const Duration fakeAPIDuration = Duration(seconds: 1);

void main() => runApp(const SearchAnchorAsyncExampleApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('SearchAnchor - async')),
        body: const Center(child: _AsyncSearchAnchor()),
      ),
    );
  }
}

class _AsyncSearchAnchor extends StatefulWidget {
  const _AsyncSearchAnchor();

  @override
  State<_AsyncSearchAnchor> createState() => _AsyncSearchAnchorState();
}

class _AsyncSearchAnchorState extends State<_AsyncSearchAnchor> {
  // The query currently being searched for. If null, there is no pending
  // request.
  String? _searchingWithQuery;

  // The most recent options received from the API.
  late Iterable<Widget> _lastOptions = <Widget>[];

  @override
  Widget build(BuildContext context) {
    return SearchAnchor(
      builder: (BuildContext context, SearchController controller) {
        return IconButton(
          icon: const Icon(Icons.search),
          onPressed: () {
            controller.openView();
          },
        );
      },
      suggestionsBuilder:
          (BuildContext context, SearchController controller) async {
            _searchingWithQuery = controller.text;
            final List<String> options = (await _FakeAPI.search(
              _searchingWithQuery!,
            )).toList();

            // If another search happened after this one, throw away these options.
            // Use the previous options instead and wait for the newer request to
            // finish.
            if (_searchingWithQuery != controller.text) {
              return _lastOptions;
            }

            _lastOptions = List<ListTile>.generate(options.length, (int index) {
              final String item = options[index];
              return ListTile(title: Text(item));
            });

            return _lastOptions;
          },
    );
  }
}

// Mimics a remote API.
class _FakeAPI {
  static const List<String> _kOptions = <String>[
    'aardvark',
    'bobcat',
    'chameleon',
  ];

  // Searches the options, but injects a fake "network" delay.
  static Future<Iterable<String>> search(String query) async {
    await Future<void>.delayed(fakeAPIDuration); // Fake 1 second delay.
    if (query == '') {
      return const Iterable<String>.empty();
    }
    return _kOptions.where((String option) {
      return option.contains(query.toLowerCase());
    });
  }
}

This example demonstrates fetching the search suggestions asynchronously and debouncing network calls.

To see it in action, copy and run this code snippet on DartPad.

import 'dart:async';

import 'package:material_ui/material_ui.dart';

/// Flutter code sample for [SearchAnchor].

const Duration fakeAPIDuration = Duration(seconds: 1);
const Duration debounceDuration = Duration(milliseconds: 500);

void main() => runApp(const SearchAnchorAsyncExampleApp());

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('SearchAnchor - async and debouncing'),
        ),
        body: const Center(child: _AsyncSearchAnchor()),
      ),
    );
  }
}

class _AsyncSearchAnchor extends StatefulWidget {
  const _AsyncSearchAnchor();

  @override
  State<_AsyncSearchAnchor> createState() => _AsyncSearchAnchorState();
}

class _AsyncSearchAnchorState extends State<_AsyncSearchAnchor> {
  // The query currently being searched for. If null, there is no pending
  // request.
  String? _currentQuery;

  // The most recent suggestions received from the API.
  late Iterable<Widget> _lastOptions = <Widget>[];

  late final _Debounceable<Iterable<String>?, String> _debouncedSearch;

  // Calls the "remote" API to search with the given query. Returns null when
  // the call has been made obsolete.
  Future<Iterable<String>?> _search(String query) async {
    _currentQuery = query;

    // In a real application, there should be some error handling here.
    final Iterable<String> options = await _FakeAPI.search(_currentQuery!);

    // If another search happened after this one, throw away these options.
    if (_currentQuery != query) {
      return null;
    }
    _currentQuery = null;

    return options;
  }

  @override
  void initState() {
    super.initState();
    _debouncedSearch = _debounce<Iterable<String>?, String>(_search);
  }

  @override
  Widget build(BuildContext context) {
    return SearchAnchor(
      builder: (BuildContext context, SearchController controller) {
        return IconButton(
          icon: const Icon(Icons.search),
          onPressed: () {
            controller.openView();
          },
        );
      },
      suggestionsBuilder:
          (BuildContext context, SearchController controller) async {
            final List<String>? options = (await _debouncedSearch(
              controller.text,
            ))?.toList();
            if (options == null) {
              return _lastOptions;
            }
            _lastOptions = List<ListTile>.generate(options.length, (int index) {
              final String item = options[index];
              return ListTile(
                title: Text(item),
                onTap: () {
                  debugPrint('You just selected $item');
                },
              );
            });

            return _lastOptions;
          },
    );
  }
}

// Mimics a remote API.
class _FakeAPI {
  static const List<String> _kOptions = <String>[
    'aardvark',
    'bobcat',
    'chameleon',
  ];

  // Searches the options, but injects a fake "network" delay.
  static Future<Iterable<String>> search(String query) async {
    await Future<void>.delayed(fakeAPIDuration); // Fake 1 second delay.
    if (query == '') {
      return const Iterable<String>.empty();
    }
    return _kOptions.where((String option) {
      return option.contains(query.toLowerCase());
    });
  }
}

typedef _Debounceable<S, T> = Future<S?> Function(T parameter);

/// Returns a new function that is a debounced version of the given function.
///
/// This means that the original function will be called only after no calls
/// have been made for the given Duration.
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
  _DebounceTimer? debounceTimer;

  return (T parameter) async {
    if (debounceTimer != null && !debounceTimer!.isCompleted) {
      debounceTimer!.cancel();
    }
    debounceTimer = _DebounceTimer();
    try {
      await debounceTimer!.future;
    } on _CancelException {
      return null;
    }
    return function(parameter);
  };
}

// A wrapper around Timer used for debouncing.
class _DebounceTimer {
  _DebounceTimer() {
    _timer = Timer(debounceDuration, _onComplete);
  }

  late final Timer _timer;
  final Completer<void> _completer = Completer<void>();

  void _onComplete() {
    _completer.complete();
  }

  Future<void> get future => _completer.future;

  bool get isCompleted => _completer.isCompleted;

  void cancel() {
    _timer.cancel();
    _completer.completeError(const _CancelException());
  }
}

// An exception indicating that the timer was canceled.
class _CancelException implements Exception {
  const _CancelException();
}

See also:

  • SearchBar, a widget that defines a search bar.
  • SearchBarTheme, a widget that overrides the default configuration of a search bar.
  • SearchViewTheme, a widget that overrides the default configuration of a search view.
Inheritance

Constructors

SearchAnchor({Key? key, bool? isFullScreen, SearchController? searchController, ViewBuilder? viewBuilder, Widget? viewLeading, Iterable<Widget>? viewTrailing, String? viewHintText, Color? viewBackgroundColor, double? viewElevation, Color? viewSurfaceTintColor, BorderSide? viewSide, OutlinedBorder? viewShape, EdgeInsetsGeometry? viewBarPadding, double? headerHeight, TextStyle? headerTextStyle, TextStyle? headerHintStyle, Color? dividerColor, BoxConstraints? viewConstraints, EdgeInsetsGeometry? viewPadding, bool? shrinkWrap, TextCapitalization? textCapitalization, ValueChanged<String>? viewOnChanged, ValueChanged<String>? viewOnSubmitted, VoidCallback? viewOnClose, VoidCallback? viewOnOpen, required SearchAnchorChildBuilder builder, required SuggestionsBuilder suggestionsBuilder, TextInputAction? textInputAction, TextInputType? keyboardType, bool enabled = true, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType})
Creates a const SearchAnchor.
const
SearchAnchor.bar({Widget? barLeading, Iterable<Widget>? barTrailing, String? barHintText, GestureTapCallback? onTap, ValueChanged<String>? onSubmitted, ValueChanged<String>? onChanged, VoidCallback? onClose, VoidCallback? onOpen, WidgetStateProperty<double?>? barElevation, WidgetStateProperty<Color?>? barBackgroundColor, WidgetStateProperty<Color?>? barOverlayColor, WidgetStateProperty<BorderSide?>? barSide, WidgetStateProperty<OutlinedBorder?>? barShape, WidgetStateProperty<EdgeInsetsGeometry?>? barPadding, EdgeInsetsGeometry? viewBarPadding, WidgetStateProperty<TextStyle?>? barTextStyle, WidgetStateProperty<TextStyle?>? barHintStyle, ViewBuilder? viewBuilder, Widget? viewLeading, Iterable<Widget>? viewTrailing, String? viewHintText, Color? viewBackgroundColor, double? viewElevation, BorderSide? viewSide, OutlinedBorder? viewShape, double? viewHeaderHeight, TextStyle? viewHeaderTextStyle, TextStyle? viewHeaderHintStyle, Color? dividerColor, BoxConstraints? constraints, BoxConstraints? viewConstraints, EdgeInsetsGeometry? viewPadding, bool? shrinkWrap, bool? isFullScreen, SearchController searchController, TextCapitalization textCapitalization, required SuggestionsBuilder suggestionsBuilder, TextInputAction? textInputAction, TextInputType? keyboardType, EdgeInsets scrollPadding, EditableTextContextMenuBuilder contextMenuBuilder, bool enabled, SmartDashesType? smartDashesType, SmartQuotesType? smartQuotesType})
Create a SearchAnchor that has a SearchBar which opens a search view.
factory

Properties

builder SearchAnchorChildBuilder
Called to create a widget which can open a search view route when it is tapped.
final
dividerColor Color?
The color of the divider on the search view.
final
enabled bool
Whether or not this widget is currently interactive.
final
hashCode int
The hash code for this object.
no setterinherited
headerHeight double?
The height of the search field on the search view.
final
headerHintStyle TextStyle?
The style to use for the viewHintText on the search view.
final
headerTextStyle TextStyle?
The style to use for the text being edited on the search view.
final
isFullScreen bool?
Whether the search view grows to fill the entire screen when the SearchAnchor is tapped.
final
key Key?
Controls how one widget replaces another widget in the tree.
finalinherited
keyboardType TextInputType?
The type of action button to use for the keyboard.
final
runtimeType Type
A representation of the runtime type of the object.
no setterinherited
searchController SearchController?
An optional controller that allows opening and closing of the search view from other widgets.
final
shrinkWrap bool?
Whether the search view should shrink-wrap its contents.
final
smartDashesType SmartDashesType?
Configures how smart dashes are handled in the text field used by this SearchAnchor.
final
smartQuotesType SmartQuotesType?
Configures how smart quotes are handled in the text field used by this SearchAnchor.
final
suggestionsBuilder SuggestionsBuilder
Called to get the suggestion list for the search view.
final
textCapitalization TextCapitalization?
Configures how the platform keyboard will select an uppercase or lowercase keyboard.
final
textInputAction TextInputAction?
The type of action button to use for the keyboard.
final
viewBackgroundColor Color?
The search view's background fill color.
final
viewBarPadding EdgeInsetsGeometry?
The padding to use for the search view's search bar.
final
viewBuilder ViewBuilder?
Optional callback to obtain a widget to lay out the suggestion list of the search view.
final
viewConstraints BoxConstraints?
Optional size constraints for the search view.
final
viewElevation double?
The elevation of the search view's Material.
final
viewHintText String?
Text that is displayed when the search bar's input field is empty.
final
viewLeading Widget?
An optional widget to display before the text input field when the search view is open.
final
viewOnChanged ValueChanged<String>?
Called each time the user modifies the search view's text field.
final
viewOnClose VoidCallback?
Called when the search view is closed.
final
viewOnOpen VoidCallback?
Called when the search view is opened.
final
viewOnSubmitted ValueChanged<String>?
Called when the user indicates that they are done editing the text in the text field of a search view. Typically this is called when the user presses the enter key.
final
viewPadding EdgeInsetsGeometry?
The padding to use for the search view.
final
viewShape OutlinedBorder?
The shape of the search view's underlying Material.
final
viewSide BorderSide?
The color and weight of the search view's outline.
final
viewSurfaceTintColor Color?
The surface tint color of the search view's Material.
final
viewTrailing Iterable<Widget>?
An optional widget list to display after the text input field when the search view is open.
final

Methods

createElement() StatefulElement
Creates a StatefulElement to manage this widget's location in the tree.
inherited
createState() State<SearchAnchor>
Creates the mutable state for this widget at a given location in the tree.
override
debugDescribeChildren() List<DiagnosticsNode>
Returns a list of DiagnosticsNode objects describing this node's children.
inherited
debugFillProperties(DiagnosticPropertiesBuilder properties) → void
Add additional properties associated with the node.
inherited
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
toDiagnosticsNode({String? name, DiagnosticsTreeStyle? style}) DiagnosticsNode
Returns a debug representation of the object that is used by debugging tools and by DiagnosticsNode.toStringDeep.
inherited
toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) String
A string representation of this object.
inherited
toStringDeep({String prefixLineOne = '', String? prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 65}) String
Returns a string representation of this node and its descendants.
inherited
toStringShallow({String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug}) String
Returns a one-line detailed description of the object.
inherited
toStringShort() String
A short, textual description of this widget.
inherited

Operators

operator ==(Object other) bool
The equality operator.
inherited