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
-
- Object
- DiagnosticableTree
- Widget
- StatefulWidget
- SearchAnchor
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