【タスク編集】既存タスクをタップして内容を更新できるようにする
このページでやること
このページでは、すでに作成したタスクをタップして、内容を編集できるようにします。
編集する項目は、次の5つです。
| 項目 | 内容 |
|---|---|
title | タスク名 |
description | 説明 |
assigneeEmail | 担当者メール |
priority | 優先度 |
status | ステータス |
保存場所はここです。
teams/{teamId}/tasks/{taskId}
今日のゴール
タスクをタップすると、編集用のボトムシートが開くようにします。
タスク一覧
↓
タスクをタップ
↓
編集ボトムシートを表示
↓
内容を変更
↓
更新する
↓
Firestoreのタスクが更新される
npmや環境変数は必要?
このページでは、npmは使いません。
環境変数も設定しません。
すでに使っているFirestoreだけで進めます。
必要なimportはこちらです。
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
Step 1:main.dartを開く
次のファイルを開きます。
lib/main.dart
ターミナルから開く場合はこちらです。
code lib/main.dart
Step 2:TaskCardにonTapを追加する
今の TaskCard は表示だけになっています。
タップできるように、onTap を受け取れる形にします。
TaskCard の最初を、次のように変更します。
class TaskCard extends StatelessWidget {
const TaskCard({
super.key,
required this.title,
required this.description,
required this.status,
required this.priority,
required this.assigneeEmail,
required this.onTap,
});
final String title;
final String description;
final String status;
final String priority;
final String assigneeEmail;
final VoidCallback onTap;
追加したのは、ここです。
required this.onTap,
final VoidCallback onTap;
VoidCallback は、タップしたときに動かす処理です。
Step 3:TaskCard全体をタップできるようにする
TaskCard の build() の中で、今はこのような形になっています。
return Align(
alignment: Alignment.centerLeft,
child: Container(
...
),
);
これを、InkWell で包みます。
return Align(
alignment: Alignment.centerLeft,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(18),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(18),
bottomLeft: Radius.circular(18),
bottomRight: Radius.circular(18),
),
border: Border.all(color: AppColors.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_Label(text: statusLabel(status)),
_Label(text: priorityLabel(priority)),
],
),
const SizedBox(height: 10),
Text(
title,
style: const TextStyle(
color: AppColors.text,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
if (description.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
description,
style: const TextStyle(
color: AppColors.subText,
height: 1.5,
fontWeight: FontWeight.w600,
),
),
],
if (assigneeEmail.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: AppColors.bg,
borderRadius: BorderRadius.circular(999),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
child: Text(
'担当:$assigneeEmail',
style: const TextStyle(
color: AppColors.subText,
fontSize: 12,
fontWeight: FontWeight.w800,
),
),
),
],
],
),
),
),
),
);
もし withValues でエラーが出る場合は、次に変えてください。
color: Colors.black.withOpacity(0.04),
Step 4:TaskCardを呼び出す場所を修正する
TaskListPage の中で、TaskCard を呼び出している場所を探します。
今はこのようになっています。
return TaskCard(
title: title,
description: description,
status: status,
priority: priority,
assigneeEmail: assigneeEmail,
);
これを、次のように変更します。
return TaskCard(
title: title,
description: description,
status: status,
priority: priority,
assigneeEmail: assigneeEmail,
onTap: () {
showEditTaskSheet(
taskId: doc.id,
currentTitle: title,
currentDescription: description,
currentAssigneeEmail: assigneeEmail,
currentPriority: priority,
currentStatus: status,
);
},
);
ここで、タップしたタスクの情報を編集画面に渡します。
Step 5:編集用ボトムシートを作る
_TaskListPageState の中に、次の関数を追加します。
Future<void> showEditTaskSheet({
required String taskId,
required String currentTitle,
required String currentDescription,
required String currentAssigneeEmail,
required String currentPriority,
required String currentStatus,
}) async {
titleController.text = currentTitle;
descriptionController.text = currentDescription;
assigneeEmailController.text = currentAssigneeEmail;
selectedPriority = currentPriority;
selectedStatus = currentStatus;
errorText = null;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20),
),
),
builder: (context) {
return StatefulBuilder(
builder: (context, setSheetState) {
return Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 20,
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'タスクを編集',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w900,
color: AppColors.text,
),
),
const SizedBox(height: 12),
AppTextField(
controller: titleController,
label: 'タスク名',
),
const SizedBox(height: 12),
AppTextField(
controller: descriptionController,
label: '説明',
maxLines: 3,
),
const SizedBox(height: 12),
AppTextField(
controller: assigneeEmailController,
label: '担当者メール',
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: selectedPriority,
decoration: const InputDecoration(
labelText: '優先度',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'low',
child: Text('低'),
),
DropdownMenuItem(
value: 'normal',
child: Text('中'),
),
DropdownMenuItem(
value: 'high',
child: Text('高'),
),
],
onChanged: (value) {
if (value == null) return;
setSheetState(() {
selectedPriority = value;
});
},
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: selectedStatus,
decoration: const InputDecoration(
labelText: 'ステータス',
border: OutlineInputBorder(),
),
items: const [
DropdownMenuItem(
value: 'todo',
child: Text('未対応'),
),
DropdownMenuItem(
value: 'doing',
child: Text('進行中'),
),
DropdownMenuItem(
value: 'done',
child: Text('完了'),
),
],
onChanged: (value) {
if (value == null) return;
setSheetState(() {
selectedStatus = value;
});
},
),
if (errorText != null) ...[
const SizedBox(height: 12),
ErrorBox(message: errorText!),
],
const SizedBox(height: 16),
FilledButton(
onPressed: isCreating
? null
: () async {
await updateTask(
setSheetState: setSheetState,
taskId: taskId,
);
},
child: Text(isCreating ? '更新中...' : '更新する'),
),
],
),
),
);
},
);
},
);
}
これで、タスク編集用のボトムシートができます。
Step 6:更新処理 updateTask() を作る
次に、Firestoreのタスクを更新する処理を作ります。
_TaskListPageState の中に、次の関数を追加します。
Future<void> updateTask({
required StateSetter setSheetState,
required String taskId,
}) async {
final title = titleController.text.trim();
final description = descriptionController.text.trim();
final assigneeEmail = assigneeEmailController.text.trim();
if (title.isEmpty) {
setSheetState(() {
errorText = 'タスク名を入力してください。';
});
return;
}
setSheetState(() {
isCreating = true;
errorText = null;
});
try {
await FirebaseFirestore.instance
.collection('teams')
.doc(widget.teamId)
.collection('tasks')
.doc(taskId)
.update({
'title': title,
'description': description,
'assigneeEmail': assigneeEmail,
'priority': selectedPriority,
'status': selectedStatus,
'updatedAt': FieldValue.serverTimestamp(),
});
if (mounted) {
Navigator.of(context).pop();
}
} on FirebaseException catch (e) {
setSheetState(() {
errorText = e.message ?? 'タスク更新に失敗しました。';
});
} catch (_) {
setSheetState(() {
errorText = 'タスク更新に失敗しました。';
});
} finally {
if (mounted) {
setSheetState(() {
isCreating = false;
});
}
}
}
これで、既存タスクを更新できます。
Step 7:更新する場所を確認する
今回の中心はここです。
await FirebaseFirestore.instance
.collection('teams')
.doc(widget.teamId)
.collection('tasks')
.doc(taskId)
.update({
...
});
これは、次の場所を更新しています。
teams/{teamId}/tasks/{taskId}
teamId は、今開いているチームのIDです。
taskId は、タップしたタスクのIDです。
Step 8:add() と update() の違い
タスク作成では add() を使いました。
.collection('tasks').add({...})
タスク編集では update() を使います。
.collection('tasks').doc(taskId).update({...})
違いはこうです。
| 命令 | 役割 |
|---|---|
add() | 新しいデータを作る |
update() | すでにあるデータを更新する |
今回は既存タスクの編集なので、update() を使います。
Step 9:updatedAtを更新する
編集時には、必ず updatedAt も更新します。
'updatedAt': FieldValue.serverTimestamp(),
updatedAt は、最後に編集した日時です。
これを入れておくと、あとで「最近更新されたタスク順」に並べることができます。
Step 10:titleは必須にする
タスク名が空だと、何のタスクか分かりません。
編集時も、タスク名は必須にします。
if (title.isEmpty) {
setSheetState(() {
errorText = 'タスク名を入力してください。';
});
return;
}
説明や担当者メールは、空でもOKです。
Step 11:保存する
main.dart を保存します。
Macの場合:
command + S
Windowsの場合:
Ctrl + S
Step 12:実行する
ターミナルで実行します。
flutter run
すでに起動している場合は、ターミナルで r を押します。
r
うまく反映されない場合は、R を押します。
R
Step 13:タスクを編集してみる
アプリを開きます。
ログイン
↓
トーク一覧
↓
チームをタップ
↓
タスク一覧画面
↓
編集したいタスクをタップ
編集ボトムシートが開きます。
次のように変更してみます。
タスク名:
ログイン画面を作る
↓
ログイン画面を完成させる
ステータス:
未対応
↓
進行中
優先度:
中
↓
高
「更新する」を押します。
タスク一覧の表示が変われば成功です。
Step 14:Firestoreで確認する
Firebase Consoleを開きます。
https://console.firebase.google.com/
次の場所を確認します。
Firestore Database
↓
teams
↓
作成したteamId
↓
tasks
↓
編集したtaskId
次の項目が更新されていればOKです。
title
description
assigneeEmail
priority
status
updatedAt
よくあるエラーと直し方
| エラー | 原因 | 直し方 |
|---|---|---|
onTap がない | TaskCardにonTapを追加していない | final VoidCallback onTap; を追加 |
showEditTaskSheet isn't defined | 編集用関数がない | _TaskListPageState に追加 |
updateTask isn't defined | 更新処理がない | updateTask() を追加 |
taskId が取れない | doc.id を渡していない | taskId: doc.id にする |
widget.teamId が使えない | TaskListPageがStatefulWidgetでない | StatefulWidgetにする |
update() で失敗する | taskIdが間違っている | FirestoreのtaskIdを確認 |
permission-denied | Firestore Rulesで拒否 | 開発用Rulesを確認 |
permission-denied** が出たとき**
開発中だけ、Firestore Rulesを次のようにして確認できます。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
これは学習用です。
本番公開では使わないでください。
本番では、チームメンバーだけがタスクを編集できるようにします。
最短作業まとめ
読むのが大変な人は、ここだけ見てください。
1. TaskCardにonTapを追加
final VoidCallback onTap;
2. TaskCardをタップできるようにする
InkWell(
onTap: onTap,
child: Container(...),
)
3. TaskCard呼び出しで編集画面を開く
return TaskCard(
title: title,
description: description,
status: status,
priority: priority,
assigneeEmail: assigneeEmail,
onTap: () {
showEditTaskSheet(
taskId: doc.id,
currentTitle: title,
currentDescription: description,
currentAssigneeEmail: assigneeEmail,
currentPriority: priority,
currentStatus: status,
);
},
);
4. Firestoreを更新する
await FirebaseFirestore.instance
.collection('teams')
.doc(widget.teamId)
.collection('tasks')
.doc(taskId)
.update({
'title': title,
'description': description,
'assigneeEmail': assigneeEmail,
'priority': selectedPriority,
'status': selectedStatus,
'updatedAt': FieldValue.serverTimestamp(),
});
5. 保存して実行
flutter run
チェックリスト
□ main.dartを開いた
□ TaskCardにonTapを追加した
□ TaskCardをInkWellで包んだ
□ TaskCard呼び出しにonTapを追加した
□ doc.idをtaskIdとして渡した
□ showEditTaskSheet()を作った
□ 現在の値をControllerに入れた
□ updateTask()を作った
□ teams/{teamId}/tasks/{taskId} を更新した
□ titleを更新した
□ descriptionを更新した
□ assigneeEmailを更新した
□ priorityを更新した
□ statusを更新した
□ updatedAtを更新した
□ 保存した
□ flutter runで確認した
□ タスクをタップして編集できた
ミニ確認問題
Q1. 既存タスクの更新には add() と update() のどちらを使いますか?
回答
update() を使います。
add() は新規作成、update() は既存データの更新です。
Q2. 編集するタスクを特定するために必要なIDは何ですか?
回答
taskId です。
Firestoreの doc.id を使います。
Q3. 編集時に必ず更新する日時項目は何ですか?
回答
updatedAt です。
最後に編集した日時を保存します。
Q4. このページでnpmや環境変数は必要ですか?
回答
必要ありません。
このページでは、既存のFirestoreデータを update() で更新するだけです。
このページのまとめ
- タスクをタップして編集ボトムシートを開く。
TaskCardにonTapを追加する。- 編集前の値をControllerに入れて表示する。
- 既存タスクの更新には
update()を使う。 - 更新場所は
teams/{teamId}/tasks/{taskId}。 title、description、assigneeEmail、priority、statusを更新する。- 編集時は
updatedAtも更新する。 - このページではnpmや環境変数は不要。
次のページでやること
次のページでは、タスクを左スワイプして削除できるようにします。
Dismissible を使って、Firestoreから tasks/{taskId} を削除します。
