# 不專業教學系列 - 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 的功能,並且自動更新畫面了,恭喜恭喜~~~