【スワイプ削除】Dismissibleでチームを左スワイプ削除できるようにする
このページでやること
このページでは、トーク一覧に表示されているチームを、左スワイプで削除できるようにします。
Flutterでは、スワイプ削除に Dismissible というWidgetを使います。
チーム一覧
↓
チームカードを左にスワイプ
↓
削除確認
↓
Firestoreからチームを削除
↓
一覧から消える
このページでは、まずシンプルに削除できるところまで作ります。
今日のゴール
チームカードを左へスワイプすると、赤い削除背景が出て、チームを削除できるようにします。
開発チーム ← 左にスワイプ
↓
削除
↓
一覧から消える
削除対象は、Firestoreのここです。
teams/{teamId}
このページで出てくる単語
| 単語 | 一言説明 |
|---|---|
Dismissible | スワイプで項目を消せるWidget |
direction | スワイプできる方向 |
endToStart | 右から左へスワイプする方向 |
confirmDismiss | 本当に削除してよいか確認する処理 |
onDismissed | スワイプ後に実行される処理 |
delete() | Firestoreのデータを削除する命令 |
Key | 一覧の1件1件を区別するための印 |
Widget とは、Flutterの画面部品のことです。
npmや環境変数はこのページで必要?
このページでは、npmは使いません。
環境変数も設定しません。
必要なのは、すでに使っているFirestoreです。
import 'package:cloud_firestore/cloud_firestore.dart';
このページでやることは、TeamCard を Dismissible で包むことです。
Step 1:main.dartを開く
次のファイルを開きます。
lib/main.dart
ターミナルから開く場合は、次を実行します。
code lib/main.dart
Step 2:TeamListPageを探す
main.dart の中で、次を検索します。
class TeamListPage
VS Codeなら、次で検索できます。
command + F
この中にある ListView.separated を変更します。
Step 3:今のTeamCard表示を確認する
前のページでは、チーム一覧の1件を次のように表示していました。
return TeamCard(
name: name,
onTap: () {
// 次のページでタスク一覧画面へ移動します
},
);
今回は、この TeamCard を Dismissible で包みます。
Step 4:チーム削除処理を作る
まず、_TeamListPageState の中に、次の関数を追加します。
Future<void> deleteTeam(String teamId) async {
await FirebaseFirestore.instance.collection('teams').doc(teamId).delete();
}
これは、Firestoreの teams/{teamId} を削除する処理です。
teamId は、削除したいチームのIDです。
Step 5:削除確認ダイアログを作る
いきなり削除すると危ないので、確認画面を出します。
_TeamListPageState の中に、次の関数を追加します。
Future<bool> confirmDeleteTeam(String teamName) async {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('チームを削除しますか?'),
content: Text('「$teamName」を削除します。'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('キャンセル'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: AppColors.danger,
),
child: const Text('削除する'),
),
],
);
},
);
return result == true;
}
showDialog は、確認用の小さな画面を出す命令です。
true が返ったら削除します。
false なら削除しません。
Step 6:TeamCardをDismissibleで包む
ListView.separated の中で、次の部分を探します。
return TeamCard(
name: name,
onTap: () {
// 次のページでタスク一覧画面へ移動します
},
);
これを、次のコードに置き換えます。
return Dismissible(
key: ValueKey(doc.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: AppColors.danger,
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (_) async {
return confirmDeleteTeam(name);
},
onDismissed: (_) async {
await deleteTeam(doc.id);
},
child: TeamCard(
name: name,
onTap: () {
// 次のページでタスク一覧画面へ移動します
},
),
);
これで、チームを左スワイプできるようになります。
Step 7:Dismissibleの意味を見る
今回の中心はここです。
Dismissible(
key: ValueKey(doc.id),
direction: DismissDirection.endToStart,
...
)
Dismissible は、スワイプで消せる一覧項目を作るWidgetです。
key は、一覧の中の1件を区別するための印です。
今回は、FirestoreのドキュメントIDを使います。
key: ValueKey(doc.id),
doc.id は、FirestoreのチームIDです。
Step 8:左スワイプだけにする
この部分です。
direction: DismissDirection.endToStart,
endToStart は、右から左へスワイプする方向です。
日本語の画面では、左スワイプ削除のイメージになります。
右 → 左
間違えて右スワイプでも消えないように、方向を限定しています。
Step 9:赤い削除背景を作る
この部分です。
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: AppColors.danger,
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
スワイプしたときに、赤い背景と削除アイコンが出ます。
alignment: Alignment.centerRight にすることで、右側に削除アイコンを表示します。
Step 10:削除前に確認する
この部分です。
confirmDismiss: (_) async {
return confirmDeleteTeam(name);
},
confirmDismiss は、スワイプ後に本当に削除してよいか確認する場所です。
ここで true が返ると、削除が進みます。
false が返ると、元に戻ります。
左スワイプ
↓
確認画面
↓
削除する → true
キャンセル → false
Step 11:Firestoreから削除する
この部分です。
onDismissed: (_) async {
await deleteTeam(doc.id);
},
onDismissed は、スワイプ削除が確定したあとに動きます。
ここで、Firestoreのチームを削除します。
await FirebaseFirestore.instance.collection('teams').doc(teamId).delete();
Step 12:保存する
main.dart を保存します。
Macの場合:
command + S
Windowsの場合:
Ctrl + S
Step 13:実行する
ターミナルで実行します。
flutter run
すでに起動している場合は、ターミナルで r を押します。
r
動きがおかしい場合は、R でホットリスタートします。
R
Step 14:スワイプ削除を試す
ログイン後、トーク一覧画面を開きます。
チームカードを左へスワイプします。
チームカード
↓
左へスワイプ
↓
赤い削除背景が出る
↓
確認画面が出る
↓
削除する
削除後、一覧からチームが消えれば成功です。
Step 15:Firestoreで確認する
Firebase Consoleを開きます。
https://console.firebase.google.com/
次の順番で確認します。
Firestore Database
↓
データ
↓
teams
削除したチームのドキュメントが消えていればOKです。
注意:membersやtasksはどうなる?
今回のコードでは、teams/{teamId} だけを削除します。
ただし、Firestoreでは、親ドキュメントを削除しても、サブコレクションが自動で完全削除されるとは限りません。
つまり、次のデータが残る場合があります。
teams/{teamId}/members
teams/{teamId}/tasks
学習段階では、まず teams/{teamId} を削除できればOKです。
本番アプリでは、Cloud Functionsや管理用処理で、関連する members や tasks も整理する設計にします。
よくあるエラーと直し方
| エラー | 原因 | 直し方 |
|---|---|---|
Dismissible が使えない | Materialのimport不足 | import 'package:flutter/material.dart'; を確認 |
doc.id が使えない | doc の場所が違う | final doc = teams[index]; の下で使う |
| スワイプできない | Dismissible で包んでいない | TeamCard を Dismissible で包む |
| 右スワイプでも消える | direction未指定 | DismissDirection.endToStart を指定 |
| 削除後にエラー | Firestore Rulesで拒否 | 開発用Rulesを確認 |
| 削除後に復活する | Firestore削除に失敗している | deleteTeam(doc.id) を確認 |
| 確認画面が出ない | confirmDismiss がない | confirmDismiss を追加 |
permission-denied** が出たとき**
開発中だけ、Firestore Rulesを次のようにして確認できます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
これは学習用です。
本番公開では使わないでください。
本番では、owner や admin だけがチームを削除できるようにします。
TeamCardだけ消えてFirestoreに残る場合
confirmDismiss は動いているが、Firestore削除が失敗している可能性があります。
この部分を確認してください。
onDismissed: (_) async {
await deleteTeam(doc.id);
},
そして、deleteTeam() が次のようになっているか確認します。
Future<void> deleteTeam(String teamId) async {
await FirebaseFirestore.instance.collection('teams').doc(teamId).delete();
}
本番では権限チェックが必要
この教材では、まず動きを理解するために、誰でも削除できる形で進めています。
本番では、必ず権限チェックを入れます。
ownerだけ削除できる
adminだけ削除できる
memberは削除できない
viewerは削除できない
権限チェックは、後のSecurity Rulesのページで扱います。
最短作業まとめ
読むのが大変な人は、ここだけ見てください。
1. 削除処理を追加
Future<void> deleteTeam(String teamId) async {
await FirebaseFirestore.instance.collection('teams').doc(teamId).delete();
}
2. 削除確認を追加
Future<bool> confirmDeleteTeam(String teamName) async {
final result = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('チームを削除しますか?'),
content: Text('「$teamName」を削除します。'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('キャンセル'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
style: FilledButton.styleFrom(
backgroundColor: AppColors.danger,
),
child: const Text('削除する'),
),
],
);
},
);
return result == true;
}
3. TeamCardをDismissibleで包む
return Dismissible(
key: ValueKey(doc.id),
direction: DismissDirection.endToStart,
background: Container(
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
decoration: BoxDecoration(
color: AppColors.danger,
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.delete,
color: Colors.white,
),
),
confirmDismiss: (_) async {
return confirmDeleteTeam(name);
},
onDismissed: (_) async {
await deleteTeam(doc.id);
},
child: TeamCard(
name: name,
onTap: () {
// 次のページでタスク一覧画面へ移動します
},
),
);
4. 保存して実行
flutter run
チェックリスト
□ main.dartを開いた
□ TeamListPageを探した
□ deleteTeam()を作った
□ confirmDeleteTeam()を作った
□ TeamCardをDismissibleで包んだ
□ keyにValueKey(doc.id)を指定した
□ directionにendToStartを指定した
□ 赤い削除背景を作った
□ confirmDismissを追加した
□ onDismissedでdeleteTeam(doc.id)を呼んだ
□ 保存した
□ flutter runで起動した
□ 左スワイプで削除確認が出た
□ 削除後に一覧から消えた
ミニ確認問題
Q1. Dismissible は何をするWidgetですか?
回答
一覧の1件を、スワイプで削除できるようにするWidgetです。
Q2. DismissDirection.endToStart はどの方向ですか?
回答
右から左へのスワイプです。
今回の左スワイプ削除に使います。
Q3. confirmDismiss は何のために使いますか?
回答
本当に削除してよいか、削除前に確認するために使います。
true を返すと削除、false を返すとキャンセルです。
Q4. このページでnpmや環境変数は必要ですか?
回答
必要ありません。
このページでは、Flutterの Dismissible とFirestoreの delete() を使うだけです。
このページのまとめ
Dismissibleを使うと、一覧項目をスワイプ削除できる。direction: DismissDirection.endToStartで左スワイプに限定できる。backgroundで赤い削除背景を作る。confirmDismissで削除前に確認できる。onDismissedでFirestoreから削除する。- 今回は
teams/{teamId}を削除する。 - 本番では
ownerやadminだけが削除できるようにする。 - このページではnpmや環境変数は不要。
次のページでやること
次のページでは、チームをタップしてタスク一覧画面へ移動します。
teamId と teamName を渡して、そのチーム専用のタスク一覧を表示する準備をします。
