Flutter Pro Design

Small details that build taste in Flutter.

curated by Kamran BekirovKamran Bekirov

Show users your swipe actions exist

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.