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

สัปดาห์ที่ 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 สร้างโปรเจกต์ใหม่

  1. เปิด VS Code
  2. เปิด Terminal → พิมพ์ Flutter create "projectname"
  3. รอสักครู่ให้ 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 สร้างเฉดที่เข้ากันให้เอง