Flutter Swipe list item to delete

eye-catch Dart and Flutter

It’s common to let a user swipe a list item for more options in smartphone applications. For example, we can archive an item in Gmail. Flutter offers a Dismissible class for the feature. We will implement it to let a user swipe a list item in both directions, namely left to right and right to left.

You can find the complete code here.

Sponsored links

Dismissible Widget with minimal code

First of all, let’s check how Dismissible widget works with the minimal code.

class SwipeListItem extends StatefulWidget {
  @override
  _SwipeListItem createState() => _SwipeListItem();
}

class _SwipeListItem extends State<SwipeListItem> {
  GlobalKey<ScaffoldState> _key = GlobalKey();
  List<int> _data = [for (var i = 0; i < 50; i++) i];

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        key: _key,
        appBar: AppBar(
          title: Text("Swipe List Item"),
        ),
        body: _createBody(context),
      ),
    );
  }

  Widget _createBody(BuildContext context) {
    final count = _data.length;
    final countDisp = Text("Count: ${_data.length}");

    final listView = ListView.builder(
        itemCount: count,
        itemBuilder: (context, index) {
          return Dismissible(
            key: ObjectKey(_data[index]),
            child: ListTile(
              title: Text("Item number - ${_data[index]}"),
          );
        });

    return Column(
      children: [
        Center(child: countDisp),
        Flexible(child: listView),
      ],
    );
  }
}

The items can be swiped in both directions but the background of the items is white. The count keeps showing 50 even though the items look deleted. We need to implement them ourselves.

Sponsored links

Add background colors and icons

Let’s show the delete icon with red color when swiping an item from right to left. The widgets for the icons are as follows.

final leftEditIcon = Container(
  color: Colors.green,
  child: Icon(Icons.edit),
  alignment: Alignment.centerLeft,
);
final rightDeleteIcon = Container(
  color: Colors.red,
  child: Icon(Icons.delete),
  alignment: Alignment.centerRight,
);

Wrap the icon by a container and set the alignment. Set the widget to background and secondaryBackground.

return Dismissible(
  key: ObjectKey(_data[index]),
  child: ListTile(
    title: Text("Item number - ${_data[index]}"),
  ),
  // left side
  background: leftEditIcon,
  // right side
  secondaryBackground: rightDeleteIcon,
};
delete-icon
edit-icon

Add logic when a list item is dismissed

The next step is to add logic to delete an item. We need to set a function to onDismissed parameter. Since it passes direction parameter, we can define different behavior for left-right and right-left directions.

onDismissed: (DismissDirection direction) {
  if (direction == DismissDirection.startToEnd) {
    // Left to right
    print("Edit");
  } else if (direction == DismissDirection.endToStart) {
    // Right to left
    print("Delete");

    setState(() {
      _data.removeAt(index);
    });
  }
},

As you can see, count is decremented when swiping an item to the left side. The item is not deleted if it is moved to the right side.

Note that the following error occurs when the view is loaded if the item is immediately not deleted.

════════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building Dismissible-[int#00007](dirty, dependencies: [_EffectiveTickerMode, Directionality], state: _DismissibleState#e9fd1(tickers: tracking 2 tickers)):
A dismissed Dismissible widget is still part of the tree.
2

Make sure to implement the onDismissed handler and to immediately remove the Dismissible widget from the application once that handler has fired.

The relevant error-causing widget was
Dismissible-[int#00007]
lib\swipe_list_item.dart:46

Add UNDO feature

We definitely want to add undo feature because a user might delete an item by mistake. Maybe, do you want to show the undo button at the bottom? Let’s use a Snackbar widget for it.

onDismissed: (DismissDirection direction) {
  if (direction == DismissDirection.startToEnd) {
    // Left to right
    print("Edit");
  } else if (direction == DismissDirection.endToStart) {
    // Right to left
    print("Delete");

    setState(() {
      int deletedItem = _data.removeAt(index);

      ScaffoldMessenger.of(context)
        ..removeCurrentSnackBar()
        ..showSnackBar(
          SnackBar(
            content: Text("Deleted \"Item number - $deletedItem\""),
            action: SnackBarAction(
              label: "UNDO",
              onPressed: () {
                setState(() => _data.insert(index, deletedItem));
              },
            ),
          ),
        );
    });
  }
},

Double dots are useful if we want to call methods in a row for the same instance. As I mentioned above, the dismissed item must immediately be deleted, therefore we have to insert the data again if we want to undo it.

How to implement undo with database

We definitely need to interact with a database. In this case, it’s not good to delete the entry from a database and insert the data again because the data entry can be different if some of the values are assigned automatically. I think we should do the delete process if a user doesn’t undo it.

onDismissed: (DismissDirection direction) {
  if (direction == DismissDirection.startToEnd) {
    // Left to right
    print("Edit");
  } else if (direction == DismissDirection.endToStart) {
    // Right to left
    print("Delete");

    late int deletedItem;
    setState(() {
      deletedItem = _data.removeAt(index);
    });

    // Set timer here
    final timer = Timer(
      Duration(seconds: 3),
      () {
        // Call SQL query here
        showMessage(
          context,
          "Execute delete query for database",
          "Database Access here",
        );
      },
    );

    ScaffoldMessenger.of(context)
      ..removeCurrentSnackBar()
      ..showSnackBar(
        SnackBar(
          // Set duration. It should be shorter than the timer's one
          duration: Duration(
            seconds: 2,
            milliseconds: 500,
          ),
          content: Text("Deleted \"Item number - $deletedItem\""),
          action: SnackBarAction(
            label: "UNDO",
            onPressed: () {
              // Call cancel if a user presses undo
              timer.cancel();
              setState(() => _data.insert(index, deletedItem));
            },
          ),
        ),
      );
  }
},

The duration specified in the Snackbar should be shorter than the timer’s one. If we set the same value to both, undo can fail because the timer starts before Snackbar’s timer starts.

If we need to interact with a database, the data is Future value. In this case, we should call the query in initState() method in order not to call it again after we delete the item. Otherwise, the view doesn’t change.

How to handle editing

The item must immediately be deleted when it’s dismissed. If we want to edit an item, we need to prevent it from dismissing. We can define logic in confirmDismiss parameter to decide whether it’s dismissed or not.

confirmDismiss: (DismissDirection direction) async {
  if (direction == DismissDirection.startToEnd) {
    await showMessage(context, "Go to edit page", "Edit");
    return false;
  } else {
    return Future.value(direction == DismissDirection.endToStart);
  }
},

For the editing process, we always have to return false in order not to call the onDismissed event. Do the editing process on a different page. Once the process is done, go forward the process again and return false. If the item is updated, we need to set it to the current item within setState. Don’t forget to call setState to update the view.

If we want to allow a user to swipe only one direction, we can set it to direction parameter.

direction: DismissDirection.endToStart,

Related articles

Comments

Copied title and URL