selection_sheet 0.4.0 copy "selection_sheet: ^0.4.0" to clipboard
selection_sheet: ^0.4.0 copied to clipboard

A production-ready adaptive selection workflow for Flutter.

selection_sheet #

Adaptive, searchable selection workflows for Flutter.

selection_sheet provides typed single and multi-selection sheets with local and remote search, pagination, grouped results, draggable heights, keyboard avoidance, Material/Cupertino adaptation, and a theming system that supports both global defaults and per-sheet overrides.

Single selection #

final country = await SelectionSheet.showSingle<Country>(
  context: context,
  items: countries,
  searchable: true,
  searchDebounceDuration: const Duration(milliseconds: 400),
  title: 'Country',
  itemLabelBuilder: (country) => country.name,
);

Search uses a 300 ms debounce by default. Override it for an individual sheet with searchDebounceDuration, or configure it globally:

SelectionSheetThemeData.fallback(context).copyWith(
  searchDebounceDuration: const Duration(milliseconds: 500),
);

Use Duration.zero when filtering should happen immediately.

Remote search and pagination #

Use loadItems instead of items. Every request contains the current debounced query, a one-based page number, the requested page size, and the cursor returned by the previous page.

final users = await SelectionSheet.showMulti<User>(
  context: context,
  searchable: true,
  pageSize: 30,
  itemLabelBuilder: (user) => user.name,
  loadItems: (request) async {
    final result = await repository.searchUsers(
      query: request.query,
      page: request.page,
      limit: request.pageSize,
      cursor: request.cursor,
    );

    return SelectionSheetPage(
      items: result.users,
      hasMore: result.hasMore,
      nextCursor: result.nextCursor,
    );
  },
);

Page-based repositories can ignore request.cursor. Cursor-based repositories can ignore request.page. When a new search begins, responses from all older search generations are ignored automatically.

Cancelling async work #

Every request includes a cooperative cancellation token. It is cancelled when a search is superseded, the sheet refreshes, or the route closes:

loadItems: (request) async {
  final operation = api.searchUsers(
    query: request.query,
    cursor: request.cursor,
  );
  final removeListener = request.cancellationToken.onCancel(operation.cancel);

  try {
    final response = await operation.value;
    request.cancellationToken.throwIfCancelled();
    return SelectionSheetPage(
      items: response.users,
      hasMore: response.hasMore,
      nextCursor: response.nextCursor,
    );
  } finally {
    removeListener();
  }
},

Repositories that cannot cancel their underlying future can inspect request.cancellationToken.isCancelled. Stale responses are still rejected by the sheet even when the repository ignores cancellation.

Refreshing remote results #

Pass a controller when another part of the application needs to refresh the open sheet:

final selectionController = SelectionSheetController();

SelectionSheet.showMulti<User>(
  context: context,
  controller: selectionController,
  loadItems: repository.loadUsers,
  itemLabelBuilder: (user) => user.name,
);

await selectionController.refresh();

refresh() reloads page one for the current debounced query, resets the cursor, invalidates older requests, and preserves the draft selection. Remote result lists also support pull-to-refresh by default. Set enablePullToRefresh: false to disable that gesture.

Multi-selection #

Multi-selection is kept as a draft until the user confirms it.

final countries = await SelectionSheet.showMulti<Country>(
  context: context,
  items: allCountries,
  initialSelection: selectedCountries,
  searchable: true,
  title: 'Countries',
  itemLabelBuilder: (country) => country.name,
);

Selected items appear as removable chips by default. Customize them with a typed builder or disable them with showSelectedChips: false:

selectedChipBuilder: (context, country, label, onDeleted) {
  return InputChip(
    avatar: CountryFlag(code: country.code),
    label: Text(label),
    onDeleted: onDeleted,
  );
},

Sections and sticky headers #

SelectionSheet.showSingle<User>(
  context: context,
  items: users,
  itemLabelBuilder: (user) => user.name,
  sectionBuilder: (user) => user.department,
  stickySectionHeaders: true,
  sectionHeaderBuilder: (context, section) {
    return ColoredBox(
      color: Theme.of(context).colorScheme.surface,
      child: Text('${section.label} (${section.itemCount})'),
    );
  },
);

Section order follows the first occurrence of each key in the loaded items.

Grid presentation #

Grid mode uses the same local/remote search, pagination, sections, selection, and item builders as list mode:

SelectionSheet.showMulti<Product>(
  context: context,
  items: products,
  itemLabelBuilder: (product) => product.name,
  layout: SelectionSheetLayout.grid,
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    childAspectRatio: 0.8,
    crossAxisSpacing: 12,
    mainAxisSpacing: 12,
  ),
  itemBuilder: (context, product, state) {
    return ProductSelectionCard(
      product: product,
      selected: state.isSelected,
    );
  },
);

Set layout and gridDelegate in SelectionSheetThemeData to make grid mode the application-wide default.

Native form fields #

The package integrates directly with Flutter's Form and FormField APIs and does not depend on a third-party form package.

Form(
  key: formKey,
  child: SelectionSheetFormField<Country>(
    items: countries,
    searchable: true,
    itemLabelBuilder: (country) => country.name,
    decoration: const InputDecoration(labelText: 'Country'),
    validator: (country) {
      return country == null ? 'Country is required' : null;
    },
    onSaved: (country) => profile.country = country,
  ),
);

For multiple values, use SelectionSheetMultiFormField<T>. Both widgets support local and remote sources, sections, custom item rows, grid layout, validation, saving, clearing, and AutovalidateMode. hintText is optional, so a field with InputDecoration(labelText: 'Country') does not show a redundant selection prompt unless one is explicitly provided.

Loading, empty, and error states #

All initial and pagination states have per-sheet builders:

SelectionSheet.showSingle<User>(
  context: context,
  loadItems: repository.loadUsers,
  itemLabelBuilder: (user) => user.name,
  loadingBuilder: (context) => const UserListSkeleton(),
  emptyBuilder: (context, query) => EmptyUsers(query: query),
  errorBuilder: (context, error, retry) {
    return ErrorPanel(error: error, onRetry: retry);
  },
  loadingMoreBuilder: (context) => const LoadingMoreRow(),
  loadMoreErrorBuilder: (context, error, retry) {
    return RetryPageRow(onRetry: retry);
  },
);

The same builders can be configured globally in SelectionSheetThemeData.

Custom item rows #

The label builder remains the source for search and semantics. A typed item builder can replace the visual row:

final country = await SelectionSheet.showSingle<Country>(
  context: context,
  items: countries,
  itemLabelBuilder: (country) => country.name,
  itemBuilder: (context, country, state) {
    return ListTile(
      leading: CountryFlag(code: country.code),
      title: Text(country.name),
      selected: state.isSelected,
      trailing: state.isSelected ? const Icon(Icons.check) : null,
    );
  },
);

Global design #

Place SelectionSheetTheme inside MaterialApp.builder:

MaterialApp(
  builder: (context, child) {
    return SelectionSheetTheme(
      data: SelectionSheetThemeData.fallback(context).copyWith(
        selectedColor: Colors.indigo.shade50,
        showDividers: true,
        itemBuilder: (context, item) {
          return AppSelectionTile(
            label: item.label,
            selected: item.isSelected,
          );
        },
      ),
      child: child!,
    );
  },
  home: const App(),
);

A typed itemBuilder passed to showSingle or showMulti overrides the global builder. A sheet can also override design tokens:

SelectionSheet.showSingle<Country>(
  context: context,
  items: countries,
  itemLabelBuilder: (country) => country.name,
  theme: SelectionSheetTheme.of(context).copyWith(
    selectedColor: Colors.green.shade100,
    showDividers: false,
  ),
);

Roadmap #

  • Keyboard navigation for desktop grids
  • Request caching policies
  • Accessibility and localization audit
0
likes
0
points
251
downloads

Publisher

verified publisherpinz.dev

Weekly Downloads

A production-ready adaptive selection workflow for Flutter.

Repository (GitHub)
View/report issues

Topics

#bottom-sheet #selection #search #picker #flutter

License

unknown (license)

Dependencies

flutter

More

Packages that depend on selection_sheet