บทที่ 21advanced|8 KB
บทที่ 21: Dynamic Theme System
สารบัญเนื้อหา
บทที่ 21: Dynamic Theme System
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- สร้างระบบ Theme ที่มีความยืดหยุ่นสูง (ไม่ใช่แค่ Light/Dark)
- ใช้
ThemeExtensionเพื่อเพิ่มสีพิเศษที่ Material Design ไม่มีให้ - สร้าง Animation ตอนเปลี่ยน Theme (Smooth Transition)
- จัดการ Theme แยกตาม User Preference
📚 ทฤษฎีและแนวคิด
1. ทำไมต้อง Dynamic Theme?
ผู้ใช้แต่ละคนมีความชอบไม่เหมือนกัน บางคนชอบสีมืด บางคนชอบสีสว่าง หรือบางแอปอาจจะมี "เทศกาล" (Christmas Theme, Halloween Theme) การเขียนโค้ดแบบ Hard-code สี จะทำให้แก้ยากมาก
2. ThemeData vs ThemeExtension
- ThemeData: คือกล่องมาตรฐานที่ Flutter ให้มา (มี primary, secondary, background ฯลฯ)
- ThemeExtension: คือ "ช่องเสริม" ที่เราเจาะเพิ่มเองได้ เช่น ถ้าเราอยากได้สี
success,warning,infoซึ่งไม่มีในมาตรฐาน MD3 เราต้องใช้ตัวนี้
💻 การเขียนโค้ด (Implementation)
1. สร้าง Theme Extension
ไฟล์: mobile/lib/theme/app_colors.dart
สมมติเราอยากได้สีตระกูล "Vivid" (สดใส) ที่ใช้ทั่วแอป
import 'package:flutter/material.dart';
**อธิบายทีละบรรทัด (Imports):**
- `import 'package:flutter/material.dart'`
- ใช้สำหรับคลาส `ThemeExtension` และ `Color`
**อธิบาย:**
@immutable
class AppColors extends ThemeExtension<AppColors> {
final Color vividGreen;
final Color vividYellow;
final Color vividRed;
const AppColors({
required this.vividGreen,
required this.vividYellow,
required this.vividRed,
});
// สร้างค่า Default (Light)
factory AppColors.light() {
return const AppColors(
vividGreen: Color(0xFF00C853),
vividYellow: Color(0xFFFFD600),
vividRed: Color(0xFFFF1744),
);
}
// สร้างค่า Default (Dark) - อาจจะปรับให้หม่นลงนิดนึง
factory AppColors.dark() {
return const AppColors(
vividGreen: Color(0xFF00E676), // สว่างขึ้นนิดหน่อยบนพื้นดำ
vividYellow: Color(0xFFFFEA00),
vividRed: Color(0xFFFF5252),
);
}
@override
ThemeExtension<AppColors> copyWith({
Color? vividGreen,
Color? vividYellow,
Color? vividRed,
}) {
return AppColors(
vividGreen: vividGreen ?? this.vividGreen,
vividYellow: vividYellow ?? this.vividYellow,
vividRed: vividRed ?? this.vividRed,
);
}
@override
ThemeExtension<AppColors> lerp(ThemeExtension<AppColors>? other, double t) {
if (other is! AppColors) return this;
// Feature เด็ด: Flutter จะคำนวณการไล่สีให้เองตอนเปลี่ยน Theme (Animation)
return AppColors(
vividGreen: Color.lerp(vividGreen, other.vividGreen, t)!,
vividYellow: Color.lerp(vividYellow, other.vividYellow, t)!,
vividRed: Color.lerp(vividRed, other.vividRed, t)!,
);
}
}
2. ผูกกับ ThemeData หลัก
ไฟล์: mobile/lib/theme/app_theme.dart
import 'app_colors.dart';
class AppTheme {
static ThemeData light() {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
// ... การตั้งค่าอื่นๆ ...
// ใส่ Extension เข้าไป
extensions: [
AppColors.light(),
],
);
}
static ThemeData dark() {
return ThemeData(
// ...
extensions: [
AppColors.dark(),
],
);
}
}
3. การเรียกใช้งานใน Widget
เวลาจะใช้สี เราจะไม่เรียก Colors.green ตรงๆ แต่จะเรียกผ่าน Theme
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ดึง Extension ออกมา
final appColors = Theme.of(context).extension<AppColors>()!;
return Container(
color: appColors.vividGreen, // สีจะเปลี่ยนเองตาม Theme
child: Text("Hello Dynamic World"),
);
}
}
4. Animated Theme Switcher
ไฟล์: mobile/lib/widgets/theme_switcher_button.dart
ปุ่มเปลี่ยน Theme สวยๆ พร้อม Animation
class ThemeSwitcherButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ThemeMode>(
valueListenable: ThemeController.instance.themeMode,
builder: (context, mode, child) {
// Icon จะเปลี่ยนตาม Mode
IconData icon;
if (mode == ThemeMode.light) icon = Icons.wb_sunny;
else if (mode == ThemeMode.dark) icon = Icons.nights_stay;
else icon = Icons.brightness_auto;
return IconButton(
icon: AnimatedSwitcher(
duration: Duration(milliseconds: 300),
transitionBuilder: (child, anim) => RotationTransition(
turns: anim,
child: child
),
child: Icon(icon, key: ValueKey(mode)),
),
onPressed: () {
// Logic วน Loop: Light -> Dark -> System
if (mode == ThemeMode.light) {
ThemeController.instance.setTheme(ThemeMode.dark);
} else if (mode == ThemeMode.dark) {
ThemeController.instance.setTheme(ThemeMode.system);
} else {
ThemeController.instance.setTheme(ThemeMode.light);
}
},
);
},
);
}
}
📝 แบบฝึกหัด
โจทย์ที่ 1: Custom Font Scale
สิ่งที่ต้องทำ:
- สร้าง
AppTypographyExtension - เก็บค่า
scaleFactor(เช่น 1.0, 1.2, 0.8) - ในหน้า UI ให้อ่านค่านี้ไปคูณกับขนาดฟอนต์ปกติ (User ปรับตัวหนังสือใหญ่เล็กได้)
โจทย์ที่ 2: Theme Presets (Color Palettes)
สิ่งที่ต้องทำ:
- สร้างธีมสีเพิ่ม: "Ocean" (ฟ้า), "Forest" (เขียว), "Sunset" (ส้ม)
- มีหน้าจอให้ User เลือก Theme สีที่ชอบ
- (ต้องแก้
ThemeControllerให้เก็บค่าสีด้วย ไม่ใช่แค่ Light/Dark)
📌 สรุป
Dynamic Theme ช่วยให้แอปดูมีชีวิตชีวาและใส่ใจผู้ใช้:
- Extensions: ช่วยให้เราหลุดกรอบจาก Material Design เดิมๆ
- Lerp (Linear Interpolation): คือเวทมนตร์ที่ทำให้การเปลี่ยนสีนุ่มนวล
- Separation of Concerns: แยก Logic สีออกจาก Logic หน้าจออย่างชัดเจน
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 22: Performance Optimization บทสุดท้าย! มาดูวิธีการทำให้แอปลื่นหัวแตก ไม่กระตุก ไม่กินแบต