swipe_actions_kit
Swipe-to-reveal action rows for DartNative lists:
leading and trailing actions, a full swipe that performs the primary action,
RTL mirroring, one open row per group and close on scroll. The behaviour and
defaults follow flutter_slidable; the names follow UIKit
(leadingSwipeActionsConfiguration).
Works in FastList, ListView, CustomScrollView, sheets and pushed routes.
Install
dependencies:
swipe_actions_kit: ^0.1.0
dn pub get
The package ships a small iOS pod (see How it works); the
generated registrant wires it, so DartNativePluginRegistrant.registerAll()
in main() is all it needs.
Use
import 'package:swipe_actions_kit/swipe_actions_kit.dart';
SwipeActionsRow(
key: ValueKey(trip.id), // a stable key inside recycled lists
extentRatio: 0.28, // pane width as a fraction of the row
leading: [
SwipeAction(
icon: LucideIcons.check,
label: 'Paid',
color: const Color(0xFFE3F5EA),
foregroundColor: const Color(0xFF1E8E4E),
borderRadius: BorderRadius.circular(10),
onTap: (context) => togglePaid(trip),
),
],
trailing: [
SwipeAction(
icon: LucideIcons.trash2,
label: 'Delete',
color: const Color(0xFFFDE7E7),
foregroundColor: const Color(0xFFD93025),
onTap: (context) async {
if (await confirmDelete(context)) deleteTrip(trip);
},
),
],
child: TripTile(trip),
)
- The first action of each list sits at the outer edge; a full swipe (past
fullSwipeThreshold, 60 % of the row) performs it once and closes the row.fullSwipe: falseturns that off. - A release past half the pane, or a fling of 400 px/s toward opening, opens
the row; otherwise it closes. Settling takes 200 ms with CSS
ease. - Tapping an action runs
onTap(context)and then closes the row (autoClose: falsekeeps it open). Tapping the content of an open row only closes it. SwipeAction.foregroundColordefaults to white or black, whichever reads better oncolor.
Drive a row from code
final hint = SwipeActionsController();
SwipeActionsRow(controller: hint, …);
await hint.openLeading(duration: const Duration(milliseconds: 400));
await hint.close(duration: const Duration(milliseconds: 300));
await hint.openTrailing(duration: const Duration(milliseconds: 400));
await hint.close(duration: const Duration(milliseconds: 300));
A call made while the row isn't mounted completes at once, so an
if (!mounted) return between steps is enough. openEdge / isOpen report
the resting state and notify listeners.
Close on scroll and one open row at a time
DartNative has no Scrollable.of or scroll notifications, so the list's
scroll signal is wired to a SwipeActionsGroup explicitly.
ListView / CustomScrollView — pass the list's controller:
final scroll = ScrollController();
SwipeActionsGroup(
scrollController: scroll,
child: ListView(controller: scroll, children: rows),
)
FastList — hand it the group's onScroll callback. The context must be
below the group, hence the Builder:
SwipeActionsGroup(
child: Builder(
builder: (context) => FastList(
itemCount: items.length,
itemBuilder: (_, i) => row(items[i]),
onScroll: SwipeActionsGroup.onScrollOf(
context,
chain: (offset, maxExtent, viewport, dragging) => maybeLoadMore(),
),
),
),
)
Row width in a FastList. A cell reports the list's full width to its
row, the list's padding ignored, and in RTL a FastList drops its side
padding from cells after a relayout. Give the list no side padding, inset each
row yourself, and pass the row's width (doc/upstream-gaps.md, G6):
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SwipeActionsRow(
key: ValueKey(item.id),
width: MediaQuery.sizeOf(context).width - 32,
trailing: [...],
child: row,
),
)
Inside a group, opening a row closes the other open rows with the same
groupTag (closeWhenOpened), and a tap on another row while one is open
only closes it (closeWhenTapped). SwipeActionsGroup.maybeOf(context) ?.closeAll() closes everything, e.g. before a refresh. Without a group, rows
are independent and closeOnScroll does nothing.
Right-to-left
leading is the start edge, read from the nearest Directionality: the
left edge in English, the right edge in Arabic. So the same right-to-left
full swipe performs the first trailing action in English and the first
leading action in Arabic.
| Direction | Leading pane | Opened by |
|---|---|---|
| LTR | left edge | dragging right |
| RTL | right edge | dragging left |
DartNative lays an app out right-to-left when the app declares a
right-to-left language (CFBundleLocalizations on iOS, android:supportsRtl
on Android) and the device runs in one. Wrapping a subtree in
Directionality(textDirection: TextDirection.rtl, …) flips the rows without
changing the device language; the example's RTL switch does this.
Porting from flutter_slidable
| flutter_slidable | swipe_actions_kit |
|---|---|
Slidable |
SwipeActionsRow |
startActionPane |
leading |
endActionPane |
trailing |
SlidableAction(backgroundColor, foregroundColor, icon, label, borderRadius, onPressed, autoClose, flex) |
SwipeAction(color, foregroundColor, icon, label, borderRadius, onTap, autoClose, flex) |
ActionPane.extentRatio: 0.28 |
extentRatio: 0.28 (one per row, both panes) |
ActionPane.openThreshold / closeThreshold |
fixed at half the pane |
SlidableController(vsync) |
SwipeActionsController() (no vsync) |
openStartActionPane / openEndActionPane / close |
openLeading / openTrailing / close |
SlidableAutoCloseBehavior |
SwipeActionsGroup |
Slidable.groupTag |
SwipeActionsRow.groupTag |
closeOnScroll (via Scrollable.of) |
closeOnScroll, wired through SwipeActionsGroup |
DrawerMotion |
the only look |
BehindMotion, ScrollMotion, StretchMotion |
not provided |
DismissiblePane |
not provided |
CustomSlidableAction |
not provided |
direction: Axis.vertical |
not provided |
Not included
- Motions other than the drawer look, swipe-to-dismiss, vertical swipes and custom action widgets.
- A haptic tick on Android: the tick at the full-swipe threshold uses core
HapticFeedback, which is iOS-only.
Accessibility
VoiceOver and TalkBack can't perform a swipe on a row, and DartNative has no API for custom accessibility actions yet. Keep a second path to every swipe action — a long-press menu, or the action on the detail screen.
How it works
The row is a Stack: the action buttons behind, the content in a
Transform.translate in front, clipped to the row. Positions, thresholds and
RTL are a pure-Dart state machine.
On Android the drag comes from GestureDetector.onHorizontalDrag*. On iOS a
horizontal-drag GestureDetector inside a list stops the list scrolling
vertically, so the row places a zero-size native probe that adds a
UIPanGestureRecognizer to the row and only lets it begin for horizontal
pans toward a pane. Details: doc/spike.md,
doc/upstream-gaps.md.
Example
example/ has a 100-row FastList and a ListView tab, leading and
trailing actions on every row, a status line with the last action, a
"Play hint" button on row 0 and an RTL switch.
cd example && dn pub get && dn run
Credits & license
The behaviour and defaults are modelled on flutter_slidable 4.0.3 by Romain Rastel (MIT). No code was copied. This package's own code is MIT (see LICENSE); the flutter_slidable notice is in THIRD_PARTY_NOTICES.