สัปดาห์ที่ 1: หน้าตาและการนำทาง (UI + Layout + Navigation)
เป้าหมายสัปดาห์นี้: สร้างโครงหน้าตาของแอป MyDiary — มีรายการบันทึก แตะดูรายละเอียดได้ มีเมนูสลับหน้า (ข้อมูลยังพิมพ์ไว้ในโค้ดก่อน สัปดาห์หน้าค่อยให้ผู้ใช้เพิ่มเอง)
สัปดาห์นี้เราจะสร้างอะไร
เราจะสร้างแอปไดอารี MyDiary ที่เมื่อจบสัปดาห์นี้จะมี - หน้ารายการบันทึก (แสดงบันทึกเป็นการ์ดเรียงกัน) - หน้ารายละเอียด (แตะบันทึกแล้วเปิดดูเนื้อหาเต็ม) - เมนูล่าง (สลับระหว่างหน้า "บันทึก" กับ "เกี่ยวกับ")
สิ่งที่ยังทำไม่ได้ (เก็บไว้สัปดาห์หน้า): ผู้ใช้ยังเพิ่มบันทึกเองไม่ได้ — ข้อมูลถูกพิมพ์ไว้ในโค้ดก่อน เพราะสัปดาห์นี้เราโฟกัสที่ "หน้าตา" และ "การนำทาง" ให้แน่นก่อน
ส่วนที่ 1: ทำความเข้าใจ "อะไรเป็นอะไร" ใน Flutter
อ่านส่วนนี้ให้เข้าใจก่อนลงมือ — เหมือนรู้จักชิ้นส่วนก่อนต่อเลโก้
1.1 Flutter คืออะไร
Flutter คือเครื่องมือสร้างแอปมือถือ (ทั้ง Android และ iOS) ด้วยโค้ดชุดเดียว เขียนด้วยภาษา Dart
1.2 หัวใจของ Flutter: ทุกอย่างคือ "Widget"
ใน Flutter ทุกอย่างบนหน้าจอคือ Widget — ข้อความก็ Widget, ปุ่มก็ Widget, รูปก็ Widget, แม้แต่การจัดวาง (เรียงแนวตั้ง/แนวนอน) ก็เป็น Widget
คิดง่ายๆ: Widget = ชิ้นส่วนเลโก้ เราเอาชิ้นเล็กๆ มาประกอบกันเป็นหน้าจอ
หน้าจอ = Widget ใหญ่
└─ ประกอบจาก Widget เล็กๆ
├─ ข้อความ (Text)
├─ ปุ่ม (Button)
└─ รูป (Image)
1.3 Widget มี 2 ตระกูล
| ตระกูล | คือ | ตัวอย่าง |
|---|---|---|
| StatelessWidget | Widget ที่ "นิ่ง" ไม่มีข้อมูลเปลี่ยน | หน้า About, การ์ดแสดงข้อมูล |
| StatefulWidget | Widget ที่ "มีชีวิต" ข้อมูลเปลี่ยนได้ | ตัวนับ, ฟอร์ม (เรียนสัปดาห์หน้า) |
สัปดาห์นี้เราใช้ StatelessWidget เป็นหลัก เพราะข้อมูลยังไม่เปลี่ยน
1.4 Widget สำคัญที่จะใช้สัปดาห์นี้
| Widget | หน้าที่ | เปรียบเทียบ |
|---|---|---|
Scaffold |
โครงหน้าจอ 1 หน้า (มีหัว-ตัว-ล่าง) | กระดาษเปล่า 1 แผ่น |
AppBar |
แถบหัวด้านบน (ชื่อหน้า) | หัวกระดาษ |
Text |
แสดงข้อความ | ปากกาเขียนตัวอักษร |
Column |
เรียง Widget แนวตั้ง (บนลงล่าง) | ชั้นวางของแนวตั้ง |
Row |
เรียง Widget แนวนอน (ซ้ายไปขวา) | ชั้นวางของแนวนอน |
Card |
กล่องมีเงา มุมโค้ง | การ์ดกระดาษ |
ListView |
รายการเลื่อนได้ | ม้วนกระดาษยาวๆ |
ListTile |
1 แถวในรายการ (มีหัว-ท้าย) | 1 บรรทัดในสมุด |
1.5 โครงสร้างพื้นฐานของไฟล์ Flutter
ทุกแอป Flutter เริ่มที่ฟังก์ชัน main():
void main() {
runApp(MyApp()); // สั่งให้แอปเริ่มทำงาน แสดง MyApp
}
main()= จุดเริ่มของโปรแกรม (ทุกแอปมี)runApp(...)= สั่งวาดหน้าจอด้วย Widget ที่ใส่เข้าไป
ส่วนที่ 2: เตรียมเครื่องมือ
2.1 สร้างโปรเจกต์ใหม่
- เปิด VS Code
- เปิด
Terminal→ พิมพ์Flutter create "projectname" - รอสักครู่ให้ Flutter สร้างไฟล์ให้
2.2 ไฟล์ที่เราจะแก้
เปิดไฟล์ lib/main.dart — นี่คือไฟล์หลักที่เราจะเขียนโค้ดทั้งหมดในสัปดาห์นี้
2.3 ลบโค้ดตัวอย่างทิ้ง
โค้ดที่ Flutter สร้างมาให้ (แอปนับเลข) ให้ ลบทั้งหมด แล้วเราจะเขียนใหม่ทีละขั้น
ส่วนที่ 3: ลงมือทำทีละขั้น
ทำตามทีละขั้น รันดูผลทุกขั้น (กด
F5) — เห็นแอปค่อยๆ โตขึ้น
ขั้นที่ 1 — หน้าจอแรกสุด (Hello MyDiary)
เป้าหมาย: ให้แอปแสดงข้อความบนจอได้ก่อน
วางโค้ดนี้ใน main.dart:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('MyDiary')),
body: const Center(
child: Text('สวัสดี ไดอารีของฉัน'),
),
),
);
}
}
- import ...material.dart = เรียกใช้ชุด Widget สำเร็จรูปของ Flutter (ต้องมีเสมอ)
- MaterialApp = ห่อแอปทั้งหมด (ตั้งค่าธีม สี ฯลฯ)
- Scaffold = โครงหน้าจอ 1 หน้า
- AppBar = แถบหัวสีน้ำเงินด้านบน
- body = เนื้อหาตรงกลางจอ
- Center = จัดให้อยู่กึ่งกลาง
- Text = ข้อความที่แสดง
ขั้นที่ 2 — เตรียมข้อมูลบันทึก (Model)
เป้าหมาย: สร้าง "แม่พิมพ์" ของบันทึก 1 อัน
แนวคิด: บันทึก 1 อันมี "ชื่อเรื่อง" กับ "เนื้อหา" โดยทำการเราสร้าง class มาเก็บข้อมูล
เพิ่มโค้ดนี้ (ใต้ import):
// แม่พิมพ์ของบันทึก 1 อัน
class Diary {
final String title; // ชื่อเรื่อง
final String content; // เนื้อหา
Diary({required this.title, required this.content});
}
- class Diary = แม่พิมพ์บันทึก
- final String title = ตัวแปรเก็บชื่อเรื่อง (final = ตั้งค่าครั้งเดียว)
- Diary({required ...}) = วิธีสร้างบันทึกใหม่ ต้องใส่ title และ content
ยังไม่เห็นผลบนจอ — ขั้นนี้แค่เตรียมโครงสร้างข้อมูล
ขั้นที่ 3 — สร้างข้อมูลตัวอย่าง
เป้าหมาย: เตรียมบันทึกตัวอย่าง 3-4 อัน (พิมพ์ไว้ในโค้ดก่อน)
ในคลาส _DiaryListPageState (จะสร้างในขั้นถัดไป) เราจะมี List นี้ — เตรียมไว้ก่อน:
final List<Diary> diaries = [
Diary(title: 'วันแรกของการเรียน Flutter', content: 'วันนี้ได้เริ่มเขียนแอปตัวแรก รู้สึกตื่นเต้นมาก'),
Diary(title: 'เข้าใจ Widget แล้ว', content: 'ทุกอย่างใน Flutter คือ Widget เหมือนต่อเลโก้'),
Diary(title: 'ทำ ListView สำเร็จ', content: 'วันนี้ทำรายการเลื่อนได้ ภูมิใจจัง'),
];
- List<Diary> = รายการที่เก็บบันทึกหลายอัน
- ข้างในมีบันทึก 3 อัน แต่ละอันสร้างด้วยแม่พิมพ์ Diary
ขั้นที่ 4 — หน้ารายการบันทึก (ListView)
เป้าหมาย: แสดงบันทึกทั้งหมดเป็นการ์ดเรียงกัน เลื่อนได้
แทนที่ MyApp เดิมด้วยโครงใหม่นี้ (แยกหน้ารายการออกมา):
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
// แม่พิมพ์บันทึก
class Diary {
final String title;
final String content;
Diary({required this.title, required this.content});
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'MyDiary',
theme: ThemeData(colorSchemeSeed: Colors.indigo), // ธีมสีคราม
home: const DiaryListPage(),
);
}
}
// หน้ารายการบันทึก
class DiaryListPage extends StatelessWidget {
const DiaryListPage({super.key});
@override
Widget build(BuildContext context) {
// ข้อมูลตัวอย่าง (พิมพ์ไว้ในโค้ดก่อน)
final List<Diary> diaries = [
Diary(title: 'วันแรกของการเรียน Flutter', content: 'วันนี้ได้เริ่มเขียนแอปตัวแรก รู้สึกตื่นเต้นมาก'),
Diary(title: 'เข้าใจ Widget แล้ว', content: 'ทุกอย่างใน Flutter คือ Widget เหมือนต่อเลโก้'),
Diary(title: 'ทำ ListView สำเร็จ', content: 'วันนี้ทำรายการเลื่อนได้ ภูมิใจจัง'),
];
return Scaffold(
appBar: AppBar(title: const Text('บันทึกของฉัน')),
body: ListView.builder(
itemCount: diaries.length, // มีกี่อัน
itemBuilder: (context, i) { // สร้างการ์ดทีละอัน
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: const Icon(Icons.book, color: Colors.indigo),
title: Text(diaries[i].title),
subtitle: Text(
diaries[i].content,
maxLines: 1,
overflow: TextOverflow.ellipsis, // ยาวเกินตัดด้วย ...
),
),
);
},
),
);
}
}
- ListView.builder = สร้างรายการเลื่อนได้ (เหมาะกับข้อมูลหลายอัน)
- itemCount = จำนวนรายการ (นับจาก diaries.length)
- itemBuilder = สูตรสร้างการ์ด 1 ใบ ทำซ้ำทุกอันในรายการ
- i = ตำแหน่งของรายการ (0, 1, 2, ...)
- diaries[i].title = ชื่อเรื่องของบันทึกตำแหน่งที่ i
- Card = กล่องมีเงา / ListTile = จัดแถวสวยงามให้อัตโนมัติ
ขั้นที่ 5 — หน้ารายละเอียด (หน้าที่สอง)
เป้าหมาย: สร้างหน้าที่แสดงบันทึกเต็มๆ
เพิ่ม class นี้ (ท้ายไฟล์):
// หน้ารายละเอียดบันทึก
class DiaryDetailPage extends StatelessWidget {
final Diary diary; // รับบันทึกที่ถูกแตะเข้ามา
const DiaryDetailPage({super.key, required this.diary});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('รายละเอียด')),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // ชิดซ้าย
children: [
Text(
diary.title,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16), // เว้นระยะ
Text(diary.content, style: const TextStyle(fontSize: 16)),
],
),
),
);
}
}
- final Diary diary = ตัวแปรรับบันทึกที่ส่งมาจากหน้ารายการ
- Column = เรียงชื่อเรื่องกับเนื้อหาแนวตั้ง
- SizedBox(height: 16) = ช่องว่างระหว่างชื่อกับเนื้อหา
ยังเปิดหน้านี้ไม่ได้ — ต้องต่อการนำทางในขั้นถัดไป
ขั้นที่ 6 — เชื่อมสองหน้าด้วยการนำทาง (Navigation)
เป้าหมาย: แตะการ์ด → เปิดหน้ารายละเอียด
แนวคิด: Flutter นำทางแบบ "ซ้อนหน้า" — เปิดหน้าใหม่ทับ (push) แล้วกดย้อนกลับ (pop) ได้
กลับไปที่ ListTile ในขั้น 4 เพิ่ม onTap:
child: ListTile(
leading: const Icon(Icons.book, color: Colors.indigo),
title: Text(diaries[i].title),
subtitle: Text(diaries[i].content, maxLines: 1, overflow: TextOverflow.ellipsis),
onTap: () { // เมื่อแตะการ์ด
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DiaryDetailPage(diary: diaries[i]), // ส่งบันทึกที่แตะไป
),
);
},
),
- onTap = ทำงานเมื่อแตะการ์ด
- Navigator.push(...) = เปิดหน้าใหม่ทับหน้าเดิม
- MaterialPageRoute(...) = ตัวห่อหน้าใหม่ให้เข้ากับระบบ Android/iOS (ดูอธิบายเพิ่มด้านล่าง)
- builder: (context) => DiaryDetailPage(...) = บอกว่าจะเปิดหน้าอะไร
- DiaryDetailPage(diary: diaries[i]) = เปิดหน้ารายละเอียด พร้อมส่งบันทึกที่ถูกแตะไปด้วย
- ปุ่มย้อนกลับ (←) บน AppBar โผล่ให้เอง กดกลับได้อัตโนมัติ
ทำไมต้องมี MaterialPageRoute?
Navigator.push แปลว่า "เปิดหน้าใหม่" แต่มันต้องรู้ 2 อย่างก่อน:
1. จะเปิดหน้าอะไร → บอกผ่าน builder
2. เปิดยังไง (สไตล์ไหน) → นี่คืองานของ MaterialPageRoute
MaterialPageRoute คือ "ผู้ห่อหน้า" ที่ทำให้การเปิดหน้าใหม่มี แอนิเมชันและพฤติกรรมแบบ Material Design — บน Android หน้าใหม่จะเลื่อนขึ้นมา บน iOS จะเลื่อนจากขวาเข้ามา และมีปุ่มย้อนกลับให้อัตโนมัติ พร้อมรองรับการปัดกลับ (swipe back)
จำง่ายๆ: ทุกครั้งที่
pushไปหน้าใหม่ ต้องห่อด้วยMaterialPageRouteเสมอ — เป็นคู่กันเหมือนพัสดุต้องมีกล่อง Flutter จะได้รู้ว่าเปิดหน้าใหม่ด้วยสไตล์มาตรฐานจุดที่นักศึกษามักงง:
builderมีคำว่าcontextซ้ำกับข้างนอก — ไม่ต้องกังวล มันคือตัวแปรที่ Flutter ส่งให้ใช้ภายใน แค่เขียนตามรูปแบบนี้ได้เลย
MaterialPageRoute มีตัวอื่นแทนได้ไหม? มีค่ะ แต่สัปดาห์นี้ใช้ตัวนี้ก็พอ — รู้จักไว้ว่ามีทางเลือก:
| ตัวเลือก | ใช้เมื่อ | ระดับ |
|---|---|---|
| MaterialPageRoute | เปิดหน้าใหม่ทั่วไป — ที่เราใช้ พอสำหรับเกือบทุกกรณี | เริ่มต้น |
CupertinoPageRoute |
อยากได้สไตล์ iOS ล้วน (เลื่อนจากขวาแบบ iPhone) ทุกเครื่อง | ทางเลือก |
PageRouteBuilder |
อยากออกแบบแอนิเมชันเปลี่ยนหน้าเอง (จางหาย/หมุน/ซูม) | ขั้นสูง |
Named Routes (Navigator.pushNamed) |
แอปหน้าเยอะมาก อยากตั้งชื่อเส้นทางไว้เรียก | ขั้นกลาง |
เริ่มต้นใช้
MaterialPageRouteให้คล่องก่อน ตัวอื่นค่อยศึกษาเพิ่มตอนทำ Semester Project ถ้าต้องการแอนิเมชันพิเศษ
กด F5 → ควรเห็น: แตะการ์ด → เปิดหน้ารายละเอียด → กด ← กลับมาหน้ารายการได้
เช็คพอยต์ 3: แตะแล้วเปิดหน้าใหม่ได้ = การนำทางสำเร็จ!
ขั้นที่ 7 — เมนูล่างสลับหน้า (BottomNavigationBar)
เป้าหมาย: เพิ่มเมนูล่างสลับ "บันทึก" กับ "เกี่ยวกับ"
แนวคิด: เมนูล่างต้องจำว่าเลือกหน้าไหนอยู่ → ต้องใช้ StatefulWidget (Widget มีชีวิต ตัวแรกของเรา)
สร้าง class ใหม่ห่อทั้งแอป (แทน home: DiaryListPage()):
// เปลี่ยน home ใน MyApp เป็น
home: const MainPage(),
// เพิ่ม class นี้
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _selectedIndex = 0; // หน้าที่เลือกอยู่ (0=บันทึก, 1=เกี่ยวกับ)
// รายการหน้า
final List<Widget> _pages = [
const DiaryListPage(),
const AboutPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_selectedIndex], // แสดงหน้าที่เลือก
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) {
setState(() { // เปลี่ยนหน้า + วาดจอใหม่
_selectedIndex = index;
});
},
items: const [
BottomNavigationBarItem(icon: Icon(Icons.book), label: 'บันทึก'),
BottomNavigationBarItem(icon: Icon(Icons.info), label: 'เกี่ยวกับ'),
],
),
);
}
}
// หน้าเกี่ยวกับ (ง่ายๆ)
class AboutPage extends StatelessWidget {
const AboutPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('เกี่ยวกับ')),
body: const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.book, size: 80, color: Colors.indigo),
SizedBox(height: 16),
Text('MyDiary', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Text('แอปสมุดบันทึกส่วนตัว', style: TextStyle(fontSize: 16)),
Text('สร้างโดย [ชื่อของคุณ]'),
],
),
),
),
);
}
}
- _selectedIndex = เก็บว่าเลือกหน้าไหน (state)
- setState(...) = สั่งเปลี่ยนหน้า + วาดจอใหม่ (สัปดาห์หน้าเรียนละเอียด)
- BottomNavigationBar = เมนูล่าง / items = ปุ่มแต่ละอัน
F5 → ควรเห็น: เมนูล่าง 2 ปุ่ม กดสลับหน้าบันทึก↔เกี่ยวกับได้
เช็คพอยต์ 4 (สุดท้าย): สลับหน้าได้ = จบสัปดาห์ที่ 1!
สรุปสิ่งที่ได้สัปดาห์นี้
| ขั้น | ทำอะไร | Widget/แนวคิดหลัก |
|---|---|---|
| 1 | หน้าจอแรก | MaterialApp, Scaffold, AppBar, Text |
| 2 | แม่พิมพ์ข้อมูล | class Diary |
| 3 | ข้อมูลตัวอย่าง | List |
| 4 | รายการบันทึก | ListView.builder, Card, ListTile |
| 5 | หน้ารายละเอียด | Column, ส่งข้อมูลผ่าน constructor |
| 6 | การนำทาง | Navigator.push |
| 7 | เมนูล่าง | BottomNavigationBar, StatefulWidget |
หัวใจที่ต้องจำ 3 ข้อ: 1. ทุกอย่างคือ Widget — ประกอบชิ้นเล็กเป็นหน้าจอ 2. ListView.builder — แสดงรายการหลายอันจาก List 3. Navigator.push — เปิดหน้าใหม่ + ส่งข้อมูลผ่าน constructor
ข้อจำกัดของสัปดาห์นี้ → โจทย์สัปดาห์หน้า
ตอนนี้แอปสวยแล้ว แต่ลองสังเกต - ผู้ใช้เพิ่มบันทึกเองไม่ได้ — ข้อมูลถูกพิมพ์ไว้ในโค้ด - ถ้าอยากเพิ่มบันทึกต้องแก้โค้ดเอง ซึ่งผู้ใช้จริงทำไม่ได้
สัปดาห์หน้า เราจะเพิ่ม ฟอร์มให้ผู้ใช้เขียนบันทึกเอง และทำให้ เพิ่ม/ลบ/แก้ได้ พร้อม เก็บถาวร (ปิดแอปเปิดใหม่ข้อมูลยังอยู่) ด้วย State + Form + SQLite
ภาคผนวก: คำที่เห็นบ่อยในโค้ด (เปิดดูเมื่อสงสัย)
คำเหล่านี้โผล่ในโค้ดหลายที่ ไม่ต้องท่องจำ — เข้าใจคร่าวๆ แล้วเปิดดูตารางนี้เมื่อเจอ
โครงสร้างที่พิมพ์ตามแบบ (เขียนเหมือนกันทุกครั้ง)
| คำ | คืออะไร | ต้องจำไหม |
|---|---|---|
class X extends StatelessWidget |
ประกาศ Widget แบบนิ่ง | เขียนตามแบบ |
@override |
บอกว่าเมธอดนี้เขียนทับของเดิม | เขียนตามแบบ |
Widget build(BuildContext context) |
เมธอดที่วาดหน้าจอ (ทุก Widget มี) | เขียนตามแบบ |
super.key |
ส่งค่าให้ Widget แม่ (เรื่องภายใน) | เขียนตามแบบ ไม่ต้องเข้าใจลึก |
BuildContext context |
ข้อมูลตำแหน่งของ Widget ในจอ | Flutter ส่งให้ ใช้ตามแบบ |
const — เจอบ่อยมาก
const = บอกว่า "ค่านี้ไม่เปลี่ยนแน่นอน" ช่วยให้แอปเร็วขึ้น
- ใส่หน้า Widget ที่ค่าคงที่ เช่น const Text('MyDiary')
- ถ้าลืมใส่ ไม่ error แค่แอปทำงานหนักขึ้นนิดหน่อย (VS Code จะขีดเส้นเตือนให้ใส่)
การเว้นระยะ (EdgeInsets)
| โค้ด | ความหมาย |
|---|---|
EdgeInsets.all(16) |
เว้นขอบทุกด้าน 16 |
EdgeInsets.symmetric(horizontal: 12, vertical: 6) |
เว้นซ้าย-ขวา 12, บน-ล่าง 6 |
SizedBox(height: 16) |
ช่องว่างสูง 16 (คั่นระหว่าง Widget) |
Padding(padding: ..., child: ...) |
ดันขอบให้ Widget ข้างใน |
การจัดตำแหน่งใน Column / Row
| คำ | ควบคุม | ค่าที่ใช้บ่อย |
|---|---|---|
mainAxisAlignment |
แกนหลัก (Column=แนวตั้ง, Row=แนวนอน) | .center, .spaceBetween |
crossAxisAlignment |
แกนขวาง | .start (ชิดซ้าย), .center |
การจัดการข้อความยาว
| คำ | ความหมาย |
|---|---|
maxLines: 1 |
แสดงมากสุด 1 บรรทัด |
overflow: TextOverflow.ellipsis |
เกินให้ตัดด้วย ... |
ธีมสี
| คำ | ความหมาย |
|---|---|
theme: ThemeData(...) |
ตั้งค่าหน้าตารวมของแอป |
colorSchemeSeed: Colors.indigo |
ใช้สีครามเป็นสีหลัก Flutter สร้างเฉดที่เข้ากันให้เอง |