Dependency Injection (DI) คือวิธีการส่งมอบวัตถุที่คลาสต้องการ (Dependencies) ผ่านทางคอนสตรักเตอร์ (Constructor Injection) แทนที่จะให้คลาสนั้นสร้างขึ้นเอง ตัวอย่างเช่น การฉีด Service เข้าสู่ Repository ซึ่งช่วยให้การจำลองเทส (Mocking) ทำได้ง่ายขึ้นมาก
ทีมงานของ Google แนะนำให้ใช้แพ็กเกจ provider ในการทำระบบ Dependency Injection ในระดับโครงสร้างโปรเจกต์ของ Flutter
วัตถุจากเลเยอร์จัดเก็บข้อมูล (Services และ Repositories) ทั้งหมดจะถูกผูกขึ้นระดับบนสุดของแอปด้วย MultiProvider เพื่อให้สามารถเข้าถึงและฉีดส่งกันผ่าน BuildContext.read() ได้อย่างสะดวกรวดเร็ว
runApp(
MultiProvider(
providers: [
Provider(create: (context) => ApiClient()),
ChangeNotifierProvider(
create: (context) => AuthRepositoryRemote(
apiClient: context.read(), // ฉีด API Client เข้าไป
) as AuthRepository,
),
Provider(
create: (context) => BookingRepositoryRemote(
apiClient: context.read(),
) as BookingRepository,
),
],
child: const MainApp(),
),
);
ในการเปิดหน้าจอระบบ (Screen routes) การฉีดข้อมูลจะทำในเลเยอร์การนำทาง (Routing) เช่นในคลาสตั้งค่าเราเตอร์ go_router
🔒 หลักการจัดเก็บข้อมูลให้ปลอดภัย:
การส่งผ่าน Repository เข้าสู่ ViewModel จะเก็บไว้เป็นตัวแปรแบบส่วนตัว (Private ตัวอย่างเช่น _bookingRepository) เพื่อป้องกันไม่ให้เลเยอร์ View เข้ามาดักใช้งานคลังข้อมูลและจัดการเปลี่ยนแปลงสถานะระบบโดยตรง
GoRoute(
path: Routes.home,
builder: (context, state) {
return HomeScreen(
viewModel: HomeViewModel(
bookingRepository: context.read(), // ดึงจากด้านบน
),
);
},
);
class HomeViewModel extends ChangeNotifier {
HomeViewModel({required BookingRepository bookingRepository})
: _bookingRepository = bookingRepository; // Private Assign
final BookingRepository _bookingRepository; // ซ่อนตัวแปร
}
จุดเด่นที่สำคัญที่สุดอย่างหนึ่งของการจัดโครงสร้างแอปแบบ MVVM (Model-View-ViewModel) คือการเพิ่มขีดความสามารถในการทดสอบระบบได้อย่างมีคุณภาพ (Testability)
การแยกตรรกะทางธุรกิจ (Business Logic) ออกจากวิดเจ็ตแสดงผล (View) ช่วยให้เราสามารถจำลองสิ่งแวดล้อมเพื่อเขียนคำสั่งตรวจสอบความถูกต้อง (Unit Test) บน ViewModel ได้โดยตรงโดยไม่ต้องเปิดหน้าจอ UI หรือเชื่อมต่ออินเทอร์เน็ตจริง
ทดสอบตรรกะและการเปลี่ยนสถานะข้อมูลผ่านคลาสจำลอง (Fakes) โดยไม่ต้องต่อ Backend จริง
ทดสอบการวาดส่วนแสดงผลและการโต้ตอบปุ่มต่าง ๆ บนหน้าจอผ่านการป้อนข้อมูลจำลอง
ใน Data Layer ของ Compass App มีการสืบทอดคลาส BookingRepository เป็นแบบ BookingRepositoryRemote สำหรับใช้งานจริง และมีคลาสย่อยจำลองสำหรับการพัฒนาและเขียนเทส
คลาส Fake (Fake Repository): ทำงานเหมือนของจริงทุกประการแต่เก็บข้อมูลชั่วคราวอยู่ในหน่วยความจำ (In-memory List) แทนการส่งคำขอดึงข้อมูลผ่านเน็ตเวิร์กจริง ทำให้ทดสอบระบบได้เสถียรและเร็วขึ้นมาก
ในการทดสอบ HomeViewModel เราจะเขียนกลุ่มทดสอบ (Group test) เพื่อทดสอบการโหลดรายการจองข้อมูลสำเร็จ
FakeBookingRepository() และใส่ข้อมูลตั้งต้นด้วย ..createBooking(kBooking) FakeUserRepository() เข้าสู่ ViewModel ผ่านคอนสตรักเตอร์ expect(viewModel.bookings.isNotEmpty, true) เพื่อยืนยันว่า ViewModel ทำการประมวลผลข้อมูลถูกต้องทันที void main() {
group('HomeViewModel tests', () {
test('Load bookings', () {
// 1. จัดเตรียมคลาสจำลองและส่งเข้า ViewModel
final viewModel = HomeViewModel(
bookingRepository: FakeBookingRepository()
..createBooking(kBooking),
userRepository: FakeUserRepository(),
);
// 2. ยืนยันว่าข้อมูลได้รับการดึงเข้า ViewModel สำเร็จ
expect(viewModel.bookings.isNotEmpty, true);
});
});
}
ในการทดสอบหน้าจอ HomeScreen (Widget Test) เราต้องแน่ใจว่าหน้าจอสามารถโหลดข้อมูลขึ้นมาแสดงผลได้ถูกต้องและปุ่มนำทางสามารถทำงานได้ครบถ้วน
💡 การจัดส่งคลังข้อมูลระดับวิดเจ็ต:
เราจะใช้ระบบส่งผ่านข้อมูลในฟังก์ชันช่วยเหลือ loadWidget โดยจำลองเราเตอร์ MockGoRouter ผ่านโมดูล mocktail ร่วมกับการผูก provider คลาสปลอม เพื่อตรวจจับการเคลื่อนย้ายหน้าจอของผู้เรียน
void loadWidget(WidgetTester tester) async {
await testApp(
tester,
ChangeNotifierProvider.value(
value: FakeAuthRepository() as AuthRepository,
child: Provider.value(
value: FakeItineraryConfigRepository() as ItineraryConfigRepository,
child: HomeScreen(viewModel: viewModel), // วาด HomeScreen
),
),
goRouter: goRouter, // ใช้เราเตอร์จำลอง
);
}
ในการสาธิตระบบ (Live Demo) ผู้เรียนต้องทำการรัน **Flutter Frontend** ร่วมกับการดึงข้อมูลผ่านเครือข่ายจริงจาก **Django REST Framework (DRF) Backend**
# คำสั่งเตรียมเดโม (เปิด 2 terminals)
$ cd oidc-backend && uv run manage.py runserver
$ flutter run -d chrome --web-port 50000
# ตรวจความพร้อมของ OIDC server ก่อนขึ้นเดโม
$ http :8000/.well-known/openid-configuration
การจัดทำเกณฑ์การวิพากษ์แนวทางการแก้ปัญหาทางสถาปัตยกรรม (Architectural Peer Critique) ช่วยกระตุ้นให้ผู้เรียนเข้าใจลึกซึ้งถึงหลักการ **Separation of Concerns**
ผู้เรียนจะร่วมกันวิเคราะห์และตั้งคำถามว่า เลเยอร์นำเสนอ (View) มีตรรกะทางธุรกิจปะปนอยู่หรือไม่? หรือตัวแปรสำคัญภายในระบบจำพวกการเปลี่ยนแปลงสถานะถูกจัดเก็บและเข้าถึงอย่างรัดกุมปลอดภัยหรือไม่?
View Isolation: วิดเจ็ตหน้าจอไม่มีการเรียกเชื่อมต่อเครือข่ายหรือเรียกใช้ Service โดยตรงโดยไม่ผ่าน ViewModel
Encapsulation (Private Members): คลังข้อมูลและออบเจ็กต์สำคัญต่าง ๆ ถูกซ่อนไว้เป็นตัวแปรแบบ Private เพื่อไม่ให้ View เข้าถึงโดยตรง
Error Safety: มีการจัดการข้อผิดพลาดผ่านคลาสครอบคลุมผลลัพธ์ (Result Class) อย่างเป็นระบบในเลเยอร์ข้อมูล
สถาปัตยกรรม **MVVM (Model-View-ViewModel)** ของ Google ไม่ใช่เพียงกฎเกณฑ์ในการวางระเบียบไฟล์ แต่ช่วยแก้ปัญหาระยะยาวในการทำแอปพลิเคชันเชิงพานิชย์อย่างแท้จริง