A fairly common UI action for lists in native apps is “swipe to dismiss”. That is, the user can swipe left, or right, and a leave-behind UI element indicates what will happen if the user continues on with swiping. Typically, the leave-behind element is a delete icon.
Flutter comes with a UI widget called Dismissible – as the name suggests, it enables us to implement this pattern. This code tutorial will show you how.
Displaying a list
For this code tutorial, we will set up an app with 2 screens. The first screen will show a list using ListTile, and the second will show a list using a custom widget.
To follow the code tutorial, create a new app as follows.
1 |
flutter create swipetodismissexample |
If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.
Firstly, we create a Material app in main.dart, which will launch the List1Page widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import 'package:flutter/material.dart'; import 'list/list1_page.dart'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'Swipe To Dismiss Item In List Example', theme: new ThemeData( primaryColor: const Color(0xFF43a047), accentColor: const Color(0xFFffcc00), primaryColorBrightness: Brightness.dark, ), home: new List1Page(), ); } } |
Secondly, we create a subpackage list and create list1_page.dart in it. It displays a simple list using ListTile. We also add an icon in the toolbar from where we show List2Page widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
import 'package:flutter/material.dart'; import '../data/list_data.dart'; import 'list2_page.dart'; class List1Page extends StatefulWidget { List1Page({Key key}) : super(key: key); @override _List1PageState createState() => new _List1PageState(); } class _List1PageState extends State<List1Page> { List<Item> _items; @override void initState() { super.initState(); new ItemsRepository().init(); _items = new ItemsRepository().getItems(); } @override Widget build(BuildContext context) { List<Widget> menu = <Widget>[ new IconButton( icon: new Icon(Icons.settings), onPressed: _toList2, ), ]; return new Scaffold( appBar: new AppBar( title: new Text('List 1 Page'), actions: menu, ), body: new Padding( padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), child: new ListView( scrollDirection: Axis.vertical, shrinkWrap: true, children: _items.map((Item value) { return _buildListRow(value); }).toList(), ), ), ); } Widget _buildListRow(Item item) { return new ListTile( title: new Text(item.name), subtitle: new Text(item.content), ); } void _toList2() { Navigator.of(context).push(new MaterialPageRoute<dynamic>( builder: (BuildContext context) { return new List2Page(); }, )); } } |
Thirdly, we create list2_page.dart. For the list item, we will reuse the UI setup in the example 3 of mastering Row and Column code tutorial.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
import 'package:flutter/material.dart'; import '../data/list_data.dart'; class List2Page extends StatefulWidget { List2Page({Key key}) : super(key: key); @override _List2PageState createState() => new _List2PageState(); } class _List2PageState extends State<List2Page> { List<Item> _items; @override void initState() { super.initState(); _items = new ItemsRepository().getItems(); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text('List 2 Page'), ), body: new Padding( padding: new EdgeInsets.symmetric(vertical: 0.0, horizontal: 0.0), child: new ListView( scrollDirection: Axis.vertical, shrinkWrap: true, children: _items.map((Item value) { return _buildListRow(value); }).toList(), ), ), ); } Widget _buildListRow(Item item) { Widget titleRow = new Row( children: <Widget>[ new Icon(Icons.people), new Expanded( child: new Container( padding: new EdgeInsets.symmetric(horizontal: 4.0), child: new Text(item.name, overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), new Text(item.date), ], ); Widget textSection = new Container( child: new Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: <Widget>[ titleRow, new Text(item.content, overflow: TextOverflow.ellipsis, maxLines: 2,) ], ), ); return new Container ( padding: new EdgeInsets.all(8.0), child: new Row( children: <Widget>[ new IconButton( onPressed: null, // Not implemented in this code tutorial icon: new Icon(Icons.reply), ), new Expanded( child: textSection), new IconButton( onPressed: null, // Not implemented in this code tutorial icon: new Icon(Icons.archive), ), ], ), ); } } |
Lastly, we add a subpackage data and create list_data.dart in it. Note: For simplification, we are setting up a repository with hardcoded data, as this code tutorial is about UI and not about architecturing a Flutter app.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class Item { final int id; final String name; final String date; final String content; const Item(this.id, this.name, this.date, this.content); } class ItemsRepository { static final ItemsRepository _singleton = new ItemsRepository._internal(); factory ItemsRepository() { return _singleton; } ItemsRepository._internal(); List<Item> _items; void init() { // TODO for simplicity, we hard code the items _items = new List<Item>(); _items.add(new Item(0, 'Andrew', '14 Dec', "Just a reminder about the report I was telling you about over the phone. Don't forget!")); _items.add(new Item(1, 'James', '13 Dec', "Up for long lunch today?")); _items.add(new Item(2, 'Andrew', '31 Nov', "I've got a great idea for formatting the report for the meeting tomorrow. I'm around between 1 and 2 pm.")); _items.add(new Item(3, 'Andrew', '30 Nov', "Found the perfect gift for your wife!")); _items.add(new Item(4, 'Jane', '29 Nov', "Can we cancel tonight and reschedule? I've got a headache.")); _items.add(new Item(5, 'Andrew', '27 Nov', "Fancy a cup of tea now? Would quite like to pick your brains about this report thingy.")); _items.add(new Item(6, 'Andrew', '26 Nov', "Do you think Steve really need to go to our weekly meeting? Perhaps we should just email him the notes.")); _items.add(new Item(7, 'Andrew', '25 Nov', "I forgot my lunch on top of the fridge and I'm stuck outside office in a meeting. Can you put it in please? Thanks thanks thanks!")); } List<Item> getItems() { return _items; } } |
Adding ‘swipe to dismiss’
To enable the user to “swipe to dismiss”, 3 things are required.
-
- the list item widget must be wrapped in a Dismissible widget
- as soon as onDismissed() is fired, the model must be updated to remove the actual item
- the leave-behind widget (ie what is shown behind the list item being swiped) must be specified
Let’s start with the leave-behind widget. For this code tutorial, we allow both swiping left and right, so we need a widget that shows a delete icon on the left and on the right. How to do this? Well, our friend Row will help us (if Row isn’t your friend, check out our mastering Row and Column code tutorial). This will be used by both screens, so we define this in its own file leave_behind_view.dart, which we place in the subpackage list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import 'package:flutter/material.dart'; class LeaveBehindView extends StatelessWidget { LeaveBehindView({Key key}) : super(key: key); @override Widget build(BuildContext context) { return new Container( padding: const EdgeInsets.all(16.0), child: new Row ( children: <Widget>[ new Icon(Icons.delete), new Expanded( child: new Text(''), ), new Icon(Icons.delete), ], ), ); } } |
Then, we amend both list1_page.dart and list2_page.dart to add a Dismissible widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import 'leave_behind_view.dart'; Widget _buildListRow(Item item) { return new Dismissible( key: new Key(item.id.toString()), direction: DismissDirection.horizontal, onDismissed: (DismissDirection direction) { _delete(item); }, resizeDuration: null, dismissThresholds: _dismissThresholds(), background: new LeaveBehindView(), child: new ListTile( title: new Text(item.name), subtitle: new Text(item.content), )); } Map<DismissDirection, double> _dismissThresholds() { Map<DismissDirection, double> map = new Map<DismissDirection, double>(); map.putIfAbsent(DismissDirection.horizontal, () => 0.5); return map; } void _delete(Item item) { new ItemsRepository().delete(item); setState(() { _items = new ItemsRepository().getItems(); }); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import 'leave_behind_view.dart'; Widget _buildListRow(Item item) { Widget titleRow = new Row( children: <Widget>[ new Icon(Icons.people), new Expanded( child: new Container( padding: new EdgeInsets.symmetric(horizontal: 4.0), child: new Text(item.name, overflow: TextOverflow.ellipsis, maxLines: 1, ), ), ), new Text(item.date), ], ); Widget textSection = new Container( child: new Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: <Widget>[ titleRow, new Text(item.content, overflow: TextOverflow.ellipsis, maxLines: 2,) ], ), ); return new Dismissible( key: new Key(item.id.toString()), direction: DismissDirection.horizontal, onDismissed: (DismissDirection direction) { _delete(item); }, resizeDuration: null, dismissThresholds: _dismissThresholds(), background: new LeaveBehindView(), child: new Container ( padding: new EdgeInsets.all(8.0), child: new Row( children: <Widget>[ new IconButton( onPressed: null, // Not implemented in this code tutorial icon: new Icon(Icons.reply), ), new Expanded( child: textSection), new IconButton( onPressed: null, // Not implemented in this code tutorial icon: new Icon(Icons.archive), ), ], ), ), ); } Map<DismissDirection, double> _dismissThresholds() { Map<DismissDirection, double> map = new Map<DismissDirection, double>(); map.putIfAbsent(DismissDirection.horizontal, () => 0.3); return map; } void _delete(Item item) { new ItemsRepository().delete(item); setState(() { _items = new ItemsRepository().getItems(); }); } |
Lastly, we implement a new method to delete the item from the data repository in list_data.dart.
1 2 3 4 5 |
void delete(Item item) { if (_items.contains(item)) { _items.remove(item); } } |
Voila!
Note: I am aware both delete icons are momentarily visible during the animation. It is less visible in the release apk but still visible. Any advice on this is more than welcome! Update: see first comment below.
What next?
Can you write an integration test to verify this pattern? Read How to write an integration test in Flutter then check out FlutterDriver.scroll (hint: to swipe horizontally, set a value for dx greater than 0).

Mobile app developer with 12 years experience. Started with the Android SDK in 2010 and switched to Flutter in 2017. Past apps range from start ups to large tech (Google) and non tech (British Airways) companies, in various sectors (transport, commercial operations, e-commerce).
You can use the secondaryBackground to customize background, when dragging to the left
Thanks!
You are indeed correct. ‘background’ for background when dragging to the right, and ‘secondaryBackground’ for background when dragging to the left. Not sure why I missed that when I wrote the tutorial, I’ll amend that. For now, I’ve added a reference to your comment.
It is pretty much helpful. I have a doubt. Since list is used here we can decrement the size but how to do the same with JSON. Even if we delete this item, when ever the app reloads it fetch the JSON again and it also showing the deleted results. Any help on this will be appreciated.
Thank you.
The items are hardcoded in init() of the repository as this is an example app. In reality, you would load the items from a db or a backend, and when you delete one item, you would delete it there as well (instead of just deleting it from the in memory list only – in delete(Item item) method in list_data.dart file).