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

ใบงานฝึกปฏิบัติ สัปดาห์ที่ 4: MyDiary CRUD ผ่าน REST API

ก่อนทำ Lab: ต้องผ่านส่วนที่ 2-3 ในเอกสารสอน (POST/GET/PUT/DELETE ครบ + เชื่อมหน้าจอ) API ที่ใช้: server ของห้องเรา https://thipwimon.comsciproject.net/mydiary/api.php/<รหัสนักศึกษา> — ใส่ รหัสตัวเองท้าย URL เพื่อให้ข้อมูลแยกกล่องกัน (เห็นเฉพาะของตัวเอง) รันได้ทั้ง emulator และ Chrome (server เปิด CORS ให้แล้ว) — ถ้า server เป็น http ต้องเพิ่ม android:usesCleartextTraffic="true" ใน AndroidManifest

การส่ง: โปรเจกต์ + คลิปสั้น (เพิ่ม→แก้→ลบ บน server + ปิดแอปเปิดใหม่ข้อมูลยังอยู่)


Lab 1 — สร้าง ApiService ให้ครบ CRUD

เป้าหมาย: มี ApiService ที่ทำ POST/GET/PUT/DELETE ได้ครบ

ทำตามเอกสารสอนขั้น 1-4 ให้ครบ — สร้างไฟล์ api_service.dart ที่มี 4 เมธอด:

class ApiService {
  static const String studentId = '66xxxxxx';   // << ใส่รหัสของคุณ
  static const String baseUrl = 'https://thipwimon.comsciproject.net/mydiary/api.php/$studentId';  // กล่องของเราโดยเฉพาะ
  // createDiary (POST), getDiaries (GET), updateDiary (PUT), deleteDiary (DELETE)
}

เช็คลิสต์ว่าครบ:

เมธอด HTTP มีแล้ว?
createDiary POST ☐
getDiaries GET (คืนทุกอัน) ☐
updateDiary PUT ☐
deleteDiary DELETE ☐

ผลที่ควรได้: api_service.dart ครบ 4 เมธอด ไม่มี error แดง


Lab 1.5 — เพิ่มหน้า CRUD API เป็นแท็บใหม่

สร้างแท็บแยก ไม่แตะ SQLite เดิม

เป้าหมาย: เพิ่มแท็บ "ออนไลน์" ในแอป — ทำงานแยกจากแท็บ SQLite เดิมสิ้นเชิง ขั้นที่ 1 — สร้าง class ApiDiaryPageใหม่ (ตามเอกสารสอนขั้น 5) ห้ามแก้ DiaryListPage เดิม

ขั้นที่ 2 — เพิ่มเข้า BottomNavigationBar ที่มีอยู่ (แก้ 2 จุดใน _MainPageState):

// จุดที่ 1: เพิ่ม ApiDiaryPage เข้า _pages
final List<Widget> _pages = [
  const DiaryListPage(),      // SQLite เดิม 
  const ApiDiaryPage(),       // แท็บใหม่
  const AboutPage(),
];

// จุดที่ 2: เพิ่มปุ่มแท็บ
items: const [
  BottomNavigationBarItem(icon: Icon(Icons.book), label: 'บันทึก'),
  BottomNavigationBarItem(icon: Icon(Icons.cloud), label: 'ออนไลน์'),   // เพิ่ม
  BottomNavigationBarItem(icon: Icon(Icons.info), label: 'เกี่ยวกับ'),
],

ผลที่ควรได้: แอปมี 2 แท็บเก็บข้อมูล (SQLite เดิม + REST ใหม่) ทำงานแยกกัน ของเดิมไม่พัง


Lab 2 — เพิ่มบันทึกขึ้น server แล้วพิสูจน์ว่าไม่หาย

เป้าหมาย: POST บันทึก แล้วพิสูจน์ว่าบันทึกจริง ขั้นที่ 1 — เพิ่มบันทึกผ่านแอป (ปุ่ม +) → ใส่ข้อมูล → บันทึก ขั้นที่ 2 — พิสูจน์ว่าบันทึกจริง: - เปิดเบราว์เซอร์ไปที่กล่องของคุณ (https://thipwimon.comsciproject.net/mydiary/api.php/<รหัสของคุณ>) → เห็นบันทึกที่เพิ่งเพิ่มเป็น JSON จริงบน server! (ลองเปลี่ยนเป็นรหัสเพื่อน จะเห็นคนละชุด = แยกกล่องกันจริง) - หรือ: ปิดแอป → เปิดใหม่ → บันทึกยังอยู่ (โหลดจาก server) ผลที่ควรได้: เพิ่มบันทึก → เห็นบน server จริง → ปิดเปิดแอปยังอยู่


Lab 3 — เพิ่ม/แก้ไข/ลบ ผ่านฟอร์ม

เป้าหมาย: ทำ Create/Update/Delete ครบ ผ่านฟอร์ม ApiFormPage ส่วน A — เพิ่มใหม่ (Create): ปุ่ม + (FAB) เปิด ApiFormPage โหมดเพิ่ม → รับ Map กลับ → createDiary

floatingActionButton: FloatingActionButton(
  onPressed: () async {
    final result = await Navigator.push(
      context, MaterialPageRoute(builder: (_) => const ApiFormPage()));
    if (result is Map) {
      await _addDiary(result['title'], result['content'], result['mood'], result['date']);
    }
  },
  child: const Icon(Icons.add),
),

ส่วน B — ปุ่มแก้ไข + ลบ (รวมไว้ใน trailing: Row เดียวกัน): ปุ่มดินสอเปิด ApiFormPage โหมดแก้ (Update) · ปุ่มถังขยะถามยืนยันก่อนลบ (Delete)

วางทั้งก้อนนี้ที่ trailing ของ ListTile:

trailing: Row(
  mainAxisSize: MainAxisSize.min,        // ให้ Row กว้างเท่าที่จำเป็น
  children: [
    // ── แก้ไข (Update) ──
    IconButton(
      icon: const Icon(Icons.edit, color: Colors.blue),
      onPressed: () async {
        final result = await Navigator.push(context,
          MaterialPageRoute(builder: (_) => ApiFormPage(existing: obj))); // ส่ง object เดิม
        if (result is Map) {
          await ApiService.updateDiary(
            obj['id'], result['title'], result['content'], result['mood'], result['date']);
          _loadDiaries();
        }
      },
    ),
    // ── ลบ (Delete) + ยืนยันก่อนลบ ──
    IconButton(
      icon: const Icon(Icons.delete, color: Colors.red),
      onPressed: () async {
        final confirm = await showDialog<bool>(
          context: context,
          builder: (ctx) => AlertDialog(
            title: const Text('ลบบันทึก?'),
            content: Text('ต้องการลบ "${obj['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) {
          await ApiService.deleteDiary(obj['id']);
          _loadDiaries();       // โหลดใหม่หลังลบ
        }
      },
    ),
  ],
),

อธิบาย: - Row(mainAxisSize: MainAxisSize.min, children: [...]) = วางปุ่มแก้กับลบเรียงกันแนวนอน กว้างพอดีสองปุ่ม - showDialog<bool> = เด้งกล่องถามยืนยัน คืน true เมื่อกด "ลบ" / false หรือ null เมื่อยกเลิก — ค่อยลบจริงเมื่อ true - ใช้ obj['id'] ทั้งแก้และลบให้สม่ำเสมอ (server ของห้องคืนทั้ง id และ _id ใช้ตัวไหนก็ได้)

ทดสอบครบ CRUD (ในแท็บ "ออนไลน์"):

การกระทำ ผลที่ควรเห็น ✓
กด + กรอกฟอร์ม (POST) มีบันทึกใหม่ ☐
กดดินสอ แก้ (PUT) บันทึกเปลี่ยน ☐
กดถังขยะ (DELETE) บันทึกหาย ☐

ผลที่ควรได้: เพิ่ม/แก้/ลบ ครบ ผ่านฟอร์มจริง ทำงานบน server


Lab 4 — จัดการ error ตอนเน็ตหลุด

เป้าหมาย: แอปไม่ crash เมื่อ server มีปัญหา เพิ่ม try-catch ในการเรียก API + แสดง SnackBar เมื่อผิดพลาด:

Future<void> _addDiary(String title, String content, String mood, String date) async {
  try {
    await ApiService.createDiary(title, content, mood, date);
    _loadDiaries();
  } catch (e) {
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('เพิ่มไม่สำเร็จ ตรวจสอบอินเทอร์เน็ต')),
      );
    }
  }
}

ทดสอบ: ปิดเน็ต → กดเพิ่ม → ควรเห็นข้อความเตือน (ไม่ crash)

โจทย์: ตอนนี้แอปมี 2 แท็บ (SQLite + REST) แล้ว — ทดลองจริงแล้วเติมตาราง

  1. เพิ่มบันทึกในแท็บ "บันทึก" (SQLite) 1 อัน + แท็บ "ออนไลน์" (REST) 1 อัน
  2. ปิดเน็ต แล้วสลับดู 2 แท็บ — สังเกตว่าแท็บไหนยังใช้ได้

เช็คลิสต์ก่อนส่ง

Lab 1 ApiService ครบ 4 เมธอด (CRUD)
Lab 1.5 เพิ่มแท็บ "ออนไลน์" (แยกจาก SQLite เดิม)
Lab 2 เพิ่มบันทึกบน server ได้
Lab 3 แก้/ลบ(ยืนยันการลบ) บน server ได้ (CRUD ครบ)
Lab 4 จัดการ error เน็ตหลุด (ไม่ crash)
คลิป: เพิ่ม→แก้→ลบ บน server + ปิดเปิดข้อมูลยังอยู่ แอป build ผ่าน
หมายเหตุ: เก็บโปรเจกต์ไว้ — สัปดาห์หน้าเพิ่มระบบล็อกอิน + ย้ายขึ้น Firebase (userId แทนรหัสนักศึกษา + เรียลไทม์)