Dependency Injection (DI) คือวิธีการส่งมอบวัตถุที่คลาสต้องการ (Dependencies) ผ่านทางคอนสตรักเตอร์ (Constructor Injection) แทนที่จะให้คลาสนั้นสร้างขึ้นเอง ตัวอย่างเช่น การฉีด Service เข้าสู่ Repository ซึ่งช่วยให้การจำลองเทส (Mocking) ทำได้ง่ายขึ้นมาก
class BookingRepositoryRemote implements BookingRepository {
BookingRepositoryRemote({required ApiClient apiClient})
: _apiClient = apiClient; // ไม่สร้างเอง — รับจากภายนอก
final ApiClient _apiClient;
}
ทีมงานของ Google แนะนำให้ใช้แพ็กเกจ provider ในการทำระบบ Dependency Injection ในระดับโครงสร้างโปรเจกต์ของ Flutter
วัตถุจากเลเยอร์จัดเก็บข้อมูล (Services และ Repositories) ทั้งหมดจะถูกผูกขึ้นระดับบนสุดของแอปด้วย MultiProvider เพื่อให้สามารถเข้าถึงและฉีดส่งกันผ่าน BuildContext.read() ได้อย่างสะดวกรวดเร็ว
provider ลงใน pubspec.yaml แล้วรัน flutter pub getApiClient() หนึ่งอินสแตนซ์ใน Provider เพื่อให้ทุกเลเยอร์ใช้ตัวเดียวกันcontext.read<ApiClient>() ฉีดสืบต่อไปยัง Repository ต่าง ๆChangeNotifierProvider เฉพาะ ViewModel ที่หน้าจอต้องฟังการแจ้งเตือนเปลี่ยนแปลงสถานะ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 ไม่ใช่เพียงกฎเกณฑ์ในการวางระเบียบไฟล์ แต่ช่วยแก้ปัญหาระยะยาวในการทำแอปพลิเคชันเชิงพานิชย์อย่างแท้จริง