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.

Create app flutter create swipetodismissexample 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.

main.dart 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(), ); } } 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.

list/list1_page.dart 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(); }, )); } } 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.

list/list2_page.dart 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), ), ], ), ); } } 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.

data/list_data.dart 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; } } 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 ; } }

Looking for a Flutter job? Check out my job board dedicated to Flutter at flutterjobs.info

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.

list/leave_behind_view.dart 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), ], ), ); } } 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.

list/list1_page.dart 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 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 ( ) ; } ) ; }

list/list2_page.dart 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(); }); } 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.

data/list_data.dart void delete(Item item) { if (_items.contains(item)) { _items.remove(item); } } 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).

Check out Being a software developer in 2020!

Related