search_plus 3.2.3
search_plus: ^3.2.3 copied to clipboard
A production-grade Flutter search package with async API, local, and hybrid search adapters, plus polished UI widgets, theming, animations, and localization support.
π Search Plus #
Production-grade Flutter search β local, remote, and hybrid β with polished UI, overlay mode, theming, animations, persistent history, and a developer experience you'll love.
Ship fast, beautiful search experiences across mobile, web, and desktop using a clean adapter architecture and ready-to-use Material 3 components.
β¨ Key Features #
| Feature | Description |
|---|---|
| β‘ Async API Search | Debounced, cancellation-safe, paginated remote search |
| πΎ Local Search | Ranked matching β exact, prefix, contains, and fuzzy (Levenshtein) |
| π Hybrid Search | Merge local + remote results with weighting and deduplication |
| π§© Adapter Architecture | Plug in any data source via SearchAdapter<T> |
| πΌοΈ Built-in UI System | SearchScaffold, SearchPlusBar, SearchResultsWidget |
| πͺ Overlay / Dropdown Mode | SearchOverlay β floating results panel with auto-dismiss |
| ποΈ 7 Animation Presets | Fade, slide, scale, staggered β plus shimmer loading |
| π¨ Deep Theming | 30+ customizable properties with Material 3 defaults |
| βοΈ SearchConfig | Advanced behavior: debounce, trim, case, capitalization, limits |
| π½ Persistent History | Pluggable storage (in-memory, secure, or custom) |
| π Localization Ready | 13 customizable strings via SearchLocalizations |
| π§ Suggestions + History | Built-in support in controller and adapters |
| βΏ Accessible | Semantic labels, tooltips, keyboard-friendly |
| π± Responsive | Adaptive layouts for phone, tablet, and desktop |
π¦ Installation #
Add search_plus to your pubspec.yaml:
dependencies:
search_plus: ^1.0.0
Then run:
flutter pub get
π¬ Demo #
Visual examples of SearchPlus in action:
Search Input #
Live Suggestions #
Results UI #
Empty State / No Results #
Optional Feedback / Toast System #
Overlay Mode β floating dropdown results panel:
![]()
β‘ Quick Start #
Get a working search in under 30 seconds:
import 'package:flutter/material.dart';
import 'package:search_plus/search_plus.dart';
class QuickStartPage extends StatefulWidget {
const QuickStartPage({super.key});
@override
State<QuickStartPage> createState() => _QuickStartPageState();
}
class _QuickStartPageState extends State<QuickStartPage> {
late final SearchPlusController<String> controller;
@override
void initState() {
super.initState();
controller = SearchPlusController<String>(
adapter: LocalSearchAdapter<String>(
items: ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'],
searchableFields: (item) => [item],
toResult: (item) => SearchResult(id: item, title: item, data: item),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SearchScaffold<String>(
controller: controller,
hintText: 'Search fruits...',
),
);
}
}
That's it β debouncing, state management, empty/loading/error states, and animations are handled automatically.
π SearchPlusBar β The Generic Search Input #
SearchPlusBar is a standalone, fully generic Material 3 search input. Unlike
Flutter's built-in SearchBar, it is designed to slot into any screen, control
any data type, and be customized at the widget level without a global theme.
Why use SearchPlusBar? #
| Capability | SearchPlusBar |
Flutter SearchBar |
Manual TextField |
|---|---|---|---|
| Material 3 styling out of the box | β | β | β (manual) |
| Animated focus elevation & border | β automatic | β | β |
| Built-in clear / voice / filter buttons | β conditional | β | β |
| Debounce progress indicator | β opt-in | β | β |
readOnly + onTap (tap-to-navigate) |
β | β | β |
Direct textStyle / hintStyle override |
β | β | β |
Works with SearchPlusController<T> |
β plug-and-play | β | β |
| Standalone (no controller needed) | β | β | β |
Custom inputFormatters |
β | β | β |
Deep theming via SearchTheme |
β | β | β |
Localization via SearchLocalizations |
β automatic | β | β |
| Accessibility (semantic labels, tooltips) | β built-in | partial | β (manual) |
When to use SearchPlusBar #
Use it whenever you need a search input β it handles all the boilerplate:
| Scenario | How |
|---|---|
| Product catalog search | onChanged β controller.search() |
| Tap-to-open search page | readOnly: true + onTap |
| App bar search | Place inside AppBar with custom height: 44 |
| Settings / preference filter | Standalone with onChanged only |
| Chat message search | Pair with SearchPlusOverlay |
| Command palette / spotlight | autofocus: true + overlay mode |
| Number-only search (order IDs) | keyboardType: TextInputType.number + inputFormatters |
| Multi-language app | Wrap in SearchLocalizationsProvider |
SearchPlusBar examples #
1. Basic β search a list
SearchPlusBar(
onChanged: (query) => controller.search(query),
hintText: 'Search productsβ¦',
)
2. Tap-to-navigate (hero search bar)
A common pattern on home screens: a decorative search bar that, when tapped, pushes a dedicated search page.
SearchPlusBar(
readOnly: true,
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const FullSearchPage()),
),
hintText: 'Tap to searchβ¦',
leading: const Icon(Icons.search),
)
3. Styled inline β no theme wrapper needed
SearchPlusBar(
onChanged: (q) => controller.search(q),
hintText: 'Find a recipeβ¦',
textStyle: const TextStyle(fontSize: 18),
hintStyle: TextStyle(fontSize: 18, color: Colors.grey.shade400),
height: 52,
borderRadius: BorderRadius.circular(12),
backgroundColor: Colors.white,
elevation: 0,
)
4. With voice + filter actions
SearchPlusBar(
onChanged: (q) => controller.search(q),
onSubmitted: (q) => controller.addToHistory(q),
onVoiceSearch: () => startVoiceInput(),
onFilterPressed: () => showFilterSheet(context),
showDebounceIndicator: true,
)
5. Number-only input (order ID search)
SearchPlusBar(
hintText: 'Enter order numberβ¦',
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onSubmitted: (orderId) => lookUpOrder(orderId),
)
6. Inside an AppBar
AppBar(
title: SearchPlusBar(
height: 44,
onChanged: (q) => controller.search(q),
hintText: 'Search messagesβ¦',
borderRadius: BorderRadius.circular(22),
elevation: 0,
),
)
SearchPlusBar parameters #
| Parameter | Type | Default | Description |
|---|---|---|---|
onChanged |
ValueChanged<String>? |
β | Called on every text change |
onSubmitted |
ValueChanged<String>? |
β | Called on keyboard "search" action |
onTap |
VoidCallback? |
β | Called when bar is tapped (tap-to-navigate) |
onFocusChanged |
ValueChanged<bool>? |
β | Called when focus state changes |
controller |
TextEditingController? |
auto | External text controller |
focusNode |
FocusNode? |
auto | External focus node |
hintText |
String? |
localized | Placeholder text |
leading |
Widget? |
search icon | Leading widget |
trailing |
Widget? |
β | Trailing widget |
autofocus |
bool |
false |
Auto-request focus on mount |
enabled |
bool |
true |
Whether input is enabled |
readOnly |
bool |
false |
Display-only mode (combine with onTap) |
showClearButton |
bool |
true |
Show β button when text is present |
onVoiceSearch |
VoidCallback? |
β | Show π€ button; callback when pressed |
onFilterPressed |
VoidCallback? |
β | Show filter button; callback when pressed |
textInputAction |
TextInputAction |
search |
Keyboard action button |
textCapitalization |
TextCapitalization |
none |
Input capitalization |
keyboardType |
TextInputType? |
platform | Keyboard type (number, email, url, β¦) |
inputFormatters |
List<TextInputFormatter>? |
β | Input validation / formatting |
showDebounceIndicator |
bool |
false |
Show typing progress bar |
borderRadius |
BorderRadius? |
theme | Custom border radius |
elevation |
double? |
theme | Custom elevation |
backgroundColor |
Color? |
theme | Custom background color |
textStyle |
TextStyle? |
theme | Direct text style override |
hintStyle |
TextStyle? |
theme | Direct hint text style override |
height |
double? |
56 | Direct height override |
contentPadding |
EdgeInsets? |
zero | TextField content padding |
π SearchableListView β Ready-to-Use Searchable List #
SearchableListView<T> is a convenience widget that combines SearchPlusBar,
SearchPlusResults, and an internal SearchPlusController into a single,
drop-in widget. Just provide your items, tell it how to extract searchable
text, and how to convert each item to a SearchResult β everything else
(debouncing, state management, empty/loading/error states, animations) is
handled for you.
When to use
SearchableListViewvs. building manually:
Scenario Recommendation Simple searchable list with default layout β SearchableListViewCustom bar + results placement / overlay mode Use SearchPlusBar+SearchPlusResultsdirectlyNeed an external controller shared across widgets Use SearchPlusController+ individual widgets
Minimal example #
import 'package:flutter/material.dart';
import 'package:search_plus/search_plus.dart';
class FruitSearchPage extends StatelessWidget {
const FruitSearchPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Fruit Search')),
body: SearchableListView<String>(
items: const ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'],
searchableFields: (item) => [item],
toResult: (item) => SearchResult(id: item, title: item, data: item),
),
);
}
}
Custom model with item builder #
class Product {
final String id;
final String name;
final String category;
final String imageUrl;
const Product({
required this.id,
required this.name,
required this.category,
required this.imageUrl,
});
}
class ProductSearchPage extends StatelessWidget {
const ProductSearchPage({super.key});
@override
Widget build(BuildContext context) {
final products = [
const Product(id: '1', name: 'Laptop', category: 'Electronics', imageUrl: 'https://example.com/laptop.png'),
const Product(id: '2', name: 'Sneakers', category: 'Footwear', imageUrl: 'https://example.com/sneakers.png'),
const Product(id: '3', name: 'Backpack', category: 'Accessories', imageUrl: 'https://example.com/backpack.png'),
];
return Scaffold(
appBar: AppBar(title: const Text('Products')),
body: SearchableListView<Product>(
items: products,
searchableFields: (p) => [p.name, p.category],
toResult: (p) => SearchResult(
id: p.id,
title: p.name,
subtitle: p.category,
data: p,
),
hintText: 'Search productsβ¦',
enableFuzzySearch: true,
itemBuilder: (context, result, index) => ListTile(
leading: Image.network(result.data!.imageUrl, width: 48, height: 48),
title: Text(result.title),
subtitle: Text(result.subtitle ?? ''),
),
onItemTap: (result) {
// Navigate to product detail
debugPrint('Selected: ${result.title}');
},
),
);
}
}
Grid layout with theming #
SearchableListView<Product>(
items: products,
searchableFields: (p) => [p.name, p.category],
toResult: (p) => SearchResult(id: p.id, title: p.name, data: p),
layout: SearchResultsLayout.grid,
gridCrossAxisCount: 3,
gridChildAspectRatio: 0.75,
animationConfig: const SearchAnimationConfig(
type: SearchAnimationType.staggeredSlide,
),
theme: SearchPlusThemeData(
barTheme: SearchBarThemeData(
fillColor: Colors.grey.shade100,
borderRadius: BorderRadius.circular(16),
),
),
)
SearchableListView parameters #
| Parameter | Type | Default | Description |
|---|---|---|---|
items |
List<T> |
required | Items to search through |
searchableFields |
List<String> Function(T) |
required | Extracts searchable text from each item |
toResult |
SearchResult<T> Function(T) |
required | Converts an item into a SearchResult |
itemBuilder |
Widget Function(β¦)? |
null | Custom builder for each result row |
onItemTap |
void Function(SearchResult<T>)? |
null | Called when a result is tapped |
onQueryChanged |
ValueChanged<String>? |
null | Called each time the query text changes |
hintText |
String? |
null | Search bar placeholder |
autofocus |
bool |
false |
Auto-focus the search bar on mount |
showClearButton |
bool |
true |
Show clear (Γ) button when text is present |
debounceDuration |
Duration |
300 ms | Debounce before executing search |
minQueryLength |
int |
1 | Minimum characters to trigger search |
maxResults |
int |
50 | Maximum number of results |
enableFuzzySearch |
bool |
false |
Enable Levenshtein-distance matching |
layout |
SearchResultsLayout |
.list |
.list or .grid |
density |
SearchResultDensity |
.comfortable |
Compact / comfortable / expanded |
animationConfig |
SearchAnimationConfig |
default | Animation preset and timing |
leading |
Widget? |
null | Leading widget in the search bar |
trailing |
Widget? |
null | Trailing widget in the search bar |
onVoiceSearch |
VoidCallback? |
null | Voice-search callback (shows mic icon) |
onFilterPressed |
VoidCallback? |
null | Filter callback (shows filter icon) |
idleBuilder |
Widget Function(BuildContext)? |
null | Widget to show when no query is entered |
headerBuilder |
Widget Function(β¦)? |
null | Header above results |
footerBuilder |
Widget Function(β¦)? |
null | Footer below results |
separatorBuilder |
Widget Function(β¦)? |
null | Custom separator between list items |
theme |
SearchPlusThemeData? |
null | Theme override |
localizations |
SearchLocalizations? |
null | Localization override |
physics |
ScrollPhysics? |
null | Scroll physics for the list |
shrinkWrap |
bool |
false |
Shrink-wrap the results list |
π Using SearchableListView with Custom Widgets #
SearchableListView is a self-contained widget β it creates and manages its
own SearchPlusController and LocalSearchAdapter internally. This means
third-party or custom widgets (like the NovaDrawerSearchBar example below)
can sit alongside or wrap SearchableListView without conflicts.
How NovaDrawerSearchBar works with search_plus #
The NovaDrawerSearchBar from the NovaDrawer package is a great example of
how external libraries build on search_plus. Here's how it works:
- Controller-based constructor β accepts an external
SearchPlusControllerthat the caller creates and owns. The drawer bar delegates all search logic to the controller. - Simple constructor β accepts raw
items,searchableFields, andtoResult, then builds aLocalSearchAdapterandSearchPlusControllerinternally (the same patternSearchableListViewuses). - Wraps
SearchPlusBarβ under the hood it renders aSearchPlusBarwith drawer-friendly defaults (padding, overlay toggle, animation config).
Example: SearchableListView inside a drawer alongside NovaDrawerSearchBar
import 'package:flutter/material.dart';
import 'package:search_plus/search_plus.dart';
class AppDrawer extends StatelessWidget {
const AppDrawer({super.key});
@override
Widget build(BuildContext context) {
final menuItems = ['Home', 'Settings', 'Profile', 'Help', 'About'];
return Drawer(
child: SafeArea(
child: SearchableListView<String>(
items: menuItems,
searchableFields: (item) => [item],
toResult: (item) => SearchResult(
id: item,
title: item,
data: item,
),
hintText: 'Search menuβ¦',
shrinkWrap: false,
onItemTap: (result) {
Navigator.of(context).pop(); // close drawer
// navigate to selected menu item
},
idleBuilder: (context) => ListView(
children: menuItems
.map((item) => ListTile(
title: Text(item),
onTap: () => Navigator.of(context).pop(),
))
.toList(),
),
),
),
);
}
}
Example: Sharing a controller between NovaDrawerSearchBar and SearchableListView
If your custom search bar (like NovaDrawerSearchBar) exposes a
SearchPlusController, you can share that controller with other widgets.
However, SearchableListView manages its own controller, so use
SearchPlusBar + SearchPlusResults directly when you need a shared
controller:
class SharedControllerExample extends StatefulWidget {
const SharedControllerExample({super.key});
@override
State<SharedControllerExample> createState() =>
_SharedControllerExampleState();
}
class _SharedControllerExampleState extends State<SharedControllerExample> {
late final SearchPlusController<String> controller;
@override
void initState() {
super.initState();
controller = SearchPlusController<String>(
adapter: LocalSearchAdapter<String>(
items: ['Home', 'Settings', 'Profile', 'Help'],
searchableFields: (item) => [item],
toResult: (item) => SearchResult(id: item, title: item, data: item),
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Your custom search bar (or NovaDrawerSearchBar)
// drives the shared controller
SearchPlusBar(
onChanged: (query) => controller.search(query),
hintText: 'Search menuβ¦',
),
// Results react to the same controller
Expanded(
child: ListenableBuilder(
listenable: controller,
builder: (context, _) => SearchPlusResults<String>(
state: controller.state,
onItemTap: (result) => debugPrint('Tapped: ${result.title}'),
),
),
),
],
);
}
}
Key takeaway: Use
SearchableListViewwhen you want an all-in-one solution. UseSearchPlusBar+SearchPlusResults+ your ownSearchPlusControllerwhen you need to share state between multiple widgets (e.g. a custom drawer search bar that drives a results panel elsewhere).
π Why Search Plus vs. Alternatives #
| Feature | search_plus | DIY TextField + FutureBuilder |
Flutter SearchBar + SearchAnchor |
Other pub packages |
|---|---|---|---|---|
| Zero boilerplate for full search UX | β | β lots of code | partial | varies |
| Adapter architecture (swap data source) | β | β | β | rare |
| Local + Remote + Hybrid in one package | β | β | β | rare |
| Ranked local search (exact β fuzzy) | β | β | β | some |
| Overlay and inline result modes | β | manual | overlay only | varies |
| 7 animation presets + shimmer | β | β | β | some |
| Persistent search history | β pluggable | β | β | some |
| Theming (30+ properties) | β | β | limited | varies |
| Localization (13 strings) | β | β | β | rare |
Pagination (loadMore) |
β | manual | β | some |
| Zero runtime dependencies | β | β | β | β (often) |
Type-safe generics <T> |
β | β | β | varies |
Where search_plus is most useful #
- E-commerce apps β product search with images, categories, filters, and price ranges.
- Social / messaging apps β search users, messages, or channels with overlay dropdown.
- Content apps (news, blogs, docs) β full-text search with highlighting.
- Enterprise dashboards β search tables, reports, or records with pagination.
- Settings / preference screens β filter long lists with a minimal search bar.
- Multi-source apps β combine local cache + remote API with
HybridSearchAdapter. - Offline-first apps β
LocalSearchAdapterworks without network,HybridSearchAdapterfalls back gracefully.
π§© Examples #
The /example app ships with seven runnable examples, from minimal to full showcase. Run them with:
cd example
flutter run
1. Basic Example #
Minimal local search with a flat string list and zero custom UI.
SearchPlusController<String>(
adapter: LocalSearchAdapter<String>(
items: fruits,
searchableFields: (item) => [item],
toResult: (item) => SearchResult(id: item, title: item, data: item),
),
);
2. Intermediate Example #
Custom item builder, theming, fuzzy search, and stagger animations.
SearchTheme(
data: SearchThemeData(
searchBarTheme: SearchBarThemeData(
borderRadius: BorderRadius.circular(16),
focusedBorderColor: colorScheme.primary,
),
resultTheme: SearchResultThemeData(
highlightColor: colorScheme.primaryContainer,
),
),
child: SearchScaffold<Product>(
controller: controller,
animationConfig: const SearchAnimationConfig(
preset: SearchAnimationPreset.staggered,
),
density: SearchResultDensity.rich,
itemBuilder: (context, result, index) => MyProductTile(result),
),
)
3. Advanced Example #
Remote API search with suggestions, search history, trending items, and a toggleable list/grid layout.
SearchPlusController<AppUser>(
adapter: RemoteSearchAdapter<AppUser>(
searchFunction: api.searchUsers,
suggestFunction: api.suggestUsers,
),
debounceDuration: const Duration(milliseconds: 400),
maxHistoryItems: 8,
);
4. Full Showcase #
A complete screen with tabs (Results / Suggestions / Trending), category filter chips, custom product cards, and all states (loading, empty, error, results).
5. Original Example #
The comprehensive demo: local + remote + hybrid modes, theme switching, localization overrides, animation presets, keyboard navigation, and density settings.
6. Overlay Example #
Floating dropdown results that appear above page content. Demonstrates the SearchOverlay widget with auto-dismiss on outside tap and smooth animations.
SearchOverlay<Product>(
controller: controller,
hintText: 'Search productsβ¦',
maxOverlayHeight: 350,
animationConfig: const SearchAnimationConfig(
preset: SearchAnimationPreset.fadeSlideUp,
),
onItemTap: (result) => print('Selected: ${result.title}'),
)
7. π§ͺ Interactive Demo (searchplus_demo.dart) #
A dedicated test/demo screen with a control panel drawer for:
- Dataset: Products / Users / Articles
- Style: Minimal / Modern SaaS / Dark / Social / Glass / Dark Premium (6 styles)
- Animation: All 7 presets
- Layout: List / Grid
- Density: Compact / Comfortable / Rich
- Forced State: Auto / Loading / Empty / Error
- Result Mode: Inline / Overlay dropdown toggle
- API Delay: 100 ms β 3000 ms slider
Perfect for recording demo videos.
βοΈ Configuration Options #
Use SearchConfig for advanced control over search behavior:
const config = SearchConfig(
debounceDuration: Duration(milliseconds: 400),
minQueryLength: 2,
maxResultCount: 30,
trimInput: true,
caseSensitive: false,
inputTransformation: InputTransformation.lowercase,
autoCorrect: true,
textCapitalization: TextCapitalization.none,
searchInTitle: true,
searchInSubtitle: true,
searchInTags: true,
recentHistoryEnabled: true,
maxHistoryItems: 10,
overlayEnabled: false,
overlayMaxHeight: 400,
animationEnabled: true,
);
Config Properties #
| Property | Type | Default | Description |
|---|---|---|---|
debounceDuration |
Duration |
300ms | Debounce delay before search triggers |
minQueryLength |
int |
1 | Min characters before search starts |
maxResultCount |
int |
50 | Maximum results returned |
trimInput |
bool |
true |
Trim whitespace from input |
caseSensitive |
bool |
false |
Case-sensitive matching |
inputTransformation |
InputTransformation |
none |
Transform query: none, lowercase, uppercase |
autoCorrect |
bool |
true |
Enable autocorrect on text field |
textCapitalization |
TextCapitalization |
none |
Input capitalization mode |
searchInTitle |
bool |
true |
Search in result titles |
searchInSubtitle |
bool |
true |
Search in subtitles |
searchInTags |
bool |
true |
Search in tags/metadata |
recentHistoryEnabled |
bool |
true |
Enable search history |
maxHistoryItems |
int |
10 | Max history items to keep |
overlayEnabled |
bool |
false |
Use overlay dropdown mode |
overlayMaxHeight |
double |
400 | Max height of overlay panel |
animationEnabled |
bool |
true |
Enable animations |
πͺ Overlay Mode #
Search Plus offers two result presentation modes:
Inline Mode (Default) #
Results appear below the search bar in the page flow:
SearchScaffold<Product>(
controller: controller,
hintText: 'Search...',
)
Overlay / Dropdown Mode #
Results float above page content in a dismissible panel:
SearchOverlay<Product>(
controller: controller,
hintText: 'Search productsβ¦',
maxOverlayHeight: 400,
overlayElevation: 8,
closeOnSelect: true,
animationConfig: const SearchAnimationConfig(
preset: SearchAnimationPreset.fadeSlideUp,
),
onItemTap: (result) => handleSelection(result),
itemBuilder: (context, result, index) => ListTile(
title: Text(result.title),
subtitle: Text(result.subtitle ?? ''),
),
)
Overlay behavior:
- Opens when results become available
- Closes on outside tap, Escape, or focus loss
- Smooth fade-in/out animation
- Respects all theming and animation configs
- Works on mobile, tablet, and desktop
π½ Search History Storage #
Search Plus provides a pluggable history storage system:
In-Memory (Default) #
History lives only in memory β lost on app restart:
final manager = SearchHistoryManager(maxItems: 10);
await manager.add('flutter widgets');
print(manager.items); // ['flutter widgets']
Custom Persistent Storage #
Implement SearchHistoryStorage for any backend:
class SharedPrefsHistoryStorage extends SearchHistoryStorage {
final SharedPreferences prefs;
SharedPrefsHistoryStorage(this.prefs);
@override
Future<List<String>> load() async {
return prefs.getStringList('search_history') ?? [];
}
@override
Future<void> save(List<String> history) async {
await prefs.setStringList('search_history', history);
}
@override
Future<void> clear() async {
await prefs.remove('search_history');
}
}
Secure Fallback Storage #
For secure storage backends:
final storage = SecureFallbackHistoryStorage(
readFn: () => secureStorage.read(key: 'history') ?? '',
writeFn: (data) => secureStorage.write(key: 'history', value: data),
deleteFn: () => secureStorage.delete(key: 'history'),
);
final manager = SearchHistoryManager(
maxItems: 10,
storage: storage,
);
await manager.load(); // Load from storage on startup
History Features #
- Deduplication: Same query won't appear twice
- Max count: Oldest entries are dropped automatically
- Remove individual:
manager.remove('old query') - Clear all:
manager.clearAll() - Persistent: Survives app restarts with custom storage
π§© Fake API / Demo Mode #
The example app includes a FakeSearchApi for realistic demos:
final api = FakeSearchApi(
minDelay: Duration(milliseconds: 200),
maxDelay: Duration(milliseconds: 800),
errorRate: 0.0, // Set > 0 to simulate errors
);
// Search users, products, or articles
final results = await api.searchUsers('sarah');
final products = await api.searchProducts('keyboard');
final articles = await api.searchArticles('flutter');
// Suggestions
final suggestions = await api.suggestProducts('wire');
Features:
- Configurable simulated delay
- Configurable error rate for error state testing
- Three datasets: users (10 items), products (12 items), articles (8 items)
- Trending searches and recent search samples included
- Deterministic results for reproducible demos
π§ Core Concepts #
Adapter Architecture #
ββββββββββββββββββββ
β SearchAdapter<T> β β Abstract base
ββββββββββ¬ββββββββββ
β
ββββββ΄βββββββββββββββββββββ¬βββββββββββββββββββββββ
β β β
βββββ΄βββββββββββ ββββββββββββ΄ββββββββββ ββββββββββ΄ββββββββββββ
β LocalSearch β β RemoteSearch β β HybridSearch β
β Adapter<T> β β Adapter<T> β β Adapter<T> β
β β β β β β
β In-memory β β Future-based β β Merges local + β
β with ranking β β async delegation β β remote with dedup β
ββββββββββββββββ ββββββββββββββββββββββ ββββββββββββββββββββββ
Local adapter ranks results using a scoring strategy:
- Exact match β 1.0 Γ
boostExactMatch - Prefix match β 0.9 Γ
boostPrefixMatch - Word-start match β 0.8
- Contains match β 0.6
- Fuzzy match β similarity Γ 0.4
Remote adapter wraps any Future-based search function.
Hybrid adapter runs both in parallel, merges results, and deduplicates by ID.
State Machine #
ββββββββββββββββββββ β SearchAdapter
idle ββsearch()βββΈ loading ββresultsβββΈ success
β β
βββno resultsβββΈ empty β
β β
βββerrorβββΈ error βββββββ
Every state transition is smooth β the UI handles each automatically with customizable widgets.
π¨ Theming Guide #
Wrap any search widget in SearchTheme to customize visuals:
SearchTheme(
data: SearchThemeData(
searchBarTheme: SearchBarThemeData(
borderRadius: BorderRadius.circular(18),
focusedBorderColor: Colors.deepPurple,
elevation: 0,
focusedElevation: 4,
),
resultTheme: SearchResultThemeData(
highlightColor: Colors.deepPurple.shade100,
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
child: SearchScaffold<String>(controller: controller),
)
Available Theme Properties #
Search Bar (SearchBarThemeData):
backgroundColor, focusedBackgroundColor, borderRadius, elevation, focusedElevation, padding, height, textStyle, hintStyle, iconColor, cursorColor, borderColor, focusedBorderColor, borderWidth, shadowColor
Results (SearchResultThemeData):
backgroundColor, selectedColor, hoveredColor, titleStyle, subtitleStyle, highlightColor, highlightStyle, dividerColor, iconColor, sectionHeaderStyle, sectionHeaderBackgroundColor, contentPadding, itemSpacing, imageSize, imageBorderRadius
6 Built-in Style Presets (in Demo) #
| Preset | Look & Feel |
|---|---|
| Minimal | Clean flat borders, zero elevation |
| Modern SaaS | Rounded bars, subtle shadows, primary highlights |
| Dark | Dark backgrounds, teal accents |
| Social | Pill-shaped bar, compact items |
| Glass | Glassmorphism on gradient background |
| Dark Premium | Deep navy + red accent, elevated shadows |
ποΈ Animations #
Seven built-in animation presets:
| Preset | Effect |
|---|---|
none |
No animation |
fade |
Fade in |
slideUp |
Slide up from bottom |
slideRight |
Slide in from left |
scale |
Scale from small to full |
fadeSlideUp |
Combined fade + slide up |
staggered |
Each item animates with a delay |
SearchScaffold<String>(
controller: controller,
animationConfig: const SearchAnimationConfig(
preset: SearchAnimationPreset.staggered,
duration: Duration(milliseconds: 280),
staggerDelay: Duration(milliseconds: 40),
curve: Curves.easeOutCubic,
),
)
Shimmer loading is enabled by default β disable it with showShimmer: false.
π Localization #
Override any string with SearchLocalizationsProvider:
SearchLocalizationsProvider(
localizations: const SearchLocalizations(
hintText: 'Buscar...',
emptyResultsText: 'Sin resultados',
errorText: 'Algo saliΓ³ mal',
retryText: 'Reintentar',
loadingText: 'Buscando...',
resultsCountText: '{count} resultados',
),
child: SearchScaffold<String>(controller: controller),
)
All 13 strings are customizable: hintText, emptyResultsText, emptyResultsSubtext, errorText, retryText, clearText, cancelText, searchHistoryTitle, suggestionsTitle, loadingText, resultsCountText, voiceSearchTooltip, clearSearchTooltip.
π± Responsive Behavior #
Search Plus adapts to any screen size:
- Mobile (< 600dp): Full-width search bar, list layout, comfortable density
- Tablet (600β900dp): Wider content area, optional grid layout
- Desktop (> 900dp): Constrained max-width, grid layouts shine
Switch layouts dynamically:
SearchScaffold<Product>(
controller: controller,
layout: isWide ? SearchResultsLayout.grid : SearchResultsLayout.list,
gridCrossAxisCount: isWide ? 3 : 2,
density: isCompact
? SearchResultDensity.compact
: SearchResultDensity.comfortable,
)
π¬ Demo Video Scenarios #
Use the Interactive Demo screen to record these scenarios:
| # | Scenario | Settings |
|---|---|---|
| 1 | Basic search | Products, SaaS style, fadeSlideUp, List |
| 2 | Loading state | Force: Loading, 2000 ms delay |
| 3 | Empty state | Search "xyz", observe empty UI |
| 4 | Error & retry | Force: Error, then tap retry |
| 5 | Grid layout | Products, Grid, Rich density |
| 6 | Dark mode | Dark style, staggered animation |
| 7 | Glassmorphism | Glass style, scale animation |
| 8 | Social app feel | Users dataset, Social style |
| 9 | Overlay mode | Toggle overlay on, search products |
| 10 | Dark Premium | Premium style, staggered animation |
π Performance Notes #
- Use local adapter for low-latency offline search
- Debouncing reduces unnecessary network calls for remote APIs
- Keep
maxResultsrealistic for your UI layout and device class - Consider caching remote results for hybrid experiences
- Use sectioned or paged strategies for very large datasets
- Fuzzy search adds overhead β enable only when needed
π Search System Explained #
Debouncing #
Every keystroke is debounced (default: 300 ms). Only the latest query's results are shown β stale responses from earlier keystrokes are automatically discarded.
SearchPlusController<T>(
adapter: adapter,
debounceDuration: const Duration(milliseconds: 450),
minQueryLength: 2,
maxResults: 30,
);
Suggestions #
Both LocalSearchAdapter and RemoteSearchAdapter support suggestions:
// Local: prefix-based suggestions come built-in
// Remote: provide your own
RemoteSearchAdapter<T>(
searchFunction: api.search,
suggestFunction: (query) => api.suggest(query),
);
Search History #
The controller automatically tracks search history:
controller.addToHistory('flutter');
controller.state.history; // ['flutter']
controller.clearHistory();
For persistent history, use SearchHistoryManager with a custom SearchHistoryStorage.
π§© Extensibility #
Implement SearchAdapter<T> to integrate any data source:
class AlgoliaSearchAdapter extends SearchAdapter<Product> {
@override
Future<List<SearchResult<Product>>> search(
String query, {int limit = 50, int offset = 0}
) async {
final response = await algolia.search(query);
return response.hits.map((hit) => SearchResult<Product>(
id: hit.objectID,
title: hit['name'],
subtitle: hit['description'],
score: hit.score,
data: Product.fromAlgolia(hit),
)).toList();
}
}
Works with: REST, GraphQL, gRPC, Hive, Isar, SQLite, Elasticsearch, Algolia, and more.
π€ API Reference #
Core Classes #
| Class | Purpose |
|---|---|
SearchPlusController<T> |
Main controller β manages search, debouncing, history |
SearchResult<T> |
Immutable result model with score, metadata, source |
SearchState<T> |
Immutable state: query, results, status, suggestions, history |
SearchStatus |
Enum: idle, loading, success, empty, error |
SearchConfig |
Advanced behavior options: debounce, trim, case, limits |
SearchHistoryManager |
Manages history with dedup, limits, and persistence |
SearchHistoryStorage |
Abstract interface for history storage backends |
Adapters #
| Adapter | Purpose |
|---|---|
SearchAdapter<T> |
Abstract base β implement for custom sources |
LocalSearchAdapter<T> |
In-memory with ranked matching |
RemoteSearchAdapter<T> |
Wraps any async search function |
HybridSearchAdapter<T> |
Merges local + remote with deduplication |
SearchRankingConfig |
Tuning: weights, fuzzy threshold, boost factors |
UI Widgets #
| Widget | Purpose |
|---|---|
SearchScaffold<T> |
Complete search UI (bar + results + states) |
SearchPlusBar |
Standalone generic Material 3 search input (see deep-dive) |
SearchPlusResults<T> |
Results display (list / grid / sectioned) |
SearchPlusOverlay<T> |
Floating dropdown result panel |
SuggestionChips |
Trending / auto-complete suggestion chips |
SearchHistoryList |
Recent search history with remove actions |
SearchableListView<T> |
All-in-one searchable list (bar + results + controller) |
ScrollToTopButton |
FAB that appears on scroll |
SearchEmptyState |
No-results UI |
SearchErrorState |
Error UI with retry |
SearchLoadingState |
Loading UI with shimmer |
HighlightText |
Highlights matching query in text |
AnimatedSearchItem |
Wraps items with animation |
ShimmerLoading |
Skeleton loading effect |
Theming & Localization #
| Class | Purpose |
|---|---|
SearchTheme |
InheritedWidget for theme propagation |
SearchThemeData |
Theme configuration container |
SearchBarThemeData |
Search bar visual properties |
SearchResultThemeData |
Result item visual properties |
SearchLocalizationsProvider |
InheritedWidget for l10n propagation |
SearchLocalizations |
All customizable strings |
π§ Developer Notes #
Architecture Overview #
lib/
βββ search_plus.dart # Public API barrel file
βββ src/
βββ adapters/ # Data source abstractions
β βββ search_adapter.dart
β βββ local_search_adapter.dart
β βββ remote_search_adapter.dart
β βββ hybrid_search_adapter.dart
βββ animations/ # Animation system
β βββ animation_presets.dart
βββ cache/ # Caching layer
β βββ search_cache.dart
β βββ cached_search_adapter.dart
βββ core/ # Business logic
β βββ search_controller.dart
β βββ search_result.dart
β βββ search_state.dart
β βββ search_config.dart
β βββ search_plus_config.dart
β βββ search_history_storage.dart
βββ l10n/ # Localization
β βββ search_localizations.dart
βββ remote/ # Enhanced remote features
β βββ remote_search_config.dart
β βββ retry_strategy.dart
β βββ cancellable_operation.dart
β βββ query_deduplicator.dart
β βββ enhanced_remote_adapter.dart
βββ theme/ # Theming
β βββ search_theme.dart
βββ utils/ # Utilities
β βββ search_logger.dart
βββ ui/ # Widgets
βββ search_scaffold.dart
βββ search_bar_widget.dart β SearchPlusBar (generic search input)
βββ search_results_widget.dart
βββ search_overlay.dart
βββ suggestion_chips.dart
βββ search_history_list.dart
βββ scroll_to_top_button.dart
βββ debug/
β βββ search_debug_panel.dart
βββ devtools/
β βββ search_devtools_panel.dart
βββ pro/
β βββ skeleton_loading.dart
β βββ highlight_text.dart
β βββ glassmorphism_container.dart
β βββ search_plus_screen.dart
βββ states/
βββ search_states.dart
Clean Code Philosophy #
- Separation of concerns: UI, state, and data are fully decoupled
- Immutable state:
SearchStateandSearchResultare immutable - Generic types: Full type safety with
SearchAdapter<T>,SearchResult<T>,SearchPlusController<T> - Composable widgets: Use
SearchPlusBaralone, pair it withSearchPlusResults, or use the all-in-oneSearchScaffoldβ your choice - No external dependencies: Zero runtime dependencies beyond Flutter SDK
- Tree-shakeable: Import only what you use
π Troubleshooting #
Search returns "No results found" unexpectedly #
- Verify your
searchableFieldscallback returns the right strings - Check
minQueryLengthβ queries shorter than this won't trigger search - Ensure your fake/remote API actually matches the query (case-insensitive by default)
Overlay doesn't close on outside tap #
SearchOverlayauto-closes on focus loss with a 150ms delay- If using custom focus management, ensure the focus node can lose focus
Animations are not visible #
- Check
animationConfig.enabledistrue - Ensure
animationConfig.presetis notSearchAnimationPreset.none - Try increasing
durationfor more noticeable effects
History isn't persisted #
- The default
InMemoryHistoryStorageloses data on restart - Use
SecureFallbackHistoryStorageor implementSearchHistoryStoragefor persistence
Import conflicts with Flutter's SearchBarThemeData #
- Use
import 'package:flutter/material.dart' hide SearchBarThemeData;to resolve conflicts
SearchPlusBar onTap not firing #
- Ensure
enabledistrue(the default). A disabled bar ignores taps. - If using
readOnly: true, theonTapcallback fires on theTextFieldtap β make sure the bar is not obscured by another widget.
π Getting Started Checklist #
New to search_plus? Follow this path:
- Install β add
search_plustopubspec.yamland runflutter pub get. - Pick an adapter β
LocalSearchAdapterfor in-memory,RemoteSearchAdapterfor API, orHybridSearchAdapterfor both. - Create a controller β
SearchPlusController<YourModel>(adapter: yourAdapter). - Drop in a widget β start with
SearchScaffoldfor the full experience, orSearchPlusBar+SearchPlusResultsfor more control. - Customise β apply a
SearchTheme, add animations, tweakSearchConfig, or override localizations. - Ship π
License #
MIT β see LICENSE.
Made with β€οΈ for the Flutter community