แผนการเรียนรู้
/
Flutter Mobile Dev | Week 5 • Part 1

สัปดาห์ที่ 5: ระบบ Dependency Injection (DI) & การต่อวงจรรวมระบบ

🎯 สารบัญโครงสร้างเนื้อหาหมวดปฏิบัติการที่ 1 (DI Assembly)

⚙️ แนวคิดและการต่อสาย
⛓️ เลเยอร์ระดับบน (Root)
🛡️ เลเยอร์นำเสนอ (UI)
💡 คำชี้แนะ: คลิกหัวข้อบนสารบัญเพื่อข้ามไปยังหน้านั้นโดยตรง หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูตามลำดับการสอน
Week 5 • Slide 1 หัวข้อ: DI Concepts

แนวคิด Dependency Injection และกฎข้อตกลงการติดต่อ (Rules of Engagement)

Dependency Injection (DI) คือวิธีการส่งมอบวัตถุที่คลาสต้องการ (Dependencies) ผ่านทางคอนสตรักเตอร์ (Constructor Injection) แทนที่จะให้คลาสนั้นสร้างขึ้นเอง ตัวอย่างเช่น การฉีด Service เข้าสู่ Repository ซึ่งช่วยให้การจำลองเทส (Mocking) ทำได้ง่ายขึ้นมาก

💡 กฎการเข้าถึงเลเยอร์ (Rules of Engagement)
  • View จะต้องมองเห็นและรู้จักแค่ ViewModel ของตนเองตัวเดียวเท่านั้น
  • ViewModel รับ Repositories ผ่านคอนสตรักเตอร์ (โดยไม่รู้จัก View)
  • Repository รับ Services เข้ามาทางคอนสตรักเตอร์เพื่อดึงข้อมูลดิบ
  • Service ทำงานเป็นเครื่องมือ Stateless API Client และไม่รู้จักใครข้างบน
ผังลำดับการจัดส่งและฉีดใช้งาน (Dependency Injection Flow)
Stateless Service (e.g., ApiClient)
⬇️ Injected into
Repository (e.g., BookingRepositoryRemote)
⬇️ Injected into
ViewModel (e.g., HomeViewModel)
⬇️ Provided to
View (UI) (e.g., HomeScreen Screen)
Week 5 • Slide 2 หัวข้อ: Root-level DI

การผูก Dependency เลเยอร์สูงสุดด้วย MultiProvider

ทีมงานของ Google แนะนำให้ใช้แพ็กเกจ provider ในการทำระบบ Dependency Injection ในระดับโครงสร้างโปรเจกต์ของ Flutter

วัตถุจากเลเยอร์จัดเก็บข้อมูล (Services และ Repositories) ทั้งหมดจะถูกผูกขึ้นระดับบนสุดของแอปด้วย MultiProvider เพื่อให้สามารถเข้าถึงและฉีดส่งกันผ่าน BuildContext.read() ได้อย่างสะดวกรวดเร็ว

🔗 แหล่งข้อมูลศึกษาเพิ่มเติม: https://pub.dev/packages/provider
📄 main.dart / dependencies.dart
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(),
                ),
                );
Week 5 • Slide 3 หัวข้อ: Screen-level DI & Security

การฉีดข้อมูลระดับเราเตอร์ & การซ่อนคุณสมบัติเป็น Private

ในการเปิดหน้าจอระบบ (Screen routes) การฉีดข้อมูลจะทำในเลเยอร์การนำทาง (Routing) เช่นในคลาสตั้งค่าเราเตอร์ go_router

🔒 หลักการจัดเก็บข้อมูลให้ปลอดภัย: การส่งผ่าน Repository เข้าสู่ ViewModel จะเก็บไว้เป็นตัวแปรแบบส่วนตัว (Private ตัวอย่างเช่น _bookingRepository) เพื่อป้องกันไม่ให้เลเยอร์ View เข้ามาดักใช้งานคลังข้อมูลและจัดการเปลี่ยนแปลงสถานะระบบโดยตรง

🔗 แหล่งข้อมูลอ้างอิงและโค้ดตัวอย่าง:
เราเตอร์แพ็กเกจ: https://pub.dev/packages/go_router
โค้ดหลักแอปพลิเคชัน: https://github.com/flutter/samples/tree/main/compass_app
📄 router.dart (ฉีด Repository เข้าสู่หน้าจอ)
GoRoute(
                  path: Routes.home,
                  builder: (context, state) {
                  return HomeScreen(
                  viewModel: HomeViewModel(
                  bookingRepository: context.read(), // ดึงจากด้านบน
                  ),
                  );
                  },
                  );
📄 home_viewmodel.dart (ซ่อนเป็น Private)
class HomeViewModel extends ChangeNotifier {
                  HomeViewModel({required BookingRepository bookingRepository})
                  : _bookingRepository = bookingRepository; // Private Assign

                  final BookingRepository _bookingRepository; // ซ่อนตัวแปร
                  }
Week 5 • Slide 4 หัวข้อ: Testability in MVVM

สถาปัตยกรรมที่ทดสอบง่าย (Testability) และแนวคิดการแยกทดสอบแต่ละเลเยอร์

จุดเด่นที่สำคัญที่สุดอย่างหนึ่งของการจัดโครงสร้างแอปแบบ MVVM (Model-View-ViewModel) คือการเพิ่มขีดความสามารถในการทดสอบระบบได้อย่างมีคุณภาพ (Testability)

การแยกตรรกะทางธุรกิจ (Business Logic) ออกจากวิดเจ็ตแสดงผล (View) ช่วยให้เราสามารถจำลองสิ่งแวดล้อมเพื่อเขียนคำสั่งตรวจสอบความถูกต้อง (Unit Test) บน ViewModel ได้โดยตรงโดยไม่ต้องเปิดหน้าจอ UI หรือเชื่อมต่ออินเทอร์เน็ตจริง

🔗 เอกสารศึกษาแนวทางการทดสอบแอป: https://docs.flutter.dev/app-architecture/case-study/testing
การทดสอบแบบแยกส่วน (Isolated Testing)
⚙️ ViewModel Unit Test

ทดสอบตรรกะและการเปลี่ยนสถานะข้อมูลผ่านคลาสจำลอง (Fakes) โดยไม่ต้องต่อ Backend จริง

🖼️ View Widget Test

ทดสอบการวาดส่วนแสดงผลและการโต้ตอบปุ่มต่าง ๆ บนหน้าจอผ่านการป้อนข้อมูลจำลอง

Week 5 • Slide 5 หัวข้อ: Fake Repository

การสร้างและใช้งาน Fake Repository เพื่อทดสอบข้อมูลจำลอง

ใน Data Layer ของ Compass App มีการสืบทอดคลาส BookingRepository เป็นแบบ BookingRepositoryRemote สำหรับใช้งานจริง และมีคลาสย่อยจำลองสำหรับการพัฒนาและเขียนเทส

คลาส Fake (Fake Repository): ทำงานเหมือนของจริงทุกประการแต่เก็บข้อมูลชั่วคราวอยู่ในหน่วยความจำ (In-memory List) แทนการส่งคำขอดึงข้อมูลผ่านเน็ตเวิร์กจริง ทำให้ทดสอบระบบได้เสถียรและเร็วขึ้นมาก

🔗 คลาส Fake ตัวอย่างบน GitHub Repository: https://github.com/flutter/samples/blob/main/compass_app/.../fake_booking_repository.dart
🎯 แผนภาพการทำซ้ำ (Testing flow with Fake)
1. คลาสการทดสอบ (Test File)
ป้อนข้อมูลจำลอง kBooking ไปที่คลังข้อมูลปลอม
⬇️ เรียกใช้งานฟังก์ชัน
2. FakeBookingRepository (In-Memory)
เก็บรวบรวมข้อมูลลงใน List<Booking> _bookings ภายในแรม
⬇️ ฉีดส่งให้
3. HomeViewModel (System Under Test)
ดึงข้อมูลมาประมวลผล เพื่อตรวจสอบการเปลี่ยนสถานะของ UI
Week 5 • Slide 6 หัวข้อ: ViewModel Unit Test

ปฏิบัติการเขียน Unit Test ของ ViewModel ร่วมกับ Fake Repository

ในการทดสอบ HomeViewModel เราจะเขียนกลุ่มทดสอบ (Group test) เพื่อทดสอบการโหลดรายการจองข้อมูลสำเร็จ

📋 ลำดับการทำงานในเทสเคส (Test Case Sequence):
  • สร้างข้อมูลปลอมขึ้นมาด้วย FakeBookingRepository() และใส่ข้อมูลตั้งต้นด้วย ..createBooking(kBooking)
  • ส่งคลังปลอมและ FakeUserRepository() เข้าสู่ ViewModel ผ่านคอนสตรักเตอร์
  • ตรวจสอบและยืนยันผลการโหลดด้วยคำสั่ง expect(viewModel.bookings.isNotEmpty, true) เพื่อยืนยันว่า ViewModel ทำการประมวลผลข้อมูลถูกต้องทันที
🔗 ศึกษา Unit Test เพิ่มเติมได้ที่: https://docs.flutter.dev/app-architecture/case-study/testing#testing-the-ui-layer
📄 home_screen_test.dart (Unit Test)
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);
                });
                });
                }
Week 5 • Slide 7 หัวข้อ: View Widget Test

การตรวจสอบสิทธิ์หน้าจอด้วย Widget Test และการจำลองเราเตอร์

ในการทดสอบหน้าจอ HomeScreen (Widget Test) เราต้องแน่ใจว่าหน้าจอสามารถโหลดข้อมูลขึ้นมาแสดงผลได้ถูกต้องและปุ่มนำทางสามารถทำงานได้ครบถ้วน

💡 การจัดส่งคลังข้อมูลระดับวิดเจ็ต: เราจะใช้ระบบส่งผ่านข้อมูลในฟังก์ชันช่วยเหลือ loadWidget โดยจำลองเราเตอร์ MockGoRouter ผ่านโมดูล mocktail ร่วมกับการผูก provider คลาสปลอม เพื่อตรวจจับการเคลื่อนย้ายหน้าจอของผู้เรียน

🔗 วิธีเขียน Mock & Fakes เพิ่มเติมได้ที่: https://docs.flutter.dev/testing/overview
📄 home_screen_test.dart (Widget Test Setup)
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, // ใช้เราเตอร์จำลอง
                  );
                  }
Flutter Mobile Dev | Week 5 • Part 3

สัปดาห์ที่ 5: นำเสนอโครงงาน และบทสรุปสถาปัตยกรรม MVVM

🎯 สารบัญโครงสร้างเนื้อหาหมวดนำเสนอและบทสรุป (Module 3)

📢 นำเสนอโครงงาน
🕵️ การวิเคราะห์สถาปัตยกรรม
🏛️ สรุปบทเรียน
💡 คำชี้แนะ: คลิกหัวข้อบนสารบัญเพื่อข้ามไปยังหน้านั้นโดยตรง หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูตามลำดับการสอน
Week 5 • Slide 9 หัวข้อ: Project Demo

การนำเสนอโครงงานและการสาธิตระบบการเชื่อมต่อแบบสด (Live Demo)

ในการสาธิตระบบ (Live Demo) ผู้เรียนต้องทำการรัน **Flutter Frontend** ร่วมกับการดึงข้อมูลผ่านเครือข่ายจริงจาก **Django REST Framework (DRF) Backend**

📋 สิ่งสำคัญที่ต้องสาธิตในการตรวจผลงาน (Key Features to Demo)
  • Authentication Flow: การล็อกอินผ่าน OIDC Authorization Code Flow (django-oidc-provider จากสัปดาห์ที่ 12) แล้วกลับมาแสดงชื่อผู้ใช้ในแอป
  • Data Integrity: การเรียกแสดงรายการจองทริป และการจัดเตรียมโครงสร้างข้อมูลอย่างรวดเร็ว
  • State Completion: การสั่งลบข้อมูล Booking (เช่นการปัดสไลด์ Dismissible) และการทำงานของคำสั่ง notifyListeners ในระบบ

# คำสั่งเตรียมเดโม (เปิด 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

สถาปัตยกรรมการสื่อสารร่วมกัน (Client-Server Communication)
📱 Flutter Client ApiClient (Dio) + openid_client & Secure Storage
HTTP GET/POST ➡️ Bearer Token ⬅️ JSON Result
🐍 Django Backend django-oidc-provider (OIDC), SQLite DB — รันด้วย uv
🔗 ซอร์สโค้ดต้นแบบในการศึกษาและทดสอบ: https://github.com/flutter/samples/tree/main/compass_app
Week 5 • Slide 10 หัวข้อ: Peer Critique

การวิเคราะห์วิพากษ์ระบบ และเกณฑ์การประเมินคุณภาพซอฟต์แวร์

การจัดทำเกณฑ์การวิพากษ์แนวทางการแก้ปัญหาทางสถาปัตยกรรม (Architectural Peer Critique) ช่วยกระตุ้นให้ผู้เรียนเข้าใจลึกซึ้งถึงหลักการ **Separation of Concerns**

ผู้เรียนจะร่วมกันวิเคราะห์และตั้งคำถามว่า เลเยอร์นำเสนอ (View) มีตรรกะทางธุรกิจปะปนอยู่หรือไม่? หรือตัวแปรสำคัญภายในระบบจำพวกการเปลี่ยนแปลงสถานะถูกจัดเก็บและเข้าถึงอย่างรัดกุมปลอดภัยหรือไม่?

🔗 เอกสารแนวทางการสื่อสารระหว่างเลเยอร์อย่างเป็นทางการ: https://docs.flutter.dev/app-architecture/case-study/dependency-injection
🛡️ ดัชนีชี้วัดคุณภาพทางสถาปัตยกรรม (Code Cleanliness Checklist)

View Isolation: วิดเจ็ตหน้าจอไม่มีการเรียกเชื่อมต่อเครือข่ายหรือเรียกใช้ Service โดยตรงโดยไม่ผ่าน ViewModel

Encapsulation (Private Members): คลังข้อมูลและออบเจ็กต์สำคัญต่าง ๆ ถูกซ่อนไว้เป็นตัวแปรแบบ Private เพื่อไม่ให้ View เข้าถึงโดยตรง

Error Safety: มีการจัดการข้อผิดพลาดผ่านคลาสครอบคลุมผลลัพธ์ (Result Class) อย่างเป็นระบบในเลเยอร์ข้อมูล

Week 5 • Slide 11 หัวข้อ: MVVM Conclusion

สรุปหัวใจหลักและคุณประโยชน์ของสถาปัตยกรรม MVVM

สถาปัตยกรรม **MVVM (Model-View-ViewModel)** ของ Google ไม่ใช่เพียงกฎเกณฑ์ในการวางระเบียบไฟล์ แต่ช่วยแก้ปัญหาระยะยาวในการทำแอปพลิเคชันเชิงพานิชย์อย่างแท้จริง

🌟 ประโยชน์หลัก 3 ด้านในการนำไปประยุกต์ใช้จริง:
  • บำรุงรักษาง่าย (Maintainability): แก้ไขโค้ดตรรกะระบบได้โดยไม่ต้องรื้อวาดหน้าจอ UI ใหม่ทั้งหมด
  • ความสามารถในการขยายระบบ (Extensibility): การเพิ่มหน้าจอ UI ใหม่หรือการเปลี่ยน ViewModel จะไม่ส่งผลต่อตรรกะเดิมของ Data Layer
  • ความเสถียรและความน่าเชื่อถือ (Testability): แยกเขียน Unit Test ปลอมสภาพแวดล้อมได้เร็วขึ้นถึง 10 เท่า
🔗 คู่มือสถาปัตยกรรมแอปพลิเคชันอย่างเป็นทางการ: https://docs.flutter.dev/app-architecture
การไหลของข้อมูลในทิศทางเดียว (Unidirectional Data Flow)
1. View (UI)
ส่งต่อการกระทำผู้ใช้ (User Event) ผ่านคำสั่ง Command
⬇️
2. ViewModel (State Management)
จัดการกระบวนการทำงานและคุยกับคลังข้อมูล
⬇️
3. Repository (Data Layer)
ดึง จัดเก็บ และส่งคืนข้อมูลจริงกลับสู่ ViewModel
⬇️ (notifyListeners)
4. UI Re-renders via ListenableBuilder
หน้าจอตรวจพบการเปลี่ยนแปลงและรีเฟรชเฉพาะจุด