ในครึ่งเทอมแรก นักศึกษาได้เรียนรู้พื้นฐานการออกแบบหน้าจอและ Widget เบื้องต้นใน Flutter มาแล้ว สำหรับครึ่งเทอมหลังนี้ เราจะก้าวไปอีกขั้นสู่ "การพัฒนาแอปพลิเคชันระดับมืออาชีพ"
พัฒนานักศึกษาจากการเขียนโค้ดหน้าจอเดี่ยว สู่การออกแบบระบบแอปพลิเคชันที่สามารถเชื่อมต่อ API และทำงานร่วมกันในทีมได้จริง
โปรเจกต์หลักของเราคือ Compass App (แอปบันทึกตารางการเดินทางและการจองทริป) ซึ่งดัดแปลงโครงสร้างมาจากแอปพลิเคชันต้นแบบระดับ Production ของทีมผู้พัฒนา Flutter เพื่อให้มั่นใจว่านักศึกษาจะได้จับงานที่ใกล้เคียงการทำงานจริงที่สุด
ในการพัฒนาอินเตอร์เฟซด้วย Flutter ส่วนประกอบของหน้าจอจะถูกประกอบขึ้นจาก Widget ซึ่งถูกออกแบบให้มีขนาดเล็กและสามารถนำมาใช้งานซ้ำได้ . โดยระบบแบ่ง Widget พื้นฐานออกเป็นสองประเภทหลัก:
หน้าจอที่ทำงานแบบไร้สถานะ (Immutable) โดยข้อมูลที่ถูกกำหนดผ่านทาง Constructor จะไม่สามารถเปลี่ยนแปลงค่าได้ตลอดอายุการทำงาน . เหมาะสำหรับหน้าจอแสดงผลคงที่ .
หน้าจอที่มีการเปลี่ยนแปลงข้อมูลภายในตัวแอปพลิเคชัน (Mutable State) ซึ่งเมื่อมีการเปลี่ยนแปลงข้อมูลใด ๆ ระบบจะทำการวาดหน้าจอใหม่ (Rebuild) ทันที .
Flutter ทำงานอยู่บนแนวคิดเชิงประกาศ (Declarative UI) ซึ่งหมายความว่าระบบจะทำงานโดยการสะท้อนสถานะ (State) ล่าสุดออกมาเป็นภาพอินเตอร์เฟซบนหน้าจอ .
เมื่อสถานะหรือชุดข้อมูลหลักมีการอัปเดตเกิดขึ้น ตัวแอปพลิเคชันจะกระตุ้นคำสั่งการ Rebuild หน้าจอส่วนที่เกี่ยวข้องทั้งหมดขึ้นมาทดแทนส่วนเดิม .
สมการที่เป็นหัวใจหลักในการวาดหน้าจอของ Flutter ถูกนิยามไว้คือ:
นอกจากนี้ การออกแบบให้เลเยอร์ตัวกลางส่งผ่านสถานะที่เป็นข้อมูลประเภทที่ไม่สามารถเปลี่ยนแปลงค่าได้ (Immutable State) จะช่วยลดโอกาสเกิดบั๊กและปัญหาการแก้ไขข้อมูลที่ผิดรูปในระบบลงได้ .
การใช้งานฟังก์ชัน setState เป็นกลไกเริ่มต้นและง่ายที่สุดในการบริหารจัดการสถานะชั่วคราว (Ephemeral State) ที่ทำงานอยู่เฉพาะตัวแปรภายในหน้าจอคอมโพเนนต์เดี่ยว .
เมื่อคำสั่งถูกเรียกใช้เพื่อแก้ไขสถานะหรือค่าพารามิเตอร์ภายใน ตัวแอปพลิเคชันจะกระตุ้นคำสั่งการ Rebuild โดยสั่งรันเมธอด build ของ Widget นั้น ๆ อีกครั้งทันที .
ข้อจำกัดสำคัญ: เมื่อแอปพลิเคชันมีขนาดใหญ่และเริ่มแบ่งเป็นโครงสร้างหน้าจอซ้อนกันลึก ๆ การนำสถานะภายในไปแบ่งปันข้อมูลให้กับ Widgets ตัวอื่นที่อยู่ภายนอกจะทำได้ยากและทำให้โค้ดพันกันอย่างยุ่งเหยิง .
เมื่อขนาดของโปรเจกต์เติบโตขึ้นเรื่อย ๆ การพัฒนาโดยปราศจากการควบคุมย่อมก่อให้เกิดผลเสียอย่างรุนแรง การนำ **สถาปัตยกรรม (Architecture)** มาประยุกต์ใช้งานจะช่วยป้องกันการเขียนโค้ดที่รกรุงรังและช่วยเพิ่มคุณภาพของโปรแกรมในแง่มุมต่าง ๆ :
ช่วยกำหนดขอบเขตและหลักการแยกส่วนงาน (Separation of Concerns) ทำให้ทีมพัฒนาสามารถเขียนและแก้ไขระบบไปพร้อมกันได้โดยไม่เกิดปัญหาโค้ดชนกัน (Code Conflicts) .
แอปพลิเคชันจะมีความยืดหยุ่นสูง การเพิ่มความสามารถหรือฟีเจอร์ใหม่ ๆ เข้าไปภายหลังจะทำได้สะดวก รวดเร็ว และไม่มีความเสี่ยงที่จะพังระบบประมวลผลภายในส่วนอื่น ๆ .
เพิ่มขีดจำกัดในการวางชุดทดสอบ (Unit & Widget Tests) เพราะการจัดข้อมูลเป็นระบบชั้นทำให้คุณสามารถใช้ "ข้อมูลจำลอง" ทดสอบลอจิกแต่ละส่วนได้ทันทีโดยไม่ต้องเชื่อมต่อข้อมูลเน็ตเวิร์กของจริง .
การเขียนโค้ดที่ดีต้องแยกความรับผิดชอบออกจากกันอย่างชัดเจน โดยการแบ่งแอปพลิเคชันออกเป็น ชั้นเลเยอร์ (Layered Architecture) ซึ่งแต่ละเลเยอร์จะมีหน้าที่และขอบเขตการทำงานเฉพาะตัวที่ชัดเจน เพื่อไม่ให้ลอจิกของหน้าจอปนเปกับข้อมูลดิบ
เพื่อป้องกันไม่ให้ข้อมูลสถานะของแอปเกิดความซ้ำซ้อนหรือมีค่าไม่ตรงกัน สถาปัตยกรรมที่ถูกต้องจะต้องยึดหลักการจัดเก็บสถานะไว้ที่ "แหล่งความจริงที่หนึ่งเดียว" (Single Source of Truth - SSOT) ซึ่งสำหรับแอป Flutter แหล่งความจริงนี้จะกักเก็บอยู่ที่เลเยอร์ข้อมูล (Data Layer - Repositories) เสมอ
แอปพลิเคชัน Compass ออกแบบตามโครงสร้าง Model-View-ViewModel (MVVM) ของผู้พัฒนา Flutter เพื่อสาธิตการพัฒนาในระดับที่ใช้งานได้จริง (Production-Grade):
คือคลาสวิดเจ็ตต่างๆ ใน Flutter (เช่น HomeScreen) ทำหน้าที่เป็นผู้รับข้อมูลแสดงผลจาก ViewModel เพียงอย่างเดียว และเชื่อมผู้ใช้เข้ากับระบบด้วย UI ปราศจากการคำนวณทางตรรกะ
คือคลาสจัดการสถานะหน้าจอ เช่น HomeViewModel คอยแปลงข้อมูลดิบจากคลังเก็บให้อยู่ในสภาพพร้อมระบายบนจอ และเปิดคำสั่ง "Commands" ให้กับเลเยอร์ View เรียกใช้งาน
คือคลาส Repositories และ Services ทำหน้าที่ดูแลฐานข้อมูลทริปการท่องเที่ยว เชื่อม API ดิบ และแปลงให้เป็นวัตถุของขอบเขตงานจริง (Domain Models) เพื่อเป็นแหล่งข้อมูลที่เชื่อถือได้แหล่งเดียว
lib/
├── data/ // จัดระเบียบตาม "ประเภทข้อมูล" (ไม่จำกัดฟีเจอร์)
├── model/ # ApiClient Models (e.g. UserApiModel)
├── repositories/ # (e.g. booking_repository_remote.dart)
└── services/ # Stateless Network clients (e.g. api_client.dart)
├── domain/ // สำหรับประเภทข้อมูลส่วนกลางของระบบแอป
└── models/ # Pure Domain Models (e.g. Booking, User)
├── ui/ // จัดระเบียบตาม "ฟีเจอร์การใช้งาน"
├── core/ # Shared Widgets & Brand Themes
└── <feature_name>/ # (e.g. home/ หรือ auth/)
├── view_models/ # (e.g. home_viewmodel.dart)
└── widgets/ # (e.g. home_screen.dart, logout_button.dart)
└── main.dart / main_development.dart / main_staging.dart
แอปพลิเคชันระดับใหญ่จะผสมผสานรูปแบบการจัดโครงสร้างแฟ้มข้อมูลสองประเภทเข้าด้วยกันอย่างเหมาะสม :
data/ เพราะคลังเก็บ (Repository) และ Services ต่างๆ ต้องพร้อมใช้งานแบบแบ่งปันกันข้ามไปยังหลายๆ ฟีเจอร์ทั่วแอปพลิเคชันได้
ui/ เพราะชุดแสดงผล View และตัวจัดการสถานะ ViewModel มักจะทำงานสอดรับเป็นคู่แบบจำเพาะเจาะจงกันเสมอ
เพื่อรักษาหลักการแยกความรับผิดชอบ (Separation of Concerns) ไม่ให้โค้ดพันกันจนส่งผลเสียต่อการตรวจสอบและการบำรุงรักษา ทุกเลเยอร์ต้องเคารพกฎความสัมพันธ์ (Rules of Engagement) อย่างเคร่งครัด:
| คอมโพเนนต์ | กฎการเชื่อมโยงข้อมูล (Rules of Engagement) |
|---|---|
| View | จะรู้จักและติดต่อผ่านทาง ViewModel ของตัวเองเพียงแค่ตัวเดียวเท่านั้น และต้องไม่ข้ามเลเยอร์ไปรู้จักหรือเรียกข้อมูลจาก Repository หรือ Service โดยตรงเด็ดขาด |
| ViewModel | เชื่อมต่อและสื่อสารเข้ากับ Repository ตั้งแต่ 1 ตัวขึ้นไป ผ่านทางคอนสตรักเตอร์ของตัวเอง แต่ตัวมันจะไม่มีวันรู้จักกับเลเยอร์ View ที่คอยอ่านสถานะของมันอยู่ |
| Repository | ควบคุมและทำงานประสานกับ Service ต่างๆ ในการแปลงข้อมูลดิบ และถูกเรียกใช้งานต่อโดย ViewModels หลายๆ ตัวได้ โดยที่ตัวมันไม่มีสิทธิ์รู้ตัวตนของ ViewModels เลย |
| Service | เป็นคลาสไร้สถานะ (Stateless) ที่มีบทบาทห่อหุ้ม API ภายนอก โดยทำงานรับใช้คลังเก็บข้อมูล และ ไม่มีวันรับรู้ความมีอยู่ของเลเยอร์อื่นใดเลย |
วิดเจ็ตในเลเยอร์นี้ (เช่น StatelessWidget) จะประหยัดพลังงานหน่วยความจำ และ ไม่มีลอจิกทางธุรกิจอยู่ภายในเลย โดยมีหน้าที่ดึงข้อมูลสถานะจาก ViewModel มาระบายวาดลงหน้าจอผ่านทางตัวสร้างสัญญาณรีเฟรชเฉพาะส่วน ListenableBuilder
ตรรกะที่อนุญาตให้มีได้ในหน้าจอนี้ มีเพียงแค่การสั่งเปิด-ปิดแอนิเมชัน หรือการจัดเลย์เอาต์ตามขนาดหน้าจอและทิศทางการหมุนของเครื่องโทรศัพท์เท่านั้น
เป็นคลาสจัดสรรตรรกะของหน้าจอ โดยมีการสืบทอดความสามารถมาจาก ChangeNotifier คอยประกาศฟังก์ชัน Command Objects (เช่น Command0 และ Command1) เพื่อให้หน้าจอ Views สั่งงานได้อย่างปลอดภัยไร้บั๊ก
มีหน้าที่คอยจับตาเฝ้าดูความเปลี่ยนแปลงข้อมูลสถานะ เมื่อระบบดึงข้อมูลสำเร็จเสร็จสิ้น จะเรียกใช้คำสั่ง notifyListeners() เพื่อส่งผ่านการ Rebuild ชิ้นส่วนหน้าจอโดยอัตโนมัติ
คลังข้อมูลหลักที่เป็นแหล่งความจริงหนึ่งเดียวในการเก็บและแปลงข้อมูลทริปเดินทาง มีบทบาทสำคัญในการควบคุมและจัดเก็บลอจิกการอัปเดตข้อมูล การทำ Caching ระบบการจัดสรรข้อผิดพลาด ตลอดจนการทำซ้ำคำสั่งที่ล้มเหลว (Retry Logic)
จะประมวลผลดึงค่า JSON จาก Services และขัดเกลากรองข้อมูลดิบให้กลายเป็น "Domain Models" ที่สมบูรณ์และปลอดภัย เพื่อส่งผ่านต่อไปให้หลายๆ ViewModels ในระบบเรียกใช้งาน
คลาสทำงานที่เป็นศูนย์รวมชุดคำสั่ง API เชื่อมโยงข้อมูลเน็ตเวิร์ก มีหน้าต่างรับผิดชอบเพียงขอบเขตเดียวคือการติดต่อเพื่อดึงหรือบันทึกข้อมูลดิบกลับไปยังเครื่องฐานข้อมูลปลายทาง (เช่น Django REST Framework Backend)
ทำงานเป็นแบบไร้สถานะ (Stateless) และให้คลาส Repositories ปรากฏตัวในคอนสตรักเตอร์แบบเป็นส่วนตัว (Private) เพื่อส่งข้อมูลดิบกลับขึ้นไปประมวลผลต่อตามลำดับชั้นอย่างมีระบบระเบียบและปลอดภัยสูง
ในหมวดเรียนนี้จะเป็นการลงมือเขียนโค้ดและพิมพ์คำสั่งควบคุมระบบจริงแบบทีละขั้นตอน (Step-by-Step) ขอให้นักศึกษาจัดเตรียมเครื่องคอมพิวเตอร์และตรวจสอบสัญญาณอินเทอร์เน็ตให้เรียบร้อยก่อนเริ่มต้นทำตาม
ก่อนเริ่มต้นเขียนโครงสร้างโปรเจกต์ใหม่ นักศึกษาทุกคนจำเป็นต้องตรวจสอบระบบและสถานะของซอฟต์แวร์สนับสนุนบนระบบเครื่อง เพื่อให้แน่ใจว่าเครื่องมือสำหรับการรันและทดสอบระบบจำลอง (Emulators) ทำงานได้สมบูรณ์:
# เปิดโปรแกรม Terminal หรือ Command Prompt แล้วพิมพ์คำสั่งด้านล่าง:
$ flutter doctor
สร้างโฟลเดอร์สำหรับพัฒนาแอปพลิเคชัน Compass ของเราขึ้นมาใหม่ และติดตั้งกลุ่มไลบรารีเสริมที่จำเป็นสำหรับการเชื่อมโยงเน็ตเวิร์ก (Dio), ระบบจัดการสถานะ (Provider), และการสร้างความปลอดภัยให้โทเค็น (Secure Storage):
# สร้างโปรเจกต์ใหม่และย้ายหน้าต่างการทำงานเข้าสู่ไดเรกทอรีโปรเจกต์:
$ flutter create compass_booking_app
$ cd compass_booking_app
# นำเข้าปลั๊กอินสำหรับเชื่อมต่อ REST API และจัดเก็บข้อมูลอย่างปลอดภัย:
$ flutter pub add dio provider flutter_secure_storage
เพื่อแยกความรับผิดชอบของโค้ดให้สอดคล้องตามข้อกำหนดของทีมผู้พัฒนา Flutter และหลักการกักเก็บความจริงหนึ่งเดียว (SSOT) ให้นักศึกษาสร้างโฟลเดอร์ย่อยภายใต้ไดเรกทอรี lib/ ดังนี้:
# รันคำสั่งต่อไปนี้เพื่อสร้างโครงสร้างโฟลเดอร์พร้อมกัน:
# สำหรับระบบปฏิบัติการ Unix/macOS:
$ mkdir -p lib/data/models lib/data/repositories lib/data/services
$ mkdir -p lib/domain/models
$ mkdir -p lib/ui/core lib/ui/home/view_models lib/ui/home/widgets
# สำหรับ Windows PowerShell:
$ mkdir lib\data\models, lib\data\repositories, lib\data\services
$ mkdir lib\domain\models
$ mkdir lib\ui\core, lib\ui\home\view_models, lib\ui\home\widgets
data/ ตามชนิดช่วยให้คลาสเรียกใช้ API สามารถแชร์การทำงานกันได้ง่ายข้ามหลายส่วนui/ แยกตามฟีเจอร์ ช่วยลดการทำงานชนกันของโค้ดเมื่อทีมขยายใหญ่ขึ้นเพื่อเพิ่มประสิทธิภาพและตรวจสอบพฤติกรรมการพิมพ์โค้ดของแอปพลิเคชันให้เป็นสากล เราจะทำการติดตั้งระบบ **Flutter Agent Plugins** และเชื่อมต่อระบบ IDE เข้ากับ **Dart and Flutter MCP Server** อย่างมีประสิทธิภาพ:
# เปิดใช้งานร่วมกับ Claude Code (ผ่าน Terminal):
$ claude plugin marketplace add flutter/agent-plugins
$ claude plugin install dart-flutter@dart-flutter
เครื่องมือ AI จะได้รับพิมพ์เขียวระดับสูง สามารถใช้อุปกรณ์ประมวลผลจำลองตรวจสอบข้อผิดพลาดบนหน้าจอวิดเจ็ตแบบเรียลไทม์ และช่วยหาแพ็กเกจที่ปลอดภัยจากเว็บไซต์ผับเดฟได้ทันที
ให้นักศึกษาทำการสร้างไฟล์แบบจำลองประเภทข้อมูล lib/domain/models/booking.dart เพื่อกำหนดโครงสร้างจำลองข้อมูลของทริปสำหรับการท่องเที่ยวแบบเบื้องต้น:
// lib/domain/models/booking.dart
class BookingSummary {
final int id;
final String destinationName;
final DateTime startDate;
final DateTime endDate;
final double price;
BookingSummary({
required this.id,
required this.destinationName,
required this.startDate,
required this.endDate,
required this.price,
});
}
สร้างโครงสร้างคลาสจำลองในเลเยอร์เน็ตเวิร์กไว้ชั่วคราว เพื่อเตรียมการเชื่อมต่อและดึงข้อมูลดิบจาก API หลังบ้านตัวจริงของ Django REST Framework ในการเรียนสัปดาห์หน้า
ในวันนี้เราได้เข้าใจสถาปัตยกรรมแอปพลิเคชันระดับโปรดักชัน คุ้นเคยกับแนวคิด **MVVM, SSOT และ UDF** และสามารถตั้งโครงร่างแฟ้มโฟลเดอร์สำหรับพัฒนา Compass App ได้เสร็จสมบูรณ์แล้ว