---
# System prepended metadata

title: 不專業教學系列 - Flutter To-Do List 第四集 To-Do 的編輯終於實現了!
tags: [Flutter, Flutter-TO-DO-List]

---

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