search_plus 3.0.0 copy "search_plus: ^3.0.0" to clipboard
search_plus: ^3.0.0 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 #

Flutter Dart License Platform

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

⚑ 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.


🧩 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 #

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 maxResults realistic 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 Material 3 search input
SearchResultsWidget<T> Results display (list / grid / sectioned)
SearchOverlay<T> Floating dropdown result panel
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
    β”œβ”€β”€ core/                     # Business logic
    β”‚   β”œβ”€β”€ search_controller.dart
    β”‚   β”œβ”€β”€ search_result.dart
    β”‚   β”œβ”€β”€ search_state.dart
    β”‚   β”œβ”€β”€ search_config.dart
    β”‚   └── search_history_storage.dart
    β”œβ”€β”€ l10n/                     # Localization
    β”‚   └── search_localizations.dart
    β”œβ”€β”€ theme/                    # Theming
    β”‚   └── search_theme.dart
    └── ui/                       # Widgets
        β”œβ”€β”€ search_scaffold.dart
        β”œβ”€β”€ search_bar_widget.dart
        β”œβ”€β”€ search_results_widget.dart
        β”œβ”€β”€ search_overlay.dart
        └── states/
            └── search_states.dart

Clean Code Philosophy #

  • Separation of concerns: UI, state, and data are fully decoupled
  • Immutable state: SearchState and SearchResult are immutable
  • Generic types: Full type safety with SearchAdapter<T>, SearchResult<T>
  • 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 searchableFields callback 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 #

  • SearchOverlay auto-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.enabled is true
  • Ensure animationConfig.preset is not SearchAnimationPreset.none
  • Try increasing duration for more noticeable effects

History isn't persisted #

  • The default InMemoryHistoryStorage loses data on restart
  • Use SecureFallbackHistoryStorage or implement SearchHistoryStorage for persistence

Import conflicts with Flutter's SearchBarThemeData #

  • Use import 'package:flutter/material.dart' hide SearchBarThemeData; to resolve conflicts

License #

MIT β€” see LICENSE.


Made with ❀️ for the Flutter community

3
likes
0
points
221
downloads

Publisher

unverified uploader

Weekly Downloads

A production-grade Flutter search package with async API, local, and hybrid search adapters, plus polished UI widgets, theming, animations, and localization support.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on search_plus