dartpub.dev DartNative · beta
plugins / swipe_actions_kit
sw

swipe_actions_kit

v0.1.0 MIT

Swipe-to-reveal action rows for DartNative lists: both sides, full swipe, RTL, one open row per list.

Swipe-to-reveal action rows for DartNative lists, with flutter_slidable's behaviour. SwipeActionsRow takes leading and trailing SwipeActions (icon, label, colour, onTap), and a full swipe fires the outermost action once. Leading and trailing mirror in RTL. SwipeActionsController opens and closes rows from code; SwipeActionsGroup keeps one row open per list and closes it on tap or scroll. Works in FastList and ListView. On iOS the drag comes from a small native pan recognizer that only claims horizontal pans, because a horizontal-drag GestureDetector inside a list stops vertical scrolling there. Android is pure Dart. In a FastList, inset each row yourself and pass SwipeActionsRow(width:) (see README).

by AbdurahmanAlmehdi/swipe_actions_kit · DartNative ≥ 3.0 · updated today
Install
Free
pubspec.yaml
dependencies:
  swipe_actions_kit:
    hosted: https://dartpub.dev
    version: ^0.1.0
Weekly installs
0
Active apps
0
Rating
0.0 · 0
Open issues
0

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: false turns 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: false keeps it open). Tapping the content of an open row only closes it.
  • SwipeAction.foregroundColor defaults to white or black, whichever reads better on color.

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.