บทที่ 9mobile|15 KB

บทที่ 9: Flutter และ Dart พื้นฐาน

บทที่ 9: Flutter และ Dart พื้นฐาน

🎯 เป้าหมายการเรียนรู้

หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:

  • เข้าใจว่า Flutter คืออะไร และทำไมถึงเป็นที่นิยม
  • เข้าใจพื้นฐาน Dart Language
  • สร้าง Widget และเข้าใจ Widget Tree
  • แยกความแตกต่างระหว่าง StatelessWidget และ StatefulWidget
  • สร้าง UI ด้วย Material Design

📚 ทฤษฎีและแนวคิด

Flutter คืออะไร?

Flutter คือ UI Framework สำหรับสร้าง Cross-platform Applications โดย Google

ข้อดีของ Flutter:

  • 📱 Cross-platform: เขียนโค้ดครั้งเดียว รันได้ทั้ง iOS, Android, Web, Desktop
  • 🎨 Beautiful UI: Material Design และ Cupertino (iOS-style) widgets
  • ⚡ Hot Reload: เห็นผลการแก้ไขโค้ดทันที (ไม่ต้อง rebuild)
  • 🚀 Performance: Compile เป็น Native Code (เร็วเท่า Native Apps)
  • 💙 Dart Language: ภาษาที่เรียนรู้ง่าย คล้าย JavaScript

Dart Language พื้นฐาน

1. Variables (ตัวแปร)

// Type inference (Dart รู้ type เอง)
var name = "John";
var age = 25;
var isActive = true;

// Explicit type
String email = "john@example.com";
int count = 10;
double price = 99.99;
bool isDark = false;

// Final (กำหนดค่าได้ครั้งเดียว)
final userId = "123";

// Const (ค่าคงที่ตอน compile)
const apiUrl = "https://api.example.com";

2. Functions (ฟังก์ชัน)

// Function with return type
String greet(String name) {
  return "Hello, $name!";
}

// Arrow function (สำหรับ expression เดียว)
String greet(String name) => "Hello, $name!";

// Optional parameters
void printInfo(String name, {int? age, String? city}) {
  print("Name: $name");
  if (age != null) print("Age: $age");
  if (city != null) print("City: $city");
}

// Usage
printInfo("John", age: 25);
printInfo("Jane", city: "Bangkok");

3. Null Safety

// Non-nullable (ห้ามเป็น null)
String name = "John";
// name = null; // ❌ Error!

// Nullable (อนุญาตให้เป็น null)
String? email; // email เป็น null ได้
email = "test@example.com";
email = null; // ✅ OK

// Null-aware operators
String? username;
print(username ?? "Guest"); // ถ้า username เป็น null ใช้ "Guest"

String? displayName;
print(displayName?.toUpperCase()); // ถ้า displayName เป็น null จะไม่เรียก toUpperCase()

4. Collections

// List (Array)
List<String> fruits = ["Apple", "Banana", "Orange"];
fruits.add("Mango");
print(fruits[0]); // Apple
print(fruits.length); // 4

// Map (Dictionary/Object)
Map<String, dynamic> user = {
  "name": "John",
  "age": 25,
  "email": "john@example.com"
};
print(user["name"]); // John

// Set (ไม่มีค่าซ้ำ)
Set<int> numbers = {1, 2, 3, 3, 4};
print(numbers); // {1, 2, 3, 4}

5. Classes

class User {
  String name;
  int age;
  
  // Constructor
  User(this.name, this.age);
  
  // Named constructor
  User.guest() : name = "Guest", age = 0;
  
  // Method
  void introduce() {
    print("Hi, I'm $name, $age years old");
  }
}

// Usage
var user1 = User("John", 25);
user1.introduce(); // Hi, I'm John, 25 years old

var guest = User.guest();
guest.introduce(); // Hi, I'm Guest, 0 years old

💻 โค้ดจริงจาก QUOM App

1. Main Entry Point

ไฟล์: mobile/lib/main.dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'screens/splash_screen.dart';
import 'services/theme_controller.dart';
import 'services/language_controller.dart';
import 'theme/app_theme.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const QuomApp());
}

อธิบายทีละบรรทัด:

  1. Import Statements

    import 'package:flutter/material.dart';
    
    • นำเข้า Material Design widgets
    • material.dart มี widgets พื้นฐานทั้งหมด (Text, Button, Container, etc.)
  2. main() Function

    void main() {
      WidgetsFlutterBinding.ensureInitialized();
      runApp(const QuomApp());
    }
    
    • main(): จุดเริ่มต้นของ Dart program
    • WidgetsFlutterBinding.ensureInitialized(): เตรียม Flutter framework
    • runApp(): รัน Flutter application

2. QuomApp Widget (Root Widget)

class QuomApp extends StatelessWidget {
  const QuomApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: ThemeController.instance.themeMode,
      builder: (context, mode, child) {
        return ValueListenableBuilder<Locale>(
          valueListenable: LanguageController.instance.locale,
          builder: (context, locale, child) {
            return MaterialApp(
              debugShowCheckedModeBanner: false,
              title: 'QUOM',
              theme: _enhanceWithFonts(AppTheme.light()),
              darkTheme: _enhanceWithFonts(AppTheme.dark()),
              themeMode: mode,
              locale: locale,
              home: const SplashScreen(),
            );
          },
        );
      },
    );
  }
}

อธิบายแนวคิด:

StatelessWidget

class QuomApp extends StatelessWidget {
  const QuomApp({super.key});

คำอธิบาย:

  • StatelessWidget: Widget ที่ไม่มี state (ข้อมูลไม่เปลี่ยน)
  • const: Constructor เป็น const (ประหยัด memory)
  • super.key: ส่ง key ไปยัง parent class

build() Method

@override
Widget build(BuildContext context) {
  return MaterialApp(...);
}

คำอธิบาย:

  • build(): Method ที่ return Widget tree
  • @override: บอกว่า override method จาก parent class
  • BuildContext: ข้อมูลเกี่ยวกับตำแหน่งของ widget ใน tree

MaterialApp

return MaterialApp(
  debugShowCheckedModeBanner: false,
  title: 'QUOM',
  theme: _enhanceWithFonts(AppTheme.light()),
  darkTheme: _enhanceWithFonts(AppTheme.dark()),
  themeMode: mode,
  locale: locale,
  home: const SplashScreen(),
);

คำอธิบาย:

  • MaterialApp: Root widget สำหรับ Material Design app
  • debugShowCheckedModeBanner: false: ซ่อน "DEBUG" banner
  • title: ชื่อ app (แสดงใน task switcher)
  • theme: Theme สำหรับ light mode
  • darkTheme: Theme สำหรับ dark mode
  • themeMode: โหมดปัจจุบัน (light/dark/system)
  • locale: ภาษาที่ใช้
  • home: หน้าแรกของ app

3. ValueListenableBuilder (Reactive UI)

return ValueListenableBuilder<ThemeMode>(
  valueListenable: ThemeController.instance.themeMode,
  builder: (context, mode, child) {
    // UI จะ rebuild เมื่อ themeMode เปลี่ยน
    return MaterialApp(
      themeMode: mode,
      ...
    );
  },
);

คำอธิบาย:

  • ValueListenableBuilder: Widget ที่ rebuild เมื่อค่าเปลี่ยน
  • valueListenable: ค่าที่ต้องการ listen
  • builder: Function ที่สร้าง UI
  • Reactive: เมื่อ themeMode เปลี่ยน UI จะ update อัตโนมัติ

4. Custom Theme Enhancement

ThemeData _enhanceWithFonts(ThemeData theme) {
  final textTheme = GoogleFonts.promptTextTheme(theme.textTheme);
  
  return theme.copyWith(
    textTheme: textTheme.copyWith(
      displayLarge: GoogleFonts.prompt(
        color: theme.colorScheme.onSurface,
        fontWeight: FontWeight.bold,
      ),
      titleLarge: GoogleFonts.prompt(
        color: theme.colorScheme.onSurface,
        fontWeight: FontWeight.w700,
      ),
      bodyLarge: GoogleFonts.prompt(
        color: theme.colorScheme.onSurface,
        height: 1.5,
      ),
    ),
  );
}

คำอธิบาย:

  • GoogleFonts.promptTextTheme(): ใช้ฟอนต์ Prompt (รองรับภาษาไทย)
  • theme.copyWith(): สร้าง theme ใหม่โดยแก้ไขบางส่วน
  • FontWeight.bold: น้ำหนักฟอนต์ (bold = 700)
  • height: 1.5: ระยะห่างระหว่างบรรทัด (line height)

🎨 StatelessWidget vs StatefulWidget

StatelessWidget (ไม่มี State)

class GreetingWidget extends StatelessWidget {
  final String name;
  
  const GreetingWidget({required this.name, super.key});
  
  @override
  Widget build(BuildContext context) {
    return Text("Hello, $name!");
  }
}

// Usage
GreetingWidget(name: "John")

เมื่อไหร่ใช้:

  • UI ที่ไม่เปลี่ยนแปลง
  • รับ data จาก parent เท่านั้น

StatefulWidget (มี State)

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

class _CounterWidgetState extends State<CounterWidget> {
  int count = 0;
  
  void increment() {
    setState(() {
      count++;
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text("Count: $count"),
        ElevatedButton(
          onPressed: increment,
          child: Text("Increment"),
        ),
      ],
    );
  }
}

คำอธิบาย:

  • StatefulWidget: Widget ที่มี state (ข้อมูลเปลี่ยนได้)
  • State<CounterWidget>: Class ที่เก็บ state
  • setState(): บอก Flutter ให้ rebuild widget
  • เมื่อไหร่ใช้: UI ที่ต้องเปลี่ยนแปลงตาม user interaction

🏗️ Widget Tree Concept

MaterialApp
  └─ SplashScreen
       └─ Scaffold
            ├─ AppBar
            │    └─ Text
            └─ Body
                 └─ Center
                      └─ Column
                           ├─ Image
                           ├─ Text
                           └─ CircularProgressIndicator

แนวคิด:

  • Flutter UI เป็น tree ของ widgets
  • Widget แต่ละตัวสามารถมี children ได้
  • Parent widget ควบคุม layout ของ children

🎨 Common Widgets

1. Text

Text(
  "Hello, World!",
  style: TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: Colors.blue,
  ),
)

2. Container

Container(
  width: 200,
  height: 100,
  padding: EdgeInsets.all(16),
  margin: EdgeInsets.symmetric(vertical: 8),
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(16),
    boxShadow: [
      BoxShadow(
        color: Colors.black.withOpacity(0.2),
        blurRadius: 10,
        offset: Offset(0, 4),
      ),
    ],
  ),
  child: Text("Container"),
)

3. Column & Row

// Column (แนวตั้ง)
Column(
  mainAxisAlignment: MainAxisAlignment.center,
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text("Item 1"),
    Text("Item 2"),
    Text("Item 3"),
  ],
)

// Row (แนวนอน)
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Icon(Icons.home),
    Icon(Icons.search),
    Icon(Icons.settings),
  ],
)

4. ElevatedButton

ElevatedButton(
  onPressed: () {
    print("Button pressed!");
  },
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,
    padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
  child: Text("Click Me"),
)

5. TextField

TextField(
  controller: emailController,
  keyboardType: TextInputType.emailAddress,
  decoration: InputDecoration(
    labelText: "Email",
    hintText: "Enter your email",
    prefixIcon: Icon(Icons.email),
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

📝 แบบฝึกหัด

คำถามทบทวน

  1. Flutter คืออะไร และมีข้อดีอย่างไร?
  2. StatelessWidget และ StatefulWidget ต่างกันอย่างไร?
  3. setState() ทำหน้าที่อะไร?
  4. Widget Tree คืออะไร?
  5. MaterialApp ใช้ทำอะไร?

โจทย์ปฏิบัติ

โจทย์ที่ 1: สร้าง StatelessWidget

// TODO: สร้าง ProfileCard widget ที่รับ name และ email
// แสดงเป็น Card พร้อม Icon, ชื่อ, และ email

โจทย์ที่ 2: สร้าง StatefulWidget

// TODO: สร้าง TodoItem widget ที่มี checkbox
// เมื่อกด checkbox ให้ขีดฆ่าข้อความ

โจทย์ที่ 3: สร้าง Layout

// TODO: สร้างหน้า Login ที่มี:
// - Logo (Image)
// - Email TextField
// - Password TextField
// - Login Button
// ใช้ Column, Container, และ TextField

📌 สรุป

ในบทนี้เราได้เรียนรู้:

✅ Flutter คือ Cross-platform UI Framework
✅ Dart คือภาษาที่ใช้เขียน Flutter
✅ StatelessWidget สำหรับ UI ที่ไม่เปลี่ยน
✅ StatefulWidget สำหรับ UI ที่มี state
✅ Widget Tree คือโครงสร้างของ Flutter UI
✅ Material Design widgets สำหรับสร้าง UI สวยงาม

ไฟล์ที่เกี่ยวข้อง


🔜 บทถัดไป

บทที่ 10: App Architecture และ State Management
เราจะเรียนรู้การจัดโครงสร้างโปรเจค, Service Layer, และการจัดการ State


📚 แหล่งเรียนรู้เพิ่มเติม