ใบงานฝึกปฏิบัติ สัปดาห์ที่ 2: MyDiary — เพิ่มข้อมูลเองและเก็บถาวร
ก่อนทำ Lab: ต้องผ่านส่วนที่ 3 ในเอกสารสอน (แอปเพิ่ม/ลบบันทึกได้ + เก็บ SQLite ครบ 7 ขั้น) Lab นี้ = ต่อยอดของเดิม ให้ CRUD ครบและใช้งานจริงขึ้น
การส่ง: โปรเจกต์ + คลิปสั้น (เพิ่มบันทึก → ปิดแอป → เปิดใหม่ → ยังอยู่)
Lab 1 — เก็บ field ใหม่ (mood) ลง SQLite ให้ครบ
(สำหรับคนที่ทำ "ส่วน B ขยาย" มาแล้ว — ถ้ายังไม่ได้ทำ ข้ามไป Lab 2)
ปัญหาที่ต้องแก้: ในเอกสารสอน ส่วน B ขยาย เราเพิ่ม mood (อารมณ์) ในฟอร์มแล้ว แต่ โค้ด SQLite ในส่วน C ยังไม่รู้จัก mood — ถ้าไม่แก้ อารมณ์จะหายตอนบันทึกลง DB
สิ่งสำคัญ: เพิ่ม field ในฟอร์มอย่างเดียวไม่พอ — ต้องเพิ่มให้ครบ 3 ที่: Model, ตาราง DB, และการแปลงข้อมูล
ขั้นที่ 1 — เพิ่ม mood ใน Model (ถ้ายังไม่มี id ให้เพิ่มด้วย)
class Diary {
final int? id;
final String title;
final String content;
final String date;
final String mood; // << field ใหม่
Diary({this.id, required this.title, required this.content,
required this.date, required this.mood});
Map<String, dynamic> toMap() => {
'id': id, 'title': title, 'content': content,
'date': date, 'mood': mood, // << เพิ่มใน toMap
};
factory Diary.fromMap(Map<String, dynamic> map) => Diary(
id: map['id'], title: map['title'], content: map['content'],
date: map['date'], mood: map['mood'] ?? 'เฉยๆ', // << เพิ่มใน fromMap (?? กันค่าเก่าไม่มี mood)
);
}
ขั้นที่ 2 — เพิ่มคอลัมน์ mood ในตาราง (แก้ CREATE TABLE ใน database_helper.dart)
onCreate: (db, version) {
return db.execute(
'CREATE TABLE diaries('
'id INTEGER PRIMARY KEY AUTOINCREMENT, '
'title TEXT NOT NULL, '
'content TEXT NOT NULL, '
'date TEXT NOT NULL, '
'mood TEXT NOT NULL)', // << คอลัมน์ใหม่
);
},
ขั้นที่ 3 — สำคัญมาก! ถอนแอปออกก่อนรันใหม่
- ตาราง DB ถูกสร้างครั้งเดียว (onCreate) — เพิ่มคอลัมน์แล้วต้องถอนแอปเดิมออกให้ตารางสร้างใหม่
- วิธีถอน: กดค้างที่ไอคอนแอปใน emulator → Uninstall / หรือใน emulator ลบแอป แล้วรัน F5 ใหม่
ขั้นที่ 4 — แสดง mood ในรายการ (แก้ subtitle):
subtitle: Text('${diaries[i].mood} • ${diaries[i].date}',
maxLines: 1, overflow: TextOverflow.ellipsis),
ผลที่ควรได้: เพิ่มบันทึกพร้อมอารมณ์ → ปิดแอปเปิดใหม่ → อารมณ์ยังอยู่
จำหลักการนี้: เพิ่ม field ใหม่ = แก้ 3 ที่เสมอ (Model + CREATE TABLE + toMap/fromMap) + ถอนแอปรันใหม่
Lab 2 — แก้ไขบันทึก (Update ที่ยังขาด)
เป้าหมาย: ตอนนี้แอปเพิ่ม/ลบได้แล้ว แต่ยังแก้ไม่ได้ — เติมให้ครบ CRUD
ขั้นที่ 1 — เพิ่มคำสั่ง update ใน DatabaseHelper (ไฟล์ database_helper.dart):
static Future<void> update(Diary diary) async {
final db = await database;
await db.update(
'diaries',
diary.toMap(),
where: 'id = ?',
whereArgs: [diary.id],
);
}
ขั้นที่ 2 — ให้หน้าฟอร์มรับบันทึกเดิมมาแก้ได้ (แก้ AddDiaryPage)
class AddDiaryPage extends StatefulWidget {
final Diary? existing; // ถ้าส่งมา = โหมดแก้ไข / null = เพิ่มใหม่
const AddDiaryPage({super.key, this.existing});
@override
State<AddDiaryPage> createState() => _AddDiaryPageState();
}
ขั้นที่ 3 — เติมค่าเดิมลงช่องกรอกตอนเปิดหน้า (ใน _AddDiaryPageState)
@override
void initState() {
super.initState();
if (widget.existing != null) { // โหมดแก้ไข: เติมค่าเดิม
_titleController.text = widget.existing!.title;
_contentController.text = widget.existing!.content;
_selectedMood = widget.existing!.mood;
_selectedDate = DateTime.parse(widget.existing!.date);
}
}
ขั้นที่ 4 — ในหน้ารายการ ต้องแยกว่า "เพิ่ม" หรือ "แก้ไข" ในไฟล์ main.dart เพิ่มเมธอด _updateDiary ใน _DiaryListPageState (วางถัดจาก _addDiary):
// แก้ไข: เขียน DB แล้วโหลดใหม่
Future<void> _updateDiary(Diary diary) async {
await DatabaseHelper.update(diary);
_loadDiaries();
}
ขั้นที่ 5 — จากนั้นที่ ListTile ในหน้ารายการ ทำให้กด แก้ไข ได้ แนะนำให้เพิ่มปุ่มดินสอไว้ข้างปุ่มลบ โดยเปลี่ยน trailing จากปุ่มลบเดี่ยว ๆ เป็นสองปุ่ม:
trailing: Row(
mainAxisSize: MainAxisSize.min, // ให้ Row กว้างเท่าที่จำเป็น
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddDiaryPage(existing: diaries[i]), // ส่งตัวเดิมเข้าไป
),
);
if (result != null && result is Diary) {
await _updateDiary(result); // อัปเดตแทนการเพิ่ม
}
},
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => _deleteDiary(diaries[i].id!),
),
],
),
ผลที่ควรได้: แตะบันทึก → เปิดฟอร์มพร้อมค่าเดิม → แก้ → บันทึก → เห็นการเปลี่ยนแปลง (ปิดแอปเปิดใหม่ยังอยู่)
Lab 3 — ยืนยันก่อนลบ
เป้าหมาย: กันลบพลาด — ถามยืนยันก่อนลบ
แก้ปุ่มลบให้ถาม dialog ก่อน:
onPressed: () async {
final confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('ลบบันทึก?'),
content: Text('ต้องการลบ "${diaries[i].title}" ใช่ไหม'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('ยกเลิก')),
TextButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('ลบ', style: TextStyle(color: Colors.red)),
),
],
),
);
if (confirm == true) {
_deleteDiary(diaries[i].id!);
}
},
อธิบาย
- showDialog<bool> = แสดงกล่องโต้ตอบ คืนค่า true/false
- AlertDialog = กล่องยืนยันมาตรฐาน (หัวเรื่อง + เนื้อหา + ปุ่ม)
- กด "ลบ" คืน true → ค่อยลบจริง
ผลที่ควรได้: กดถังขยะ → กล่องถามยืนยัน → กดลบถึงจะหาย
Lab 4 — แสดงข้อความเมื่อยังไม่มีบันทึก
เป้าหมาย: ถ้ายังไม่มีบันทึก แสดงข้อความชวนเขียน (แทนจอว่างๆ)
แก้ body ในหน้ารายการ ให้เช็คก่อนว่าว่างไหม:
body: diaries.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.book_outlined, size: 80, color: Colors.grey),
SizedBox(height: 16),
Text('ยังไม่มีบันทึก', style: TextStyle(fontSize: 18, color: Colors.grey)),
Text('กดปุ่ม + เพื่อเขียนบันทึกแรก', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
// ... โค้ด ListView เดิม
),
อธิบาย:
- diaries.isEmpty ? A : B = ถ้าว่างแสดง A (ข้อความชวน) ไม่ว่างแสดง B (รายการ)
- เรียกว่า ternary — ตัวเลือก 2 ทางแบบสั้น
ผลที่ควรได้: ลบบันทึกจนหมด → เห็นข้อความ "ยังไม่มีบันทึก กดปุ่ม +"
Challenge — นับจำนวนบันทึกบน AppBar
สำหรับคนที่ทำ Lab 1-4 เสร็จ
โจทย์: แสดงจำนวนบันทึกบนหัว AppBar เช่น "บันทึกของฉัน (5)"
appBar: AppBar(title: Text('บันทึกของฉัน (${diaries.length})')),
ต่อยอด (ยากขึ้น): ทำปุ่มค้นหา — กรองบันทึกที่ชื่อเรื่องมีคำที่พิมพ์ (ใช้ where ของ List)
| เกณฑ์ | คะแนน |
|---|---|
| แสดงจำนวนบันทึกถูกต้อง (อัปเดตเมื่อเพิ่ม/ลบ) | 2 |
| ทำค้นหาได้ (ต่อยอด) | +2 |
เช็คลิสต์ก่อนส่ง
☐ Lab 1 — เก็บ mood ลง SQLite ครบ (ถ้าทำส่วน B ขยาย)
☐ Lab 2 — แก้ไขบันทึกได้ (CRUD ครบ)
☐ Lab 3 — ยืนยันก่อนลบ
☐ Lab 4 — ข้อความเมื่อว่าง
☐ Lab 5 — ดูรายละเอียดได้
☐ คลิป: เพิ่มบันทึก → ปิดแอป → เปิดใหม่ → ยังอยู่ (สำคัญ!)
หมายเหตุ: เก็บโปรเจกต์ไว้ — สัปดาห์หน้าต่อยอดให้ดึงข้อมูลจากอินเทอร์เน็ต (Async + API)