Flutter Motion Kit

Previewable Flutter animations + pitfall guide · one-click reuse from Claude Code

← Back

Swipe to dismiss

explicit · difficulty 2/5 · verified Flutter 3.32 / 2026-06

A ListView of rows wrapped in Dismissible. Swipe a row to fling it off-screen over a colored background; onDismissed removes the item from the data source so the list and the widget tree stay consistent.

▶︎ Running for real (Flutter 3.32 · self-hosted, not a recording)

Code

// ✅ Recommended: each row has a unique Key, confirmDismiss guards the action,
// background + secondaryBackground match both swipe directions, and onDismissed
// removes the item from the data source.
// Paste straight into DartPad (https://dartpad.dev) to run.
import 'package:flutter/material.dart';

void main() => runApp(const _App());

class _App extends StatelessWidget {
  const _App();
  @override
  Widget build(BuildContext context) => MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.teal),
    home: const _Demo(),
  );
}

class _Demo extends StatefulWidget {
  const _Demo();
  @override
  State<_Demo> createState() => _DemoState();
}

class _DemoState extends State<_Demo> {
  final List<String> _items = List<String>.generate(
    8,
    (i) => 'Message ${i + 1}',
  );

  Future<bool> _confirm(BuildContext context) async {
    final ok = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Delete?'),
        content: const Text('This message will be removed.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('Delete'),
          ),
        ],
      ),
    );
    // ✅ Dialog can be dismissed by tapping outside → treat null as "cancel".
    return ok ?? false;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Swipe to dismiss')),
      body: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          final item = _items[index];
          return Dismissible(
            // ✅ Unique, identity-based key — not the index.
            key: ValueKey<String>(item),
            // ✅ Ask before the destructive removal actually happens.
            confirmDismiss: (direction) => _confirm(context),
            // ✅ Remove from the data source so the widget leaves the tree.
            onDismissed: (direction) {
              setState(() => _items.removeAt(index));
              ScaffoldMessenger.of(
                context,
              ).showSnackBar(SnackBar(content: Text('Dismissed $item')));
            },
            background: const ColoredBox(
              color: Colors.green,
              child: Align(
                alignment: Alignment.centerLeft,
                child: Padding(
                  padding: EdgeInsets.only(left: 20),
                  child: Icon(Icons.archive, color: Colors.white),
                ),
              ),
            ),
            secondaryBackground: const ColoredBox(
              color: Colors.red,
              child: Align(
                alignment: Alignment.centerRight,
                child: Padding(
                  padding: EdgeInsets.only(right: 20),
                  child: Icon(Icons.delete, color: Colors.white),
                ),
              ),
            ),
            child: ListTile(
              leading: const Icon(Icons.message),
              title: Text(item),
            ),
          );
        },
      ),
    );
  }
}

⚠️ Pitfalls (4)

Dismissible needs a stable, unique Key per item; without one Flutter cannot tell which child left and dismisses or rebuilds the wrong row.
✅ Give each Dismissible a ValueKey derived from the item's identity (e.g. ValueKey(item)), not the list index.
official-docs · source
If onDismissed does not remove the item from the data source, the widget stays in the tree and Flutter throws "A dismissed Dismissible widget is still part of the tree".
✅ In onDismissed call setState and remove the backing item so the next build no longer produces that Dismissible.
official-docs · source
Removing immediately on swipe gives no chance to cancel a destructive action.
✅ Return a Future<bool> from confirmDismiss (e.g. an AlertDialog) and only let the dismiss proceed when it resolves true.
official-docs · source
A Dismissible with no background reveals nothing under the finger, and a direction without a matching background looks broken.
✅ Supply background (and secondaryBackground for the opposite direction) aligned with the icons that hint the allowed swipe direction.
official-docs · source

Official docs