π§Ό Good Dismissible
A better Dismissible experience for Flutter β precise, polished, and visually clean.
Ideal for cards, lists, and swipe-to-delete interactions that need to look as good as they feel.
β¨ Features
- β Clean border radius handling
- β Classic swipe-to-dismiss with animated background
- β LinkedIn-style reveal action that snaps open and stays tappable
- β Customizable colors, icons, text, and action widgets
- β Can disable swipe left or swipe right independently
- β
Drop-in replacement for Flutterβs
Dismissible
π¦ Installation
Add the following to your pubspec.yaml:
dependencies:
good_dismissable: ^1.1.1
π¦ Import the Package
import 'package:good_dismissable/good_dismissable.dart';
π¦ Example
Classic dismiss
GoodDismissableVariants.delete(
onDismissed: () => removeItem(),
child: const ListTile(title: Text('Swipe to delete')),
)
LinkedIn-style reveal action
GoodDismissableVariants.linkedInDelete(
onActionPressed: () => removeItem(),
child: const ListTile(title: Text('Swipe left to reveal delete')),
)
Fully customized reveal action
GoodDismissable(
swipeBehavior: GoodDismissableSwipeBehavior.reveal,
enableSwipeToLeft: true,
enableSwipeToRight: false,
backgroundColor: const Color(0xFFD11124),
borderRadius: 20,
revealActionExtent: 104,
onActionPressed: () => removeItem(),
actionContent: const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.delete_outline, color: Colors.white, size: 28),
SizedBox(height: 6),
Text(
'Delete',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w700,
fontSize: 16,
),
),
],
),
child: const ListTile(title: Text('Custom reveal action')),
)
Full app example
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
List<EmailItem> emails = List.generate(
10,
(index) => EmailItem(
id: 'email_$index',
title: 'Email ${index + 1}',
subtitle: 'This is a sample email content for item ${index + 1}...',
isRead: index % 3 == 0,
),
);
void _removeEmail(String emailId) {
setState(() {
emails.removeWhere((email) => email.id == emailId);
});
}
void _markAsRead(String emailId) {
setState(() {
final index = emails.indexWhere((email) => email.id == emailId);
if (index != -1) {
emails[index] = emails[index].copyWith(isRead: true);
}
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// TRY THIS: Try changing the color here to a specific color (to
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
// change color while the other colors stay the same.
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: ListView.builder(
itemCount: emails.length,
itemBuilder: (context, index) {
final email = emails[index];
// Different card types based on index for demonstration
if (index % 4 == 0) {
// Classic delete variant
return GoodDismissableVariants.delete(
key: ValueKey(email.id),
enableSwipeToLeft: false,
onDismissed: () => _removeEmail(email.id),
child: _buildEmailTile(email, index),
);
} else if (index % 4 == 1) {
// Archive variant
return GoodDismissableVariants.archive(
key: ValueKey(email.id),
enableSwipeToRight: false,
onDismissed: () => _removeEmail(email.id),
child: _buildEmailTile(email, index),
);
} else if (index % 4 == 2) {
// Custom variant with progress callback
return GoodDismissable(
key: ValueKey(email.id),
backgroundColor: Colors.orange,
cardOffset: 12.0,
initialScale: 0.92,
onDismissed: () => _markAsRead(email.id),
onSwipeProgress: (progress) {
// You can use this for custom animations or haptic feedback
if (progress > 0.5) {
// Trigger haptic feedback or other actions
}
},
backgroundContent: Row(
children: [
Expanded(
child: Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 20),
child: const Text(
'Mark Important',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
),
Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(
Icons.star,
color: Colors.white,
size: 24,
),
),
],
),
child: _buildEmailTile(email, index),
);
}
return GoodDismissableVariants.linkedInDelete(
key: ValueKey(email.id),
onActionPressed: () => _removeEmail(email.id),
child: _buildEmailTile(email, index),
);
},
),
);
}
Widget _buildEmailTile(EmailItem email, int index) {
return ListTile(
leading: CircleAvatar(
backgroundColor: email.isRead ? Colors.grey : Colors.blue,
child: Text(
'${index + 1}',
style: const TextStyle(color: Colors.white),
),
),
title: Text(
email.title,
style: TextStyle(
fontWeight: email.isRead ? FontWeight.normal : FontWeight.bold,
),
),
subtitle: Text(
email.subtitle,
style: TextStyle(color: email.isRead ? Colors.grey : Colors.black87),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!email.isRead)
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
const Icon(Icons.arrow_forward_ios, size: 16),
],
),
);
}
}
You can also control the swipe direction directly:
GoodDismissable(
enableSwipeToLeft: false,
enableSwipeToRight: true,
onDismissed: () {},
child: const ListTile(title: Text('Swipe right only')),
)
For a LinkedIn-style action that stops, stays open, and can be tapped:
GoodDismissableVariants.linkedInDelete(
onActionPressed: () {},
child: const ListTile(title: Text('Swipe left to reveal delete')),
)
π§© Main API
GoodDismissableSwipeBehavior.dismiss: classic swipe-to-dismiss behavior.GoodDismissableSwipeBehavior.reveal: swipe opens an action pane and waits for a tap.enableSwipeToLeftandenableSwipeToRight: toggle each swipe direction independently.backgroundContent: custom background for classic dismiss behavior.actionContent: custom action widget for reveal behavior.onDismissed: called after a classic dismiss finishes.onActionPressed: called when the revealed action is tapped.
Issues & Suggestions
If you encounter any issue you or want to leave a suggestion you can do it by filling an issue.
Thank you for the support!
Libraries
- good_dismissable
- Good Dismissable