Hint at swipe actions so user knows they exist. Example from expen.app:
Use flutter_slidable. It has a SlidableController using which you can programmatically open and close those actions:
class TaskItem extends StatefulWidget {
final Task task;
final bool isFirst;
const TaskItem({
super.key,
required this.task,
required this.isFirst,
});
@override
State<TaskItem> createState() => _TaskItemState();
}
class _TaskItemState extends State<TaskItem> with SingleTickerProviderStateMixin {
late final SlidableController _controller;
@override
void initState() {
super.initState();
_controller = SlidableController(this);
// only the first item runs the preview
if (widget.isFirst) {
// show the hint once, ever
Once.runOnce(
'task_swipe_hint',
callback: () {
_runPreview();
},
);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _runPreview() async {
// let the page finish settling first
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
// peek the delete action (start pane = swipe right)
await _controller.openStartActionPane(
duration: const Duration(milliseconds: 400),
);
// hold it open long enough for the user to notice
await Future.delayed(const Duration(milliseconds: 900));
if (!mounted) return;
await _controller.close(duration: const Duration(milliseconds: 300));
// brief pause before showing the other side
await Future.delayed(const Duration(milliseconds: 400));
if (!mounted) return;
// peek the edit action (end pane = swipe left)
await _controller.openEndActionPane(
duration: const Duration(milliseconds: 400),
);
await Future.delayed(const Duration(milliseconds: 900));
if (!mounted) return;
await _controller.close(duration: const Duration(milliseconds: 300));
}
@override
Widget build(BuildContext context) {
return Slidable(
controller: _controller,
startActionPane: ActionPane(
extentRatio: 0.2,
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (_) { /* delete */ },
backgroundColor: Colors.red,
icon: Icons.delete_outline,
),
],
),
endActionPane: ActionPane(
extentRatio: 0.2,
motion: const DrawerMotion(),
children: [
SlidableAction(
onPressed: (_) { /* edit */ },
backgroundColor: Colors.blue,
icon: Icons.edit_outlined,
),
],
),
child: ListTile(
title: Text(widget.task.title),
),
);
}
}
Pass isFirst: index == 0 from your list builder so you show preview only on first item.
Show it just once. Use Once.runOnce from the once package or make your own implementation to persist shown or not.