🦁

Flutterでキャンセルボタンを独立させた選択肢としてボトムに表示する Cupertino ver.

4 min read

iOSアプリでよくみる以下のようなキャンセルボタンが独立したモーダルを画面下に表示したい。

Image from Gyazo

環境

  • macOS Big Sur 11.3.1
  • Flutter 2.2.0

問題点1: 名前がわからない

  • これはなんていうWidget?

Webの経験はあるものの、アプリ開発についてはまったくわからず、🔼がなんて呼ばれるものなのかが分からず。。。
たまたま、以下の本を持っていることを思い出し「もしかして・・・」と思い調べたところ、「アクションシート」というものらしい。

https://www.amazon.co.jp/dp/B087BMJ1Q6

一般的な呼称がわかったので、「Flutter アクションシート」のようなキーワードで検索したところいくつか記事を発見。

https://medium.com/codechai/actionsheet-in-flutter-13b1087987f0

https://medium.com/flutterdevs/cupertinoactionsheet-in-flutter-ec5658c19e6b

どうやら、CupertinoActionSheetというWidgetを使えばよさげ。

使ってみる

勉強用につくったTODOアプリに組み込む形で試してみる。

https://zenn.dev/captain_blue/articles/display-form-bottom-and-list-top

https://github.com/captain-blue210/flutter_todo_app

動きとしては、タスクを削除するときの確認モーダルとして以下を表示させたい。

  • タスク名 + 注意文言
  • Delete Task(削除後はタスク一覧画面に遷移)
  • Cancel

実装

// 省略
IconButton(
   onPressed: () {
   showCupertinoModalPopup<void>(
         context: context,
         builder: (BuildContext context) {
         return CupertinoActionSheet(
            message: Text(
               // ignore: lines_longer_than_80_chars
               '${snapshot.data!["taskName"].toString()} will be permanentally deleted',
               style: const TextStyle(fontSize: 15),
            ),
            actions: <Widget>[
               CupertinoActionSheetAction(
               child: const Text('Delete Task'),
               isDestructiveAction: true,
               onPressed: () {
                  TaskLogic.delete(docId);
                  Navigator.pop(context);
               },
               )
            ],
            cancelButton: CupertinoActionSheetAction(
               child: Text("Cancel"),
               onPressed: () {
               Navigator.pop(context);
               },
            ),
         );
         });
   },
   icon: const Icon(Icons.delete),
)
// 省略

Image from Gyazo

いい感じにできた。Webの場合はこの形にデザインする必要があるけど、Flutterだと基本的なデザインはすでにされているのでとてもありがたい。

問題点2: タスクを削除したあとにアプリのホーム画面に戻る動きがおかしい

見た目はほぼ完璧にやりたかったものができたが、このままだと問題が1点あって「Delete Task」したときにずっとローディングになってしまう。

Image from Gyazo

アクションシートがRouteに追加されるので、pop()したときに表示されるのがタスク詳細画面になってしまい、すでに削除されたデータを読み込もうとしてずっとローディングが表示されているという事象のよう。

  1. タスク詳細画面
  2. アクションシート
  3. タスク詳細画面 ←存在しないデータを読み込もうとしてずっとローディングが表示

2.のアクションシートで「Delete Task」をしたときにタスク詳細ではなく、タスク一覧画面に遷移させるようにしたい。
タスク一覧画面は初期画面なので、popUntil()を使えばよさげ。

https://qiita.com/fujit33/items/09dc5bfcd6cab8d6698e

こちらを参考に事前定義をせずに初期画面に遷移するよう修正。

CupertinoActionSheetAction(
   child: const Text('Delete Task'),
   isDestructiveAction: true,
   onPressed: () async {
      await TaskLogic.delete(docId);
      // popUntilで最初のページまで戻る
      Navigator.of(context)
         .popUntil((route) => route.isFirst);
   },
)

Image from Gyazo

これで想定した動きが実装できた。

最後に

今回、ルートを事前定義せずに遷移させる方法で実装したけど、あらかじめルートを定義する方法でもやってみたい。
また、今回はCupertinoActionSheetを使ったが、iOS特有なWidgetなのでMaterial Componentsを使って似たようなUIにしたほうがいいかも。
-> Androidでも表示されるっぽい。(2021/06/27追記)

Image from Gyazo

参考