flutter_pan_select

English | 繁體中文

Multi-select with drag-to-select (pan) gesture for grids and lists. Lightweight, themeable selection controller with marquee selection and auto-scroll support.

Drag-to-select on a grid with overlay checkboxes and edge auto-scroll Drag-to-select on a list with left checkboxes

Features

  • Drag to select — pan across items to select a range, the file-manager / photo-gallery pattern (not a dropdown or dialog picker).
  • Grid and list — works over both SliverGrid and SliverList.
  • Edge auto-scroll — dragging toward the top or bottom edge scrolls the view and keeps selecting the items revealed under the finger.
  • Default or custom checkboxes — a built-in checkbox per layout, or supply your own decoration via checkboxBuilder.
  • Bring your own state — explicit SelectionController (Riverpod / Bloc / GetX friendly) or an implicit SelectionScope.
  • Object-based selection — selected items come back as a Set<T> of your own values, stable across reordering.

Installation

dependencies:
  flutter_pan_select: ^1.0.0

Quick start

The easy way: SelectionScope

Wrap the subtree with SelectionScope<T> and the widgets pick up the controller automatically:

SelectionScope<Photo>(
  child: CustomScrollView(
    slivers: [
      SliverPannableGrid<Photo>.builder(
        getItemFromIndex: (i) => photos[i],
        itemCount: photos.length,
        crossAxisCount: 3,
        // Each item is wrapped with SelectionItemOverlay automatically.
        itemBuilder: (context, index) => PhotoTile(photo: photos[index]),
      ),
    ],
  ),
);

To enter selection mode, grab the controller with SelectionScope.of<Photo>(context).startSelection(allItems: photos.toSet()).

The explicit way: pass your own controller

If you already manage selection state with Riverpod / Bloc / GetX, hold a SelectionController<T> in your store and pass it in:

SliverPannableList<String>.separated(
  controller: myController,
  getItemFromIndex: (i) => items[i],
  itemCount: items.length,
  separatorBuilder: (_, __) => const Divider(),
  // Each item is wrapped with SelectionLeftCheckbox automatically.
  itemBuilder: (context, index) => ListTile(title: Text(items[index])),
);

This is the same pattern Flutter itself uses for TabController / DefaultTabController. No SelectionScope ancestor needed.

Customising or disabling the checkbox

SliverPannableList / SliverPannableGrid wrap each item with their default checkbox (SelectionLeftCheckbox / SelectionItemOverlay). Override it with checkboxBuilder, or opt out by returning the child unchanged:

// Custom selection decoration
SliverPannableGrid<Photo>.builder(
  // ...
  checkboxBuilder: (context, item, child) =>
      MyBadge(item: item, child: child),
);

// No automatic wrapping — decorate it yourself in itemBuilder
SliverPannableList<String>.separated(
  // ...
  checkboxBuilder: (context, item, child) => child,
);

Public API

Type Purpose
SelectionController<T> Holds selection state. Extends ChangeNotifier.
SelectionScope<T> InheritedWidget hosting a controller. Optional.
SelectionButton<T> Toggles selection mode on/off (startSelection / endSelection).
SelectionLeftCheckbox<T> List-style checkbox on the left of each item. Default wrapper for SliverPannableList.
SelectionItemOverlay<T> Grid-style checkbox overlay at the top-right. Default wrapper for SliverPannableGrid.
SliverPannable<T> Core sliver that detects pan-to-select gestures. Layout-agnostic; no default wrapper.
SliverPannableGrid<T>.builder Wrapper over SliverPannable + SliverGrid. Wraps items with SelectionItemOverlay by default; override via checkboxBuilder.
SliverPannableList<T>.separated Wrapper over SliverPannable + SliverList.separated. Wraps items with SelectionLeftCheckbox by default; override via checkboxBuilder.

How it works

Pan select hit-tests against on-screen items using GlobalKeys. Only items currently inside the viewport have a RenderBox and are reachable by the gesture; off-screen items are skipped. Edge auto-scroll brings new items into the viewport as the user drags toward the top or bottom edge.

SliverPannable requires a Scrollable ancestor (it grabs the ScrollController from Scrollable.of(context)), and only supports vertical scrolling in this release.

FAQ

Q: Can I use this with Riverpod / Bloc / GetX? Yes. Hold a SelectionController<T> wherever you keep your other state and pass it to the widgets via the controller: parameter. SelectionScope is optional.

Q: I have two selection lists on one page with the same T. How? Use two separate controllers and pass them explicitly to each subtree's widgets. The implicit SelectionScope.of<T>(context) lookup resolves to the nearest ancestor SelectionScope<T>; with nested or sibling scopes sharing the same T, explicit injection ensures each subtree connects to the intended controller.

Q: How do I customise the bottom toolbar? The package intentionally does not ship a toolbar widget. See example/lib/widgets/selection_toolbar.dart for a reference implementation you can copy and modify, or build your own around SelectionScope.of<T>(context) / your controller.

Q: Why does T need to be value-equal? The controller stores selected items in a Set<T>, so T must have stable == and hashCode. Plain value-classes, records, and enum types all work.

Known limitations

  • Horizontal scrolling is not supported (asserted in SliverPannable).
  • Pan select can only act on items currently inside the viewport.
  • SelectionScope.of<T>(context) resolves to the nearest matching ancestor; if you have nested scopes sharing the same T, use explicit controller: injection to disambiguate.

License

MIT

Libraries

flutter_pan_select
Multi-select with drag-to-select (pan) gesture for grids and lists.