ใบงานฝึกปฏิบัติ สัปดาห์ที่ 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) แล้ว — ทดลองจริงแล้วเติมตาราง
- เพิ่มบันทึกในแท็บ "บันทึก" (SQLite) 1 อัน + แท็บ "ออนไลน์" (REST) 1 อัน
- ปิดเน็ต แล้วสลับดู 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 แทนรหัสนักศึกษา + เรียลไทม์)