# 不專業教學系列 - Flutter To-Do List 第四集 To-Do 的編輯終於實現了!
>tag: `Flutter`、`Dart`
各位讀者老爺大家好,今天我們繼續來完成 To-Do List 專案八!
在前幾篇文章中,不論是 App 內操作還是連線後端操作,我們都只有做新增與讀取而已,刪除功能也沒有去刪除資料庫內容,這很明顯是不合理的,所以今天這篇文章將會完成剩下的基本功能。
### 今天的重點:
* 更新 To-Do 內容
* 將更新後的 To-Do 或是狀態送到後端處理
* 從後端取得已完成的事項,並顯示在畫面上
後端部分請從我的[Github](https://github.com/allian000/todo_backend.git)下載,並切換分支到`section_4`
PS. 看起來只有三件事而已,但有一點小複雜,還請讀者老爺們耐心觀看文章,謝謝QQ
## 更新 To-Do 內容
很高興的是,我們終於要來完成更新內容的功能了!
先新增一個**更新**按鈕,在原本`_showTodos`的位置將`trailing`做更改:
```dart
trailing: Wrap(
spacing: 10,
children: <Widget>[
IconButton(
onPressed: () {},
icon: const Icon(Icons.edit, color: Colors.blueGrey)),
IconButton(
icon: const Icon(Icons.delete, color: Colors.blueGrey),
onPressed: () {
provider.removeTodoAt(provider.todoList[index]);
},
),
],
),
```
在原本新增 To-Do 時我們有用到`showDialog()`來顯示輸入框,那要修改以有的項目也可以`onPressed()`這樣做:
```dart
// 記得加上
final GlobalKey<FormState> _todoUpdateFormKey = GlobalKey<FormState>();
...
onPressed: () {
// 把目前的文字帶進來
_todoTitleController.text = provider.todoList[index].title;
_todoContentController.text = provider.todoList[index].content;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("修改待辦事項",
style: TextStyle(fontWeight: FontWeight.bold)),
content: Form(
key: _todoUpdateFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
TextFormField(
controller: _todoTitleController,
decoration: const InputDecoration(hintText: "待辦事項名稱"),
validator: (value) {
if (value == null || value.isEmpty) {
return '名稱不能為空!';
}
if (value.trim().isEmpty) {
return '請輸入名稱!';
}
return null;
},
),
TextFormField(
controller: _todoContentController,
decoration: const InputDecoration(hintText: "待辦事項內容"),
validator: (value) {
if (value == null || value.isEmpty) {
return '內容不能為空!';
}
if (value.trim().isEmpty) {
return '請輸入內容!';
}
return null;
},
),
],
),
),
actions: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey.shade600,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
onPressed: () {
Navigator.pop(context);
_todoTitleController.clear();
_todoContentController.clear();
},
child: const Text(
"取消",
style: TextStyle(color: Colors.white),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueGrey.shade600,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
onPressed: () {
if (_todoUpdateFormKey.currentState!.validate()) {
}
},
child: const Text(
"更新",
style: TextStyle(color: Colors.white),
),
),
],
);
},
);
},
```
在新按鈕被按下時,我們需要把修改後的資料設定回目前的物件,並且呼叫`provider`來向後端傳送要更新的資料:
```dart
onPressed: () {
if (_todoUpdateFormKey.currentState!.validate()) {
setState(() {
provider.todoList[index].title = _todoTitleController.text;
provider.todoList[index].content = _todoContentController.text;
provider.updateTodo(provider.todoList[index], index);
});
_todoTitleController.clear();
_todoContentController.clear();
Navigator.pop(context);
}
},
```
要項後端傳送資料,我們需要先知道要求的資料長甚麼樣子:
* title
* content
* isChecked
所以可以來定義 class:
```dart
// todo_update_model.dart
class TodoUpdateModel {
String title;
String content;
bool isChecked;
TodoUpdateModel({
required this.title,
required this.content,
required this.isChecked,
});
factory TodoUpdateModel.fromJson(Map<String, dynamic> json) {
return TodoUpdateModel(
title: json['title'],
content: json['content'],
isChecked: json['is_checked']);
}
}
```
前幾篇有講過:
> 在Restful API設計中,新增物件後會回傳一個新增的設計資料,這筆回傳的資料就會有來自資料庫的主鍵...
> 可以回頭看看這篇喔!【[不專業教學系列 - Flutter To-Do List 第三集 連線到後端!](/1Yc5IwVdTx6E9B4NjZHvaA)】
定義好了資料格式就可以來嘗試更新資料到後端了!
```dart
// todo_service.dart
Future<TodoUpdateModel> updateTodo(TodoUpdateModel todo, int todoId) async {
final response = await http.put(
Uri.parse(Urls.updateTodo+todoId.toString()),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode({
"title": todo.title,
"content": todo.content,
"is_checked": todo.isChecked
}),
).timeout(const Duration(seconds: 5));
if(response.statusCode == 200) {
return TodoUpdateModel.fromJson(json.decode(utf8.decode(response.bodyBytes)));
} else {
throw Exception('Failed to update todo');
}
}
```
將拿到的資料寫回 `provider` 的 `_todoList`:
```dart
// todo_provider
void updateTodo(TodoData todo, int index) async {
TodoUpdateModel todoUpdateModel = TodoUpdateModel(title: todo.title, content: todo.content, isChecked: todo.isChecked);
TodoUpdateModel todoRes = await _todoService.updateTodo(todoUpdateModel, todo.id);
_todoList[index].title = todoRes.title;
_todoList[index].content = todoRes.content;
_todoList[index].isChecked = todoRes.isChecked;
notifyListeners();
}
```
到了這裡更新的功能就好了!
## 將 To-Do 的狀態更新到後端
現在按下更新按鈕後可以更新事項資料,但是勾選或是取消勾選的狀態在切換畫面後會被還原,為了解決這個問題,我們可以新增一個 Service 來處理更新狀態的部分:
```dart
Future<TodoUpdateCheckModel> updateTodoCheck(bool isChecked, int todoId) async {
final response = await http.put(
Uri.parse(Urls.updateTodoCheck+todoId.toString()),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode({
"is_checked": isChecked
}),
).timeout(const Duration(seconds: 5));
if(response.statusCode == 200) {
return TodoUpdateCheckModel.fromJson(json.decode(utf8.decode(response.bodyBytes)));
} else {
throw Exception('Failed to update todo check');
}
}
```
然後在 Provider 中新增更新狀態的事件:
```dart
// todo_provider
void updateTodoCheck(int index, bool isChecked) async {
TodoUpdateCheckModel todoUpdateCheckModel = await _todoService.updateTodoCheck(isChecked, _todoList[index].id);
_todoList[index].isChecked = todoUpdateCheckModel.isChecked;
notifyListeners();
}
```
最後在 CheckBox 被按下時調用更新狀態的事件就好:
```dart
// display_todo_screen.dart
onChanged: (bool? value) {
provider.updateTodoCheck(index, value!);
},
```
這樣就可以在更新狀態時寫入資料庫OUOb
## 從後端取得已完成的事項,並顯示在畫面上
流程基本上就是將已完成的事項`id`存進一個清單中,並且提交到後端讓後端根據`id`來處理資料的新增與刪除。
首先需要在 Service 新增提交資料與取得資料的方法,但這是關於已完成事項的服務,所以我們需要新增一個 Service 叫做 `TodoCompleteService`:
```dart
class TodoCompleteService {
Future<List<TodoCompleteModel>> getCompletes() async {
final response = await http.get(Uri.parse(Urls.getCompletes)).timeout(const Duration(seconds: 5));
if (response.statusCode == 200){
List<dynamic> todoList = json.decode(utf8.decode(response.bodyBytes));
return todoList.map((todo) => TodoCompleteModel.fromJson(todo)).toList();
} else {
throw Exception('資料讀取失敗!');
}
}
Future<List<TodoCompleteModel>> addCompletes(List<int> completeList) async {
final response = await http.post(
Uri.parse(Urls.addCompletes),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(completeList),
).timeout(const Duration(seconds: 5));
if(response.statusCode == 200) {
List<dynamic> completedList = json.decode(utf8.decode(response.bodyBytes));
return completedList.map((todoComplete) => TodoCompleteModel.fromJson(todoComplete)).toList();
} else {
throw Exception('Failed to add todo');
}
}
}
```
其中用到的 `Model`:
```dart
// todo_complete_model.dart
class TodoCompleteModel {
int id;
String title;
String content;
bool isChecked;
TodoCompleteModel({
required this.id,
required this.title,
required this.content,
required this.isChecked
});
factory TodoCompleteModel.fromJson(Map<String, dynamic> json) {
return TodoCompleteModel(
id: json['id'],
title: json['title'],
content: json['content'],
isChecked: json['is_checked']);
}
}
```
要使用新增的 Service,我們必須在 Provider 中註冊 Service,並新增提交完成事項與取得所有完成事項的方法:
```dart
// todo_provider.dart
import 'package:flutter/foundation.dart';
import 'package:todo_app/models/todo_Update_model.dart';
import 'package:todo_app/models/todo_complete_model.dart';
import 'package:todo_app/models/todo_data.dart';
import 'package:todo_app/models/todo_update_check_model.dart';
import 'package:todo_app/services/todo_complete_service.dart';
import 'package:todo_app/services/todo_service.dart';
class TodoProvider extends ChangeNotifier {
final TodoService _todoService;
final TodoCompleteService _completeService;
List<TodoData> _todoList = [];
List<TodoCompleteModel> _completedList = [];
List<TodoData> get todoList => _todoList;
List<TodoCompleteModel> get completedList => _completedList;
TodoProvider(this._todoService, this._completeService);
// 從API取得todo清單
Future<void> getTodos() async {
_todoList = await _todoService.getTodos();
notifyListeners();
}
// 將資料交給API
void addTodo(TodoData todo) async {
TodoData todoData = await _todoService.addTodo(todo);
_todoList.add(todoData);
notifyListeners();
}
// 讓API刪除資料
void removeTodoAt(TodoData todo) async {
_todoList.remove(todo);
await _todoService.deleteTodo(todo.id);
notifyListeners();
}
void updateTodo(TodoData todo, int index) async {
TodoUpdateModel todoUpdateModel = TodoUpdateModel(title: todo.title, content: todo.content, isChecked: todo.isChecked);
TodoUpdateModel todoRes = await _todoService.updateTodo(todoUpdateModel, todo.id);
_todoList[index].title = todoRes.title;
_todoList[index].content = todoRes.content;
_todoList[index].isChecked = todoRes.isChecked;
notifyListeners();
}
void updateTodoCheck(int index, bool isChecked) async {
TodoUpdateCheckModel todoUpdateCheckModel = await _todoService.updateTodoCheck(isChecked, _todoList[index].id);
_todoList[index].isChecked = todoUpdateCheckModel.isChecked;
notifyListeners();
}
// ==========================================
Future<void> getCompletes() async {
_completedList = await _completeService.getCompletes();
notifyListeners();
}
void clearCompleted() async {
List<int> completedIdList = [];
for (var item in _todoList) {
if (item.isChecked == true) {
completedIdList.add(item.id);
}
}
_completedList = await _completeService.addCompletes(completedIdList);
_todoList.removeWhere((item) => item.isChecked == true);
notifyListeners();
}
}
```
最後更新一下 `display_completed_screen.dart` 的內容就好:
```dart
class _DisplayCompletedState extends State<DisplayCompletedScreen> {
@override
void initState() {
super.initState();
_loadCompletes();
}
void _loadCompletes() {
final provider = Provider.of<TodoProvider>(context, listen: false);
provider.getCompletes();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _showCompleted(),
);
}
Widget _showCompleted() {
final provider = Provider.of<TodoProvider>(context);
return ListView.builder(
itemCount: provider.completedList.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: <Widget>[
ListTile(
title: Text(provider.completedList[index].title,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 20)),
subtitle: Text(provider.completedList[index].content,
style: const TextStyle(fontSize: 16)),
leading: Checkbox(
activeColor: Colors.blueGrey.shade600,
checkColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
value: provider.completedList[index].isChecked,
onChanged: (bool? value) {
},
),
enabled: false,
),
const Divider(),
],
);
},
);
}
}
```
## 結語
到了這裡,想必各位讀者老爺也都完成了 CRUD 的功能,並且自動更新畫面了,恭喜恭喜~~~