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

สัปดาห์ที่ 2: เพิ่มข้อมูลเองและเก็บถาวร (State + Form + SQLite)

เป้าหมายสัปดาห์นี้: ทำให้ผู้ใช้ เขียนบันทึกเอง เพิ่ม/ลบ/แก้ได้ และ เก็บถาวร (ปิดแอปเปิดใหม่ข้อมูลยังอยู่)

ต่อจากสัปดาห์ที่แล้ว: แอปเราสวยแล้ว แต่ข้อมูลถูกพิมพ์ไว้ในโค้ด ผู้ใช้เพิ่มเองไม่ได้ — วันนี้แก้ให้ผู้ใช้เพิ่มบันทึกเองได้จริง

ก่อนเริ่ม: สัปดาห์นี้เราจะทำอะไร

เราจะอัปเกรด MyDiary ให้
- มี ฟอร์มเขียนบันทึก (กรอกชื่อเรื่อง + เนื้อหา)
- เพิ่ม บันทึกลงรายการได้ (กดปุ่ม + ที่เตรียมไว้สัปดาห์ที่แล้ว)
- ลบ / แก้ บันทึกได้
- เก็บลงฐานข้อมูล SQLite → ปิดแอปเปิดใหม่ข้อมูลยังอยู่

เส้นทางของสัปดาห์นี้แบ่ง 3 ส่วน:

ส่วน A: State           → เข้าใจการเปลี่ยนหน้าจอ (setState)
ส่วน B: Form            → รับข้อมูลจากผู้ใช้ (ฟอร์ม + ตรวจสอบ, Dropdown, DatePicker, Checkbox, Radio)
ส่วน C: SQLite          → เก็บข้อมูลถาวร (ไม่หายตอนปิดแอป)

ส่วนที่ 1: ทำความเข้าใจ "อะไรเป็นอะไร"

1.1 State (สถานะ) คืออะไร

State = ข้อมูลที่เปลี่ยนได้ และเมื่อเปลี่ยนแล้วหน้าจอต้องอัปเดตตาม

สัปดาห์ที่แล้วข้อมูล "นิ่ง" (พิมพ์ในโค้ด) — สัปดาห์นี้ข้อมูล "มีชีวิต" (ผู้ใช้เพิ่ม/ลบได้) พอข้อมูลเปลี่ยน หน้าจอต้องวาดใหม่

ตัวอย่าง: รายการบันทึกมี 5 อัน → ผู้ใช้เพิ่มอันที่ 6 → หน้าจอต้องแสดง 6 อันทันที

1.2 setState — คำสั่งสำคัญที่สุดของสัปดาห์นี้

setState = "ฉันเปลี่ยนข้อมูลแล้ว ช่วยวาดหน้าจอใหม่ด้วย"

setState(() {
  diaries.add(newDiary);     // เปลี่ยนข้อมูล
});                          // Flutter วาดจอใหม่ให้

หัวใจ: เปลี่ยนข้อมูลเฉยๆ หน้าจอไม่อัปเดต — ต้องเปลี่ยนใน setState เท่านั้น หน้าจอถึงจะวาดใหม่

1.3 StatefulWidget vs StatelessWidget (ทบทวน + ต่อยอด)

Stateless (สัปดาห์ 1) Stateful (สัปดาห์นี้)
ข้อมูล นิ่ง ไม่เปลี่ยน เปลี่ยนได้
ใช้ setState ไม่ได้ ได้
ตัวอย่าง หน้าเกี่ยวกับ หน้ารายการที่เพิ่ม/ลบได้

สัปดาห์นี้หน้ารายการต้องเปลี่ยนเป็น StatefulWidget เพราะข้อมูลเปลี่ยนได้แล้ว

1.4 Form (ฟอร์ม) — รับข้อมูลจากผู้ใช้

ส่วนประกอบของฟอร์ม

Widgetหน้าที่
Formกล่องครอบช่องกรอกทั้งหมด
TextFormFieldช่องกรอกที่ตรวจสอบข้อมูลได้
TextEditingControllerตัวอ่านค่าที่ผู้ใช้พิมพ์
validatorกติกาตรวจสอบ (เช่น ห้ามว่าง)

1.5 SQLite — ฐานข้อมูลในเครื่อง

ปัญหา: ข้อมูลใน List อยู่ในหน่วยความจำ (RAM) — ปิดแอป = หายหมด
ทางแก้: SQLite = ฐานข้อมูลที่เก็บลงไฟล์ในเครื่อง ปิดแอปข้อมูลยังอยู่

คำศัพท์ SQLite คือ เปรียบเทียบ
Database ฐานข้อมูล 1 ไฟล์ สมุด 1 เล่ม
Table ตารางเก็บข้อมูล หน้าในสมุด
Row 1 แถว (1 บันทึก) 1 บรรทัด
Column 1 ช่อง (title, content) ช่องในตาราง

ส่วนที่ 2: เตรียมเครื่องมือ

2.1 เปิดโปรเจกต์เดิม

เปิดโปรเจกต์ mydiary จากสัปดาห์ที่แล้ว (ไม่สร้างใหม่)

2.2 ติดตั้ง package SQLite

เปิด Terminal ใน VS Code (Ctrl+~) พิมพ์:

flutter pub add sqflite path
  • sqflite = ตัวจัดการฐานข้อมูล SQLite
  • path = ตัวช่วยหาที่อยู่ไฟล์ฐานข้อมูล

รอจนขึ้น Changed X dependencies! = ติดตั้งเสร็จ


ส่วนที่ 3: ลงมือทำทีละขั้น

ทำตามทีละขั้น รันดูผลทุกขั้น — เราจะทำ State ก่อน (เพิ่มในหน่วยความจำ) แล้วค่อยเปลี่ยนเป็น SQLite (ถาวร)

ส่วน A: State — เพิ่มบันทึกได้ (ยังไม่ถาวร)

ขั้นที่ 1 — เปลี่ยนหน้ารายการเป็น StatefulWidget

เป้าหมาย: ให้หน้ารายการเปลี่ยนข้อมูลได้

แนวคิด: สัปดาห์ที่แล้ว DiaryListPage เป็น Stateless (นิ่ง) — ตอนนี้ข้อมูลต้องเพิ่มได้ ต้องเปลี่ยนเป็น Stateful

แก้ DiaryListPage จาก Stateless เป็น Stateful
เพิ่ม Class _DiaryListPageState แล้วย้าย List diaries ย้ายออกมานอก build

class DiaryListPage extends StatefulWidget {
  const DiaryListPage({super.key});
  @override
  State<DiaryListPage> createState() => _DiaryListPageState();
}

class _DiaryListPageState extends State<DiaryListPage> {
  // ย้าย List ข้อมูลมาไว้ตรงนี้ (นอก build)
  final List<Diary> diaries = [
    Diary(title: 'วันแรกของการเรียน Flutter', content: 'เริ่มเขียนแอปตัวแรก', date: '1 มิ.ย. 2568'),
    Diary(title: 'เข้าใจ Widget แล้ว', content: 'ทุกอย่างคือ Widget', date: '3 มิ.ย. 2568'),
  ];

  @override
  Widget build(BuildContext context) {
    // ... โค้ด build เหมือนเดิม (ListView + FloatingActionButton)
  }
}

อธิบายความต่าง: - Stateful แยกเป็น 2 class: ตัว Widget + ตัว State (_DiaryListPageState) - List diaries ย้ายออกมานอก build — เพราะต้องอยู่ยาว ไม่ถูกสร้างใหม่ทุกครั้งที่วาดจอ

เช็คพอยต์ 1: แอปยังรันได้เหมือนเดิม (ยังไม่เห็นความต่าง แต่โครงพร้อมแล้ว)


ขั้นที่ 2 — สร้างหน้าฟอร์มเขียนบันทึก

เป้าหมาย: หน้าใหม่ที่มีช่องกรอกชื่อเรื่อง + เนื้อหา

สร้าง class ใหม่ (ท้ายไฟล์):

class AddDiaryPage extends StatefulWidget {
  const AddDiaryPage({super.key});
  @override
  State<AddDiaryPage> createState() => _AddDiaryPageState();
}

class _AddDiaryPageState extends State<AddDiaryPage> {
  final _formKey = GlobalKey<FormState>();          // กุญแจคุมฟอร์ม
  final _titleController = TextEditingController();  // อ่านค่าช่องชื่อเรื่อง
  final _contentController = TextEditingController();// อ่านค่าช่องเนื้อหา

  @override
  void dispose() {
    _titleController.dispose();       // คืนทรัพยากรตอนปิดหน้า
    _contentController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('เขียนบันทึกใหม่')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: _titleController,
                decoration: const InputDecoration(
                  labelText: 'ชื่อเรื่อง',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'กรุณากรอกชื่อเรื่อง';
                  }
                  return null;        // null = ผ่าน
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: _contentController,
                maxLines: 5,          // ช่องเนื้อหาสูง 5 บรรทัด
                decoration: const InputDecoration(
                  labelText: 'เนื้อหา',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'กรุณากรอกเนื้อหา';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: () {
                    // ขั้นถัดไปจะเติมโค้ดบันทึกตรงนี้
                  },
                  child: const Text('บันทึก'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

ส่วนสำคัญ:
- _formKey = กุญแจไว้สั่งตรวจสอบฟอร์มทั้งหมดพร้อมกัน
- TextEditingController = ตัวอ่านค่าที่พิมพ์ (ต้อง dispose ตอนปิดหน้า)
- validator = กติกา คืน String = ผิด (โชว์ข้อความแดง) / คืน null = ผ่าน

เช็คพอยต์ 2: (ยังเปิดหน้านี้ไม่ได้ — ต่อในขั้น 3)


ขั้นที่ 3 — เปิดหน้าฟอร์มจากปุ่ม + (Navigation ส่งค่ากลับ)

เป้าหมาย: กดปุ่ม + → เปิดฟอร์ม → กรอกเสร็จส่งบันทึกกลับมา

แนวคิด: สัปดาห์ที่แล้วเรา push ไปหน้าใหม่ — คราวนี้ push แล้ว รับค่ากลับ ด้วย (ใช้ await)

ขั้น 3.1 — แก้ปุ่มบันทึกในฟอร์ม ให้ส่งข้อมูลกลับ:

onPressed: () {
  if (_formKey.currentState!.validate()) {     // ตรวจฟอร์มก่อน
    final newDiary = Diary(
      id: widget.existing?.id,
      title: _titleController.text.trim(),
      content: _contentController.text.trim(),
      date: DateTime.now().toString().substring(0, 10),  // วันที่วันนี้
    );
    Navigator.pop(context, newDiary);           // ส่งบันทึกกลับหน้ารายการ
  }
},

ขั้น 3.2 — แก้ปุ่ม + ในหน้ารายการ ให้เปิดฟอร์มแล้วรับค่า:

floatingActionButton: FloatingActionButton(
  onPressed: () async {                         // async เพราะต้องรอค่ากลับ
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => const AddDiaryPage()),
    );
    if (result != null && result is Diary) {    // ถ้าได้บันทึกกลับมา
      setState(() {
        diaries.add(result);                    // เพิ่มลงรายการ + วาดจอใหม่
      });
    }
  },
  child: const Icon(Icons.add),
),

อธิบาย:
- Navigator.pop(context, newDiary) = ปิดหน้าฟอร์ม พร้อมส่งค่ากลับ
- await Navigator.push(...) = เปิดหน้าแล้ว รอ จนหน้าถูกปิด รับค่าที่ส่งกลับ
- setState(() => diaries.add(result)) = เพิ่มบันทึก + สั่งวาดจอใหม่ → รายการอัปเดตทันที

กด F5 → ควรเห็น: กดปุ่ม + → กรอกฟอร์ม → กดบันทึก → กลับมาเห็นบันทึกใหม่ในรายการ!

เช็คพอยต์ 3: เพิ่มบันทึกได้แล้ว! (แต่ปิดแอปยังหาย — แก้ในส่วน C)


ขั้นที่ 4 — ลบบันทึก

เป้าหมาย: ปัดหรือกดลบบันทึกได้

แก้ ListTile ในรายการ เพิ่มปุ่มลบ:

trailing: IconButton(
  icon: const Icon(Icons.delete, color: Colors.red),
  onPressed: () {
    setState(() {
      diaries.removeAt(i);        // ลบตัวที่ตำแหน่ง i + วาดจอใหม่
    });
  },
),

กด F5 → ควรเห็น: กดถังขยะ → บันทึกหายจากรายการ

เช็คพอยต์ 4: เพิ่มและลบได้แล้ว — แต่ทั้งหมดยังอยู่ใน RAM ปิดแอปหาย → ส่วน C แก้


ส่วน B ขยาย: ฟอร์มที่มีมากกว่าช่องข้อความ

แนวคิดสำคัญก่อนเริ่ม: Widget เลือกค่า ≠ TextFormField

TextFormField (ช่องพิมพ์) Dropdown/Checkbox/Radio (เลือกค่า)
เก็บค่าด้วย TextEditingController ตัวแปร state ธรรมดา
อัปเดตค่า อัตโนมัติเมื่อพิมพ์ ต้อง setState เอง
ตรวจสอบ ใช้ validator เช็คเองด้วย if (ไม่ผ่าน validator ของ Form)

จุดที่นักศึกษางงบ่อย: Checkbox/Dropdown ไม่ได้ใช้ controller และ ไม่เข้า _formKey.validate() เหมือน TextFormField — ต้องจัดการ state เองด้วย setState และเช็คค่าเองก่อนบันทึก


ขั้นที่ 4.1 — เพิ่ม field ใหม่ใน Model

เป้าหมาย: เตรียม Model รับข้อมูลใหม่ (อารมณ์ + วันที่ที่เลือกเอง)

แก้ Model Diary เพิ่ม field mood:

class Diary {
  final String title;
  final String content;
  final String date;
  final String mood;         // << เพิ่ม: อารมณ์ (เก็บเป็นข้อความ เช่น 'มีความสุข')
  Diary({required this.title, required this.content, required this.date, required this.mood});
}

date เดิมมีอยู่แล้ว แต่สัปดาห์นี้จะให้ผู้ใช้ เลือกวันที่เอง แทนใช้วันปัจจุบันอัตโนมัติ


ขั้นที่ 4.2 — Dropdown: เลือกอารมณ์

เป้าหมาย: เพิ่มเมนูเลือกอารมณ์ในฟอร์ม

ขั้น 1 — ประกาศตัวแปร state เก็บค่าที่เลือก (ใน _AddDiaryPageState):

String _selectedMood = 'มีความสุข';    // ค่าเริ่มต้น
final List<String> _moods = ['มีความสุข', 'เฉยๆ', 'เศร้า', 'เหนื่อย', 'ตื่นเต้น'];

ขั้น 2 — เพิ่ม Dropdown ในฟอร์ม (ใต้ช่องเนื้อหา):

DropdownButtonFormField<String>(
  value: _selectedMood,
  decoration: const InputDecoration(
    labelText: 'อารมณ์วันนี้',
    border: OutlineInputBorder(),
    prefixIcon: Icon(Icons.mood),
  ),
  items: _moods.map((mood) {
    return DropdownMenuItem(value: mood, child: Text(mood));
  }).toList(),
  onChanged: (value) {
    setState(() {              //  เลือกค่าเลือกต้อง setState เอง
      _selectedMood = value!;
    });
  },
),

อธิบาย: - DropdownButtonFormField = Dropdown เวอร์ชันฟอร์ม (มีกรอบเข้าชุดกับ TextFormField)>
- value: _selectedMood = ค่าที่เลือกอยู่ตอนนี้
- items = แปลงรายการอารมณ์เป็นตัวเลือก (ใช้ .map เหมือน ListView)
- onChanged + setState = เมื่อเลือก อัปเดตค่า + วาดจอใหม่

ผลที่ควรได้: เห็นเมนูเลือกอารมณ์ แตะแล้วเลือกได้


ขั้นที่ 4.3 — Date Picker: เลือกวันที่เอง

เป้าหมาย: ให้ผู้ใช้เลือกวันที่ (แทนใช้วันปัจจุบันอัตโนมัติ)

ขั้น 1 — ประกาศตัวแปรเก็บวันที่:

DateTime _selectedDate = DateTime.now();    // เริ่มที่วันนี้

ขั้น 2 — เพิ่มปุ่มเลือกวันที่ในฟอร์ม:

ListTile(
  leading: const Icon(Icons.calendar_today),
  title: Text('วันที่: ${_selectedDate.toString().substring(0, 10)}'),
  trailing: const Icon(Icons.edit),
  onTap: () async {
    final picked = await showDatePicker(       // เปิดปฏิทินให้เลือก
      context: context,
      initialDate: _selectedDate,
      firstDate: DateTime(2020),               // เลือกได้ตั้งแต่ปี 2020
      lastDate: DateTime(2030),                // ถึงปี 2030
    );
    if (picked != null) {                      // ถ้าเลือก (ไม่กดยกเลิก)
      setState(() {
        _selectedDate = picked;
      });
    }
  },
),

อธิบาย:
- showDatePicker = เปิดปฏิทินมาตรฐานให้เลือกวันที่ (Flutter มีให้ ไม่ต้องทำเอง)
- คืน DateTime? — เป็น null ถ้าผู้ใช้กดยกเลิก (ต้องเช็ค)
- firstDate / lastDate = ขอบเขตวันที่ที่เลือกได้

ผลที่ควรได้: แตะแถววันที่ → ปฏิทินเด้ง → เลือกวันแล้วแสดงวันที่ใหม่


ขั้นที่ 4.4 — บันทึกค่าจากทุก Widget

เป้าหมาย: ตอนกดบันทึก เก็บค่าจากทุกช่อง (รวม Dropdown + วันที่)

แก้ปุ่มบันทึกให้รวมค่าใหม่:

onPressed: () {
  if (_formKey.currentState!.validate()) {     // validate เช็คแค่ TextFormField
    final newDiary = Diary(
      title: _titleController.text.trim(),
      content: _contentController.text.trim(),
      date: _selectedDate.toString().substring(0, 10),  // วันที่ที่เลือก
      mood: _selectedMood,                              // อารมณ์ที่เลือก
    );
    Navigator.pop(context, newDiary);
  }
},

จุดสำคัญ: สังเกตว่า validate() ตรวจแค่ TextFormField (ชื่อเรื่อง/เนื้อหา)
— Dropdown กับวันที่ไม่ต้อง validate เพราะมีค่าเริ่มต้นเสมอ (เลือกอยู่แล้ว ไม่มีทางว่าง) นี่คือความต่างของ "ช่องพิมพ์" (อาจว่าง ต้องเช็ค) กับ "ช่องเลือก" (มีค่าเสมอ)

แสดง mood ในรายการ/รายละเอียด: เพิ่มการแสดงอารมณ์ เช่นใน subtitle:

subtitle: Text('${diaries[i].mood} • ${diaries[i].date}'),

ผลที่ควรได้: เพิ่มบันทึกที่มีอารมณ์ + วันที่ที่เลือกเอง แสดงครบในรายการ


Widget ฟอร์มอื่นที่ควรรู้จัก (เลือกใช้ตามงาน)

นอกจาก Dropdown/DatePicker ยังมีอีก 3 ตัวที่ใช้บ่อย — ทุกตัวใช้แพตเทิร์นเดียวกัน: ตัวแปร state + setState

Checkbox — ติ๊กถูก (เช่น "บันทึกสำคัญ"):

bool _isImportant = false;

CheckboxListTile(
  title: const Text('ทำเครื่องหมายว่าสำคัญ'),
  value: _isImportant,
  onChanged: (value) => setState(() => _isImportant = value!),
),

Switch — สวิตช์เปิด-ปิด (เช่น "ปักหมุด"):

bool _isPinned = false;

SwitchListTile(
  title: const Text('ปักหมุดบันทึก'),
  value: _isPinned,
  onChanged: (value) => setState(() => _isPinned = value),
),

Radio — เลือก 1 จากไม่กี่ตัวเลือก (เช่น ประเภทบันทึก):

String _type = 'ส่วนตัว';

Column(children: [
  RadioListTile<String>(
    title: const Text('ส่วนตัว'),
    value: 'ส่วนตัว', groupValue: _type,
    onChanged: (v) => setState(() => _type = v!),
  ),
  RadioListTile<String>(
    title: const Text('งาน'),
    value: 'งาน', groupValue: _type,
    onChanged: (v) => setState(() => _type = v!),
  ),
]),

เลือกใช้ตัวไหนดี: เลือก 1 จากหลายตัว + ประหยัดพื้นที่ → Dropdown / เลือก 1 จาก 2-3 ตัวเห็นหมด → Radio / เปิด-ปิด อย่างเดียว → Switch/Checkbox / เลือกวันเวลา → DatePicker

ดูภาพหน้าตาจริงของทุกตัวได้ใน แคตตาล็อกภาพ Widget (ไฟล์ HTML)


ส่วน C: SQLite — เก็บถาวร

หมายเหตุ: โค้ด SQLite ด้านล่างใช้ Model เดิม (title/content/date) เพื่อความกระชับ — ถ้าทำส่วน B ขยายแล้ว อย่าลืมเพิ่ม mood ใน toMap/fromMap และ CREATE TABLE ด้วย (ดูวิธีในใบงาน Lab)

ขั้นที่ 5 — เพิ่ม id ใน Model + เตรียมแปลงข้อมูล

เป้าหมาย: เตรียม Model ให้เก็บลงฐานข้อมูลได้

แนวคิด: ฐานข้อมูลต้องการ "id" ประจำตัวแต่ละบันทึก + ต้องแปลงบันทึกเป็น Map (รูปแบบที่ SQLite เก็บได้)

แก้ Model Diary:

class Diary {
  final int? id;              // id? เพราะก่อนบันทึกยังไม่มี (DB ออกให้)
  final String title;
  final String content;
  final String date;

  Diary({this.id, required this.title, required this.content, required this.date});

  // แปลง object → Map (สำหรับบันทึกลง DB)
  Map<String, dynamic> toMap() {
    return {'id': id, 'title': title, 'content': content, 'date': date};
  }

  // แปลง Map (จาก DB) → object
  factory Diary.fromMap(Map<String, dynamic> map) {
    return Diary(
      id: map['id'],
      title: map['title'],
      content: map['content'],
      date: map['date'],
    );
  }
}

อธิบาย: - int? id = id เป็นตัวเลข แต่มี ? เพราะก่อนบันทึกยังไม่มี (ฐานข้อมูลจะออกเลขให้)
- toMap() = แปลงบันทึกเป็น Map เพื่อเก็บลง DB
- fromMap() = แปลง Map จาก DB กลับเป็นบันทึก


ขั้นที่ 6 — สร้างตัวจัดการฐานข้อมูล (DatabaseHelper)

เป้าหมาย: สร้างไฟล์จัดการ SQLite แยกต่างหาก

สร้างไฟล์ใหม่ lib/database_helper.dart:

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
import 'main.dart';       // เพื่อใช้ class Diary

class DatabaseHelper {
  static Database? _db;

  // เปิดฐานข้อมูล (เปิดครั้งเดียว ใช้ซ้ำ)
  static Future<Database> get database async {
    if (_db != null) return _db!;
    _db = await openDatabase(
      join(await getDatabasesPath(), 'mydiary.db'),   // ที่อยู่ไฟล์ DB
      version: 1,
      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)',
        );
      },
    );
    return _db!;
  }

  // เพิ่มบันทึก
  static Future<void> insert(Diary diary) async {
    final db = await database;
    await db.insert('diaries', diary.toMap());
  }

  // อ่านบันทึกทั้งหมด
  static Future<List<Diary>> getAll() async {
    final db = await database;
    final maps = await db.query('diaries', orderBy: 'id DESC');
    return maps.map((map) => Diary.fromMap(map)).toList();
  }

  // ลบบันทึก
  static Future<void> delete(int id) async {
    final db = await database;
    await db.delete('diaries', where: 'id = ?', whereArgs: [id]);
  }
}

อธิบาย: - CREATE TABLE = สร้างตารางเก็บบันทึก (id, title, content, date)
- id INTEGER PRIMARY KEY AUTOINCREMENT = ฐานข้อมูลออกเลข id ให้เองอัตโนมัติ
- insert/getAll/delete = คำสั่งเพิ่ม/อ่าน/ลบ
- where: 'id = ?', whereArgs: [id] = ลบเฉพาะแถวที่ id ตรง (? กันข้อมูลอันตราย)


ขั้นที่ 7 — เชื่อมฐานข้อมูลเข้าหน้ารายการ

เป้าหมาย: เปลี่ยนจากเก็บใน List (RAM) เป็นเก็บใน SQLite (ถาวร)

แนวคิด — แพตเทิร์นทอง: ทุกครั้งที่เปลี่ยนข้อมูล = เขียน DB → โหลดใหม่

แก้ _DiaryListPageState:

class _DiaryListPageState extends State<DiaryListPage> {
  List<Diary> diaries = [];         // เริ่มจากว่าง (จะโหลดจาก DB)

  @override
  void initState() {
    super.initState();
    _loadDiaries();                 // โหลดข้อมูลตอนเปิดหน้า
  }

  // โหลดจาก DB
  Future<void> _loadDiaries() async {
    final data = await DatabaseHelper.getAll();
    setState(() {
      diaries = data;
    });
  }

  // เพิ่ม: เขียน DB แล้วโหลดใหม่
  Future<void> _addDiary(Diary diary) async {
    await DatabaseHelper.insert(diary);
    _loadDiaries();
  }

  // ลบ: เขียน DB แล้วโหลดใหม่
  Future<void> _deleteDiary(int id) async {
    await DatabaseHelper.delete(id);
    _loadDiaries();
  }

  @override
  Widget build(BuildContext context) {
    // ... (โค้ด build ปรับตามด้านล่าง)
  }
}

แก้จุดเชื่อมใน build:

// ปุ่ม + : เปลี่ยนจาก diaries.add เป็น _addDiary
if (result != null && result is Diary) {
  _addDiary(result);               // แทน setState(() => diaries.add(result))
}

// ปุ่มลบ : เปลี่ยนจาก removeAt เป็น _deleteDiary
onPressed: () => _deleteDiary(diaries[i].id!),

อธิบาย:
- initState() = ทำงานครั้งเดียวตอนเปิดหน้า → โหลดข้อมูลจาก DB
- แพตเทิร์น "เขียน DB → โหลดใหม่" = เพิ่ม/ลบเสร็จโหลดใหม่ทุกครั้ง ข้อมูลตรงเสมอ

กด F5 → เพิ่มบันทึก 2-3 อัน → ปิดแอปจริง (หยุดใน VS Code) → รันใหม่

เช็คพอยต์ 5 (โมเมนต์สำคัญ!): ปิดแอปเปิดใหม่ บันทึกยังอยู่! นี่คือความต่างของ SQLite กับ RAM


สรุปสิ่งที่ได้สัปดาห์นี้

ขั้น ทำอะไร แนวคิดหลัก
1 หน้ารายการเป็น Stateful StatefulWidget + setState
2 หน้าฟอร์ม Form + TextFormField + validator
ฟอร์มเลือกค่า Dropdown, DatePicker, Checkbox, Switch, Radio
3 เพิ่มบันทึก Navigator.pop ส่งค่ากลับ + await
4 ลบบันทึก removeAt + setState
5 เตรียม Model toMap / fromMap + id
6 ตัวจัดการ DB CREATE TABLE, insert/getAll/delete
7 เชื่อม SQLite เขียน DB → โหลดใหม่

หัวใจที่ต้องจำ 4 ข้อ:
1. setState = เปลี่ยนข้อมูลแล้วสั่งวาดจอใหม่
2. validator คืน null = ผ่าน / คืน String = ผิด
3. SQLite = เขียน DB แล้วโหลดใหม่ ข้อมูลไม่หายตอนปิดแอป
4. Widget เลือกค่า (Dropdown/Checkbox/Radio/DatePicker) จัดการ state เองด้วย setState — ไม่ใช้ controller และไม่เข้า validator เหมือนช่องพิมพ์


ข้อจำกัด → โจทย์สัปดาห์หน้า

ตอนนี้แอปเก็บข้อมูลถาวรได้แล้ว แต่: - ข้อมูลอยู่แค่เครื่องเดียว — เปลี่ยนมือถือข้อมูลไม่ตามไป - บันทึกยังไม่มีอะไรเชื่อมโลกภายนอก

สัปดาห์หน้า เราจะให้แอป ดึงข้อมูลจากอินเทอร์เน็ต ด้วย Async + REST API