【Flutter】リストをスワイプで消す(Dismissible)

対象者

  • FlutterでListView内のタイルなどをスワイプで消したい人

はじめに

Flutterの中でリストの一覧を作成することがあります。その一覧の中のデータを削除しようとしたときに、長押しで削除確認ダイアログを出すようにしていました。しかしスワイプでも同じことができるということで、UX的にそっちの方が良いケースもあるでしょうから、実験してみました。

実施するソース

urlsはListで中に画像のURLの一覧が入っています。そのURLの一覧をListView.builderで表示します。実際に表示している部分の定義は、Dismissibleの引数のchildに渡している AspectRatioの箇所で、画像を16:9の比率で表示してます。
Dismissibleでスワイプで削除できるようにできるWidgetです。各画像をこれで包むことで、画像を消せるようにしています。

ListView.builder(
    itemCount: urls.length,
    itemBuilder: (context, index) {
      var url = urls[index];
      return Padding(
        padding: const EdgeInsets.all(8.0),
        child: Dismissible(
          key: ValueKey(url),
          onDismissed: (direction) {
            setState(() {
              urls.removeAt(index);
            });
          },
          direction: DismissDirection.startToEnd,
          confirmDismiss: (direction) async {
            var result = await showDialog<bool>(
                                【中略】
            );
            return Future.value(result);
          },
          background: Container(
            color: Colors.black26,
            child: const Align(
              alignment: Alignment.centerLeft,
              child: Icon(
                Icons.delete,
                color: Colors.white,
                size: 48,
              ),
            ),
          ),
          dismissThresholds: const {DismissDirection.startToEnd: 0.4},
          child: AspectRatio(
              aspectRatio: 16 / 9,
              child: Image.network(
                url,
                fit: BoxFit.fitWidth,
              )),
        ),
      );
    },

Dismissibleを細かく見ていきましょう。

  • key
    他の画像を削除した後、同じWidgetを再利用してくれるようにkeyを渡してます。URLがユニークであることが前提になってます。

  • onDismissed
    スワイプが完了して、削除すると決まったときの処理を定義します。ここではurlsからデータを削除しています。そうすることで、削除しようとした画像が表示されなくなります。

  • direction
    スワイプする方向を指定します。今回は「DismissDirection.startToEnd」ですので、左から右にスワイプしたときに削除の処理がされます。他にも「右から左(endToStart)」「水平(horizontal)」「上から下(down)」「下から上(up)」「垂直(vertical)」が指定できます。
    ちなみに今回「垂直」を選択したら、消せる代わりに、スクロールできなくなりました(笑)

  • confirmDismiss
    スワイプを実施したときと削除処理の間に実行します。Future.value(true)を返すと削除され、Future.value(false)だと削除がキャンセルされます。
    この場合、本当に削除するかダイアログを表示して聞いています。

  • background
    削除しようとスワイプしたときに表示されるWidgetを定義します。黒色でゴミ箱が表示されるようにしています。設定無しで中央に配置されるのと、半分までスワイプしないとゴミ箱が表示されないので左寄せにしてます。

  • dismissThresholds
    どれくらいスワイプしたら、削除処理を実行するかを決めます。
    マップで「どの向きに対して」をキーにして、「どれくらいの割合」を値にします。0に近い方が少しでもスワイプすると、削除処理を開始するようになります。

  • child
    実際に表示するWidgetを定義します。この場合、16:9で表示されたネットワーク画像です。

まとめ

ということで、スワイプでWidgetを消す方法を紹介しました。以前パッケージを使って実現しましたが、標準のWidgetでできるんですね!

参考

「スライドして削除」に取り組もうとしたきっかけのページ

全ソース

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
      ),
      home: const MyHomePage(title: 'Flutter Demo: swipe to dismiss'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const urlPrefix =
      'https://docs.flutter.dev/cookbook/img-files/effects/parallax';
  final urls = [
    '$urlPrefix/01-mount-rushmore.jpg',
    '$urlPrefix/02-singapore.jpg',
    '$urlPrefix/03-machu-picchu.jpg',
    '$urlPrefix/04-vitznau.jpg',
    '$urlPrefix/05-bali.jpg',
    '$urlPrefix/06-mexico-city.jpg',
    '$urlPrefix/07-cairo.jpg',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: ListView.builder(
        itemCount: urls.length,
        itemBuilder: (context, index) {
          var url = urls[index];
          return Padding(
            padding: const EdgeInsets.all(8.0),
            child: Dismissible(
              key: ValueKey(url),
              onDismissed: (direction) {
                setState(() {
                  urls.removeAt(index);
                });
              },
              direction: DismissDirection.startToEnd,
              confirmDismiss: (direction) async {
                var result = await showDialog<bool>(
                  context: context,
                  builder: (BuildContext context) => AlertDialog(
                    content: const Text('Would you delete it?'),
                    actions: <Widget>[
                      SimpleDialogOption(
                        child: const Text('Cancel'),
                        onPressed: () => Navigator.pop(context, false),
                      ),
                      SimpleDialogOption(
                        child: const Text('Delete'),
                        onPressed: () => Navigator.pop(context, true),
                      ),
                    ],
                  ),
                );
                return Future.value(result);
              },
              background: Container(
                color: Colors.black26,
                child: const Align(
                  alignment: Alignment.centerLeft,
                  child: Icon(
                    Icons.delete,
                    color: Colors.white,
                    size: 48,
                  ),
                ),
              ),
              dismissThresholds: const {DismissDirection.startToEnd: 0.4},
              child: AspectRatio(
                  aspectRatio: 16 / 9,
                  child: Image.network(
                    url,
                    fit: BoxFit.fitWidth,
                  )),
            ),
          );
        },
      ),
    );
  }
}