MyDiaryคอร์ส Flutter · 6 สัปดาห์ ทุกสัปดาห์
สัปดาห์ที่ 2 · ใบงาน · Lab
สารบัญหน้านี้

ใบงานฝึกปฏิบัติ สัปดาห์ที่ 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)