Skip to main content

simple_search_dropdown 8.0.0 copy "simple_search_dropdown: ^8.0.0" to clipboard
simple_search_dropdown: ^8.0.0 copied to clipboard

Searchable single and multi-select dropdowns for Flutter with accent-insensitive search, add/edit/delete of items, keyboard navigation and Material 3 theming.

example/lib/main.dart

// The "Print ..." buttons write to the console on purpose: they show what
// the app reads back from the controllers.
// ignore_for_file: avoid_print

import 'package:flutter/material.dart';
import 'package:simple_search_dropdown/simple_search_dropdown.dart';
import 'package:simple_search_dropdown_example/custom.dart';
import 'package:simple_search_dropdown_example/dialog_test.dart';
import 'package:simple_search_dropdown_example/listitems.dart';

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

/// The app's Material 3 theme, seeded from the package's [darkGreen].
ThemeData exampleTheme(Brightness brightness) => ThemeData(
      colorScheme: ColorScheme.fromSeed(
        seedColor: darkGreen,
        brightness: brightness,
      ),
      useMaterial3: true,
    );

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Simple Search Dropdown',
      debugShowCheckedModeBanner: false,
      theme: exampleTheme(Brightness.light),
      // Follows the system setting: the dropdowns and dialogs take their
      // surfaces from the theme.
      darkTheme: exampleTheme(Brightness.dark),
      home: const MyHomePage(),
    );
  }
}

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

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

/// Three dropdowns: a single and a multiple one over a list the app shares
/// between them, and a single one over a custom value type.
///
/// Each controller keeps its own copy of the items. The app owns [items]:
/// the add/edit/delete callbacks of one dropdown update it and hand it to
/// the other dropdown with `updateListItems`.
class _MyHomePageState extends State<MyHomePage> {
  final List<ValueItem> items = List.of(initialItems);

  late final SearchDropDownController singleController;
  late final MultipleSearchDropDownController multipleController;
  late final SearchDropDownController<Custom> customController;

  ValueItem? selectedSingleItem = initialItems[1];
  List<ValueItem> selectedMultipleItems = [];
  ValueItem<Custom>? selectedCustomItem = initialCustomItems.first;

  @override
  void initState() {
    super.initState();

    singleController = SearchDropDownController(
      items: items,
      initialSelectedItem: selectedSingleItem,
      newValueItem: (input) => ValueItem(label: input, value: input),
      // Slow callbacks: the row shows a spinner until they complete.
      onAddItem: (item) async {
        await Future<void>.delayed(const Duration(seconds: 2));
        items.add(item);
        syncMultiple();
      },
      onDeleteItem: (item) async {
        await Future<void>.delayed(const Duration(seconds: 2));
        items.remove(item);
        syncMultiple();
      },
      confirmDelete: true,
      editMode: true,
      onEditItem: (item, edited) {
        replaceItem(item, edited);
        syncMultiple(edited: item, by: edited);
      },
      updateSelectedItem: (item) => selectedSingleItem = item,
      verifyInputItem: (item) => item.label != 'name',
      sort: SortType.selectedFirst,
      enabled: false,
    );

    multipleController = MultipleSearchDropDownController(
      items: items,
      selectedItems: selectedMultipleItems,
      updateSelectedItems: (selected) => selectedMultipleItems = selected,
      newValueItem: (input) => ValueItem(label: input, value: input),
      onAddItem: (item) {
        items.add(item);
        syncSingle();
      },
      onDeleteItem: (item) {
        items.remove(item);
        syncSingle();
      },
      confirmDelete: true,
      editMode: true,
      onEditItem: (item, edited) {
        replaceItem(item, edited);
        syncSingle(edited: item, by: edited);
      },
      onClearList: () => showMessage('Selection cleared'),
      onClearItem: (item) => showMessage('Removed ${item.label}'),
    );

    customController = SearchDropDownController<Custom>(
      items: initialCustomItems,
      initialSelectedItem: selectedCustomItem,
      addMode: false,
      // Nothing else shows these items, so the controller's copy is enough.
      onDeleteItem: (item) => showMessage('Deleted ${item.label}'),
      confirmDelete: true,
      updateSelectedItem: (item) => selectedCustomItem = item,
      sort: SortType.selectedFirst,
    );
  }

  @override
  void dispose() {
    singleController.dispose();
    multipleController.dispose();
    customController.dispose();
    super.dispose();
  }

  /// Confirms a callback with a short floating SnackBar.
  void showMessage(String message) {
    if (!mounted) {
      return;
    }
    ScaffoldMessenger.of(context)
      ..hideCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          content: Text(message),
          behavior: SnackBarBehavior.floating,
          duration: const Duration(seconds: 2),
        ),
      );
  }

  void replaceItem(ValueItem item, ValueItem edited) {
    final int index = items.indexOf(item);
    if (index >= 0) {
      items[index] = edited;
    }
  }

  /// Hands [items] to the single dropdown, keeping its selection (following
  /// it when the selected item was [edited] into [by]).
  void syncSingle({ValueItem? edited, ValueItem? by}) {
    ValueItem? selected = singleController.selectedItem;
    if (edited != null && selected == edited) {
      selected = by;
    }
    // updateListItems resets a selection; select it again if it is still there.
    singleController.updateListItems(items, false);
    if (selected != null && items.contains(selected)) {
      singleController.selectItem(selected);
    }
  }

  /// Hands [items] to the multiple dropdown; a selected item that was
  /// [edited] stays selected as [by].
  void syncMultiple({ValueItem? edited, ValueItem? by}) {
    final bool wasSelected =
        edited != null && multipleController.isSelected(edited);
    multipleController.updateListItems(items, false);
    if (wasSelected && by != null) {
      multipleController.toggle(by, forced: true);
    }
  }

  void resetCustom() {
    customController.updateListItems(<ValueItem<Custom>>[
      ValueItem(label: 'Reset 1', value: Custom('Reset 1', 80)),
      ValueItem(label: 'Reset 2', value: Custom('Reset 2', 90)),
    ], true);
  }

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;
    return Scaffold(
      appBar: AppBar(title: const Text('Simple Search Dropdown')),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.fromLTRB(16, 8, 16, 32),
          child: Center(
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 560),
              child: LayoutBuilder(
                builder: (BuildContext context, BoxConstraints box) {
                  final double width = box.maxWidth;
                  return Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      _Section(
                        title: 'Single selection',
                        description: 'Add, edit and delete with slow (2 s) '
                            'callbacks. Starts disabled.',
                        dropdown: SearchDropDown(
                          controller: singleController,
                          // This State creates and disposes the controllers.
                          disposeController: false,
                          searchBarSettings: SimpleSearchbarSettings(
                            dropdownWidth: width,
                            showKeyboardOnTap: false,
                            // `actions` replaces the arrow and clear icons.
                            actions: const [
                              Padding(
                                padding: EdgeInsets.only(right: 8),
                                child: Icon(Icons.search),
                              ),
                            ],
                            showDivider: true,
                            clearOnClose: true,
                          ),
                        ),
                        actions: [
                          (
                            'Enable/Disable Single Selection',
                            singleController.toggleEnabled,
                          ),
                          (
                            'Force Single Selection',
                            () => singleController.forceSelection('one more'),
                          ),
                          (
                            'Clear Single Selection',
                            singleController.resetSelection,
                          ),
                          (
                            'Print Single Result',
                            () {
                              print(selectedSingleItem);
                              print(
                                singleController.localSearchController.text,
                              );
                              print(singleController.selectedItem);
                            },
                          ),
                          (
                            'Print new Single Selection',
                            () => print(singleController.selectedItem?.label),
                          ),
                        ],
                      ),
                      _Section(
                        title: 'Multiple selection',
                        description:
                            'Shares its list with the single dropdown.',
                        dropdown: MultipleSearchDropDown(
                          controller: multipleController,
                          searchBarSettings:
                              SimpleSearchbarSettings(dropdownWidth: width),
                        ),
                        actions: [
                          (
                            'Force Multiple Selection',
                            () => multipleController.forceSelection('one more'),
                          ),
                          (
                            'Clear Multiple Selection',
                            multipleController.resetSelection,
                          ),
                          (
                            'Print Multiple Result',
                            () => print(selectedMultipleItems),
                          ),
                        ],
                      ),
                      _Section(
                        title: 'Custom type and row',
                        description: 'SearchDropDown<Custom> with a custom '
                            'itemWidgetBuilder.',
                        dropdown: SearchDropDown<Custom>(
                          controller: customController,
                          disposeController: false,
                          searchBarSettings: SimpleSearchbarSettings(
                            dropdownWidth: width,
                            showArrow: false,
                          ),
                          overlayListSettings: SimpleOverlaySettings(
                            dialogHeight: 280,
                            // The row's own text colours need a selected
                            // fill they contrast with, light and dark.
                            selectedItemBackgroundColor:
                                scheme.secondaryContainer,
                            itemWidgetBuilder: (item) => PersonRow(
                              item: item,
                            ),
                          ),
                        ),
                        actions: [
                          (
                            'Print Custom Result',
                            () {
                              final ValueItem<Custom>? custom =
                                  selectedCustomItem;
                              print(custom);
                              if (custom != null) {
                                print('name: ${custom.value!.name}');
                                print(custom.runtimeType);
                              }
                            },
                          ),
                          (
                            'Clear Custom Selection',
                            customController.resetSelection,
                          ),
                          ('Reset custom list', resetCustom),
                        ],
                      ),
                      _Section(
                        title: 'Inside a dialog',
                        description: 'A read-only dropdown over the shared '
                            'list.',
                        actions: [
                          (
                            'Open Dialog',
                            () => showDialog<void>(
                                  context: context,
                                  builder: (BuildContext context) =>
                                      DialogTest(items: items),
                                ),
                          ),
                        ],
                      ),
                    ],
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// A custom list row: an initial in a circle, the name and the id below it.
/// Its text colours come from the theme, so it reads in light and dark.
class PersonRow extends StatelessWidget {
  const PersonRow({required this.item, super.key});

  final ValueItem item;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final ColorScheme scheme = theme.colorScheme;
    final Object? value = item.value;
    final String label = item.label;
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 6),
      child: Row(
        children: [
          CircleAvatar(
            radius: 16,
            backgroundColor: scheme.primaryContainer,
            foregroundColor: scheme.onPrimaryContainer,
            child: Text(
              label.isEmpty ? '?' : label.characters.first.toUpperCase(),
              style: theme.textTheme.labelLarge?.copyWith(
                color: scheme.onPrimaryContainer,
              ),
            ),
          ),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(
                  label,
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                  style: theme.textTheme.bodyLarge?.copyWith(
                    color: scheme.onSurface,
                  ),
                ),
                if (value is Custom)
                  Text(
                    'ID ${value.id}',
                    style: theme.textTheme.bodySmall?.copyWith(
                      color: scheme.onSurfaceVariant,
                    ),
                  ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// A titled block of the demo page: a description, the dropdown and its
/// demo buttons, with the page's standard spacing.
class _Section extends StatelessWidget {
  const _Section({
    required this.title,
    required this.description,
    required this.actions,
    this.dropdown,
  });

  final String title;
  final String description;
  final Widget? dropdown;
  final List<(String, VoidCallback)> actions;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.only(top: 24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(title, style: theme.textTheme.titleMedium),
          const SizedBox(height: 4),
          Text(
            description,
            style: theme.textTheme.bodyMedium?.copyWith(
              color: theme.colorScheme.onSurfaceVariant,
            ),
          ),
          if (dropdown != null) ...[
            const SizedBox(height: 16),
            dropdown!,
          ],
          const SizedBox(height: 12),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              for (final (String label, VoidCallback onPressed) in actions)
                OutlinedButton(onPressed: onPressed, child: Text(label)),
            ],
          ),
        ],
      ),
    );
  }
}
10
likes
160
points
210
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Searchable single and multi-select dropdowns for Flutter with accent-insensitive search, add/edit/delete of items, keyboard navigation and Material 3 theming.

Repository (GitHub)
View/report issues

Topics

#dropdown #search #autocomplete #select #widget

License

MIT (license)

Dependencies

flutter, scrollable_positioned_list, stringr

More

Packages that depend on simple_search_dropdown