แผนการเรียนรู้
/
Flutter Mobile Dev | Week 3 • Parts 1 & 2

พัฒนา Flutter Data Layer & Service Integration

🎯 สารบัญโครงสร้างเนื้อหาหมวดที่ 1 และ 2

🔵 หมวดที่ 1: แนะนำสถาปัตยกรรมชั้นข้อมูล (Data Layer Architecture)
🟢 หมวดที่ 2: พัฒนา Service Layer และจัดการข้อมูล (Network Clients)
💡 คำชี้แนะ: คลิกหัวข้อบนเมนูเพื่อข้ามไปยังเนื้อหาสไลด์ที่ต้องการศึกษา หรือใช้ปุ่มควบคุมด้านล่างเพื่อไล่ดูเนื้อหาทีละหน้า
Slide 1 • Data Layer Architecture

บทบาทหน้าที่ของชั้นข้อมูล (Data Layer Role) 📦

ตามคู่มือมาตรฐานสถาปัตยกรรมแอปพลิเคชัน Flutter ชั้นข้อมูลหรือ Data Layer จะทำหน้าที่ห่อหุ้มตรรกะการจัดเตรียม เข้าถึง และอัปเดตข้อมูลของแอปพลิเคชัน โดยแยกออกเป็น 2 คอมโพเนนต์หลักที่มีหน้าที่ต่างกันชัดเจน [1, 2]:

🔌 1. Services (ผู้ติดต่อระบบภายนอก)

ทำหน้าที่ติดต่อสื่อสารรับส่งข้อมูลดิบกับแหล่งข้อมูลภายนอกแบบไร้สถานะ (Stateless) เช่น การทำ HTTP Requests คุยกับ Django REST API Backend หรือการดึงข้อมูลจาก Local Database [2, 3]

📂 2. Repositories (ผู้ดูแลลอจิกการเข้าถึงข้อมูล)

ทำหน้าที่ประสานงานระหว่าง Services ดึงข้อมูลดิบมาล้าง ขัดเกลาแปลงเป็น Domain Models รวมถึงควบคุมลอจิกธุรกิจ เช่น การทำ Caching หรือการกักเก็บความจริงหนึ่งเดียว (SSOT) ของข้อมูล [1-4]

🔗 เจาะลึกโครงสร้าง Data Layer เพิ่มเติม: https://docs.flutter.dev/app-architecture/case-study/data-layer 🚀
Data Layer Architecture
📊 แผนภาพแสดงการแบ่งสัดส่วนเลเยอร์ข้อมูล และทิศทางไหลเวียนสถานะขึ้นสู่ UI แบบทิศทางเดียว (UDF)
Slide 2 • dio Client Setup

การติดตั้งและตั้งค่า HTTP Client ด้วยแพ็กเกจ dio 🛠️

สำหรับการพัฒนาแอปพลิเคชัน Compass เราเลือกใช้แพ็กเกจ `dio` ซึ่งเป็นคลังไลบรารีเครือข่ายยอดนิยมที่มีลูกเล่นขั้นสูง เช่น การตั้งค่า Base Options, การแนบไฟล์ และการทำ Interceptors ดักจับข้อมูลอัตโนมัติ [5]:

# เพิ่มไลบรารี dio เข้าสู่โปรเจกต์ Flutter:

$ flutter pub add dio

💡 แนวทางปฏิบัติดีที่สุด (Best Practice):

แนะนำให้จัดสร้างอินสแตนซ์ของ Dio เป็นแบบ Singleton เพื่อความสะดวกคงที่ในการจัดการชุดคอนฟิกูเรชันหลัก เช่น พอร์ตเส้นทางฐาน (Base URL) และจำกัดระยะเวลาดึงข้อมูล (Timeout Limits) ร่วมกันทั่วแอปพลิเคชัน [6]:

final options = BaseOptions(
  baseUrl: 'http://localhost:8000/api', // Django Server API Port
  connectTimeout: Duration(seconds: 5), // ระยะเวลาเชื่อมต่อระบบสูงสุด
  receiveTimeout: Duration(seconds: 3), // ระยะเวลารอคอยรับข้อมูล
);
final dio = Dio(options);
🔗 ดูรายละเอียดปลั๊กอิน dio เพิ่มเติม: https://pub.dev/packages/dio 🚀
Slide 3 • Simulation & CORS Handling

ระบบเครือข่ายจำลองและสิทธิ์ดึงข้อมูล CORS บนหน้าเว็บ 🌐

ในการรันจำลองระบบแอปพลิเคชัน Compass เพื่อดึงข้อมูลจริงจากเซิร์ฟเวอร์หลังบ้าน Django REST Framework นักศึกษาต้องคำนึงถึงเป้าหมายการรันและสิทธิ์ความปลอดภัยให้ถูกต้อง:

📱 เครือข่ายจำลองอุปกรณ์ (Emulators):

ระบบจำลองของ Android (Android Emulator) มักจะใช้งานระบบพอร์ตเซิร์ฟเวอร์ฐานผ่านทางไอพีวงในจำเพาะคือ http://10.0.2.2:8000 แทนการชี้เป้าหา localhost ตรงๆ เพื่อเลี่ยงข้อจำกัดวงลูปเน็ตเวิร์กของเครื่องจำลอง.

🌐 ความปลอดภัย Cross-Origin (CORS):

เมื่อรันแอปบน Web Browser เบราว์เซอร์จะส่งคำขอทดสอบความสอดคล้อง CORS Preflight ซึ่งต้องการให้ฝั่งเซิร์ฟเวอร์หลังบ้าน Django มีการติดตั้งชุด Middleware เปิดใช้งาน django-cors-headers เพื่อยินยอมและอนุญาตให้เข้าถึงข้อมูลข้ามโดเมนได้ [7, 8].

# Terminal 2 — เริ่ม Backend ที่สร้างไว้ในสัปดาห์ที่ 12 (ต้องรันคู่กันเสมอ)

$ cd oidc-backend && uv run manage.py runserver

# ตรวจความพร้อมของ CORS + endpoints ด้วย HTTPie ก่อนเขียนแอป

$ http :8000/.well-known/openid-configuration

🔗 ศึกษากลไกความรอบคอบ CORS บนเว็บ (MDN):

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS 🚀
Slide 4 • Define ApiClient Service

การสร้าง Stateless Network Client (ApiClient) 🔌

เราจะสร้างคลาสวิศวกรรมข้อมูลจำเพาะชื่อ ApiClient จัดให้อยู่ในชั้น Service Layer เพื่อเป็นศูนย์กลางติดต่อสื่อสารอินเทอร์เน็ตในการยิง API แลกเปลี่ยนค่าดิบ [1, 2]:

// lib/data/services/api_client.dart

import 'package:dio/dio.dart';

class ApiClient {

final Dio _dio;

ApiClient({required Dio dio}) : _dio = dio;

// ยิงคำขอดึงข้อมูลดิบจากเซิร์ฟเวอร์แบบไร้สถานะ (Stateless HTTP GET)

Future<Response<T>> get<T>(String path) async {

return _dio.get<T>(path);

}

Future<Response<T>> post<T>(String path, dynamic data) async {

return _dio.post<T>(path, data: data);

}

Future<Response<T>> delete<T>(String path) async {

return _dio.delete<T>(path);

}

}

🔗 ดูโค้ด ApiClient ของโครงการตัวอย่างทางการบน GitHub: https://github.com/flutter/samples/tree/main/compass_app 🚀
Slide 5 • Error Handling & Result Pattern

การรับมือข้อผิดพลาดอย่างปลอดภัยด้วย Result Pattern 🛡️

แอปพลิเคชัน Compass ออกแบบตามคู่มือวิศวกรรมข้อมูลแนะนำให้ใช้งานคลาสผู้รับผิดชอบพิเศษคือ `Result` ในการเป็นกล่องห่อหุ้มค่าตอบรับแบบอะซิงโครนัส เพื่อลดโอกาสเกิดข้อผิดพลาดรุนแรงเมื่ออินเทอร์เน็ตหลุด [9]:

🧩 โครงสร้างกล่อง Result (Sealed Class Concept):

  • Result.ok(T value): คืนค่าข้อมูลที่ขัดเกลาแล้วสำเร็จเมื่อดึงข้อมูลผ่าน API ไร้ปัญหา [9].
  • Result.error(Exception error): ส่งต่อชุดข้อผิดพลาดดักจับขึ้นไปประมวลผลต่อเพื่อแสดงหน้าต่าง Warning บน UI โดยที่โปรแกรมระบบไม่ล่มดับกลางคัน [9].

พฤติกรรมการควบคุมลักษณะนี้ช่วยให้สามารถเขียนประโยคควบคุมสวิตช์เงื่อนไข (Switch Case Statement) ในการตรวจดูสถานะสิทธิ์ได้อย่างรวดเร็วและปลอดภัยแบบไร้ช่องโหว่ [9].

🔗 ศึกษารายละเอียดแนวคิด Result Design Pattern ทางการ: https://docs.flutter.dev/app-architecture/design-patterns/result 🚀
Slide 6 • Implementing GET Requests

ขั้นตอนปฏิบัติ: การดึงข้อมูลทริปการท่องเที่ยว (GET Request) 📡

เขียนคำสั่งและดักจับความคืบหน้าในการดึงข้อมูลทริปเดินทางจำลองจากเซิร์ฟเวอร์ โดยส่งพารามิเตอร์จำกัดข้อมูล และตรวจสอบสิทธิ์พิจารณาการตอบสนอง:

// lib/data/services/api_client.dart (GET Method Extension)

Future<Result<List<BookingSummary>>> fetchBookings() async {

try {

final response = await _dio.get('/bookings/');

if (response.statusCode == 200) {

// แปลงค่า JSON ขาเข้าเป็นประเภท List ของอ็อบเจกต์โมเดลข้อมูลจริง

final list = (response.data['bookings'] as List)

.map((json) => BookingSummary.fromJson(json))

.toList();

return Result.ok(list);

} else {

return Result.error(Exception('Failed to load bookings'));

}

} on Exception catch (e) {

return Result.error(e);

}

}

# 🧪 ทดสอบ endpoint เดียวกันด้วย HTTPie ก่อนเขียนโค้ด Dart

$ http :8000/api/bookings/ "Authorization: Bearer <access_token>"

💡 คำแนะนำเชิงปฏิบัติ: การประมวลผลดึงค่า JSON จะตอบกลับสถานะเรียบร้อยผ่านทางรหัส HTTP 200 OK สกัดชุดโครงร่างมาห่อไว้ในกล่อง Result.ok ทันที [10].
Slide 7 • Implementing POST & DELETE Requests

ขั้นตอนปฏิบัติ: การบันทึกและลบข้อมูลแบบระบุสิทธิ์ 🔄

การบันทึกทริปใหม่หรือลบทริปท่องเที่ยวสำเร็จต้องการการเรียกใช้คำสั่งระบุสิทธิ์จำเพาะผ่าน POST หรือ DELETE เพื่อกระตุ้นระบบปรับเปลี่ยนข้อมูลฝั่งฐานข้อมูล [9]:

// lib/data/services/api_client.dart (Mutation Methods)

Future<Result<void>> deleteBooking(int id) async {

try {

final response = await _dio.delete('/bookings/$id/');

if (response.statusCode == 204 || response.statusCode == 200) {

return Result.ok(null);

} else {

return Result.error(Exception('Failed to delete booking'));

}

} on Exception catch (e) {

return Result.error(e);

}

}

# 🧪 ทดสอบคำสั่งลบด้วย HTTPie (ต้องได้กลับมา 200 หรือ 204 No Content)

$ http DELETE :8000/api/bookings/1/ "Authorization: Bearer <access_token>"

⚠️ คำชี้แจง: การลบทริปจะเดินทางเริ่มจากการสไลด์หน้าจอปัดทิ้ง (Dismissible Widget Event) ฝั่ง UI แล้วส่งพาสมาให้เลเยอร์ข้อมูลเพื่อส่งคำสั่งไปทำลายสถิติฝั่ง Backend ของ Django และรายงานสถานะกลับ [9, 11].
Slide 8 • Parts 1 & 2 Summary

บทสรุปการเชื่อมโยงระบบ Services และเป้าหมายต่อไป 🏁

ยินดีด้วยครับ! ตอนนี้นักศึกษาเข้าใจแก่นแท้ของ **Service Layer** สามารถใช้งานแพ็กเกจ `dio` ในการทำแทร็กคิวเรชันดึงผลลัพธ์เครือข่ายจำลอง และรับมือข้อผิดพลาดอัตราส่วนข้อมูลผ่านกล่อง `Result` ได้อย่างปลอดภัยแล้ว [9]

🚀 สิ่งที่เราจะทำต่อในหมวดที่ 3 ของสัปดาห์นี้:

  • สร้าง Abstract Repositories: ออกโครงร่างหน้ากากเป็นแนวทางฐานของข้อมูล
  • แยกโมเดลขอบเขตทริป: สกัดความต่างคลาสข้อมูล API Model สู่ Pure Domain Model
  • ปกป้อง Service เป็นส่วนตัว (Private): ดำเนินการล็อคสิทธิ์ปิดไม่ให้ UI ชั้นนอกข้ามมาติดต่อ Services โดยตรงเด็ดขาดเพื่อความเสถียรสูงสุด [3]
📚 ศึกษาการสื่อสารสัมพันธ์ระหว่างเลเยอร์และสถาปัตยกรรมล่วงหน้า: https://docs.flutter.dev/app-architecture/guide 🚀
Flutter Mobile Dev | Week 3 • Parts 3 & 4

หมวดที่ 3 และ 4: Repository Layer, Secure Storage & dio Interceptors

🎯 สารบัญสไลด์ปฏิบัติการหมวดที่ 3 และ 4

📂 หมวดที่ 3: พัฒนา Repository Layer
🔐 หมวดที่ 4: ความปลอดภัยและการจัดการ Token
💡 คำชี้แนะ: คลิกที่รายการเมนูหัวข้อด้านบนเพื่อข้ามไปยังหน้าสไลด์ที่สนใจ หรือใช้ปุ่มควบคุมถัดไปด้านล่างได้ทันที
Slide 10 • Defining Repository Interfaces

การออกแบบคลาสฐานด้วยอินเทอร์เฟซ (Abstract Repositories) 📐

สถาปัตยกรรมระดับ Production ต้องการความยืดหยุ่นในการสลับเปลี่ยนสภาพแวดล้อมทำโครงงาน (Environments) เราจึงเริ่มจากการออกแบบสัญญากลางหรืออินเทอร์เฟซฐานด้วยคำสั่ง **`abstract class`** เพื่อกำหนดพฤติกรรมการทำงานของข้อมูลที่ทุกสภาพแวดล้อมต้องเขียนรองรับเหมือนกัน:

// lib/data/repositories/booking.dart (สัญญาฐานร่วมกัน)

import 'package:compass_booking_app/domain/models/booking.dart';

import 'package:compass_booking_app/utils/result.dart';

abstract class BookingRepository {

Future<Result<List<BookingSummary>>> loadBookings();

Future<Result<void>> delete(int id);

}

💡 ทำไมต้องออกแบบแบบ Abstract Class?

ช่วยให้เราเขียนแอปแยกเลเยอร์ได้ง่าย โดยเลเยอร์ UI ด้านบนไม่จำเป็นต้องรู้ว่าข้อมูลมาจากไหน รู้เพียงสัญญาการมีอยู่ของคำสั่งเรียกใช้งานเท่านั้น และทำให้สลับไปเทสด้วย Fake คลาสจำลองได้สะดวกรวดเร็ว

🔗 ศึกษาหลักการจัดทำคลังข้อมูลของแอปพลิเคชัน Compass: https://docs.flutter.dev/app-architecture/case-study/data-layer 🚀
Slide 11 • Local and Remote Implementations

พัฒนา BookingRepositoryLocal และ BookingRepositoryRemote 📂

เราจะสร้างคลาสลูกที่สืบทอดสัญญาจากคลาสหลัก ได้แก่ **`BookingRepositoryLocal`** (เพื่อดึงข้อมูลออฟไลน์จำลองจากไฟล์ดิบในเครื่องตอนเขียนโปรแกรม) และ **`BookingRepositoryRemote`** (ส่งมอบคำสั่งดึงข้อมูลเครือข่ายจริงข้ามพอร์ตเซิร์ฟเวอร์):

// lib/data/repositories/booking_repository_remote.dart (ระบบรันงานจริง)

class BookingRepositoryRemote implements BookingRepository {

final ApiClient _apiClient; // รับตัวติดต่อ Service เครือข่ายผ่าน Constructor

BookingRepositoryRemote({required ApiClient apiClient}) : _apiClient = apiClient;

@override

Future<Result<List<BookingSummary>>> loadBookings() async {

try {

final response = await _apiClient.get('/bookings/');

final list = (response.data['bookings'] as List).map((e) => BookingSummary.fromJson(e)).toList();

return Result.ok(list);

} on Exception catch (e) {

return Result.error(e);

}

}

@override

Future<Result<void>> delete(int id) async => _apiClient.deleteBooking(id);

}

🔗 ตรวจสอบสลักโค้ดการแยก Local & Remote เพิ่มเติมของแอปพลิเคชันต้นแบบ Compass: https://github.com/flutter/samples/tree/main/compass_app/app/lib/data/repositories/booking 🚀
Slide 12 • Data Mapping & Translation

การเชื่อมโยงข้อมูล: API Model สู่ Pure Domain Model 🔄

เพื่อรักษาเสถียรภาพของ UI หน้าจอไม่ให้ปั่นป่วนเมื่อสคีมาของหลังบ้าน (Backend Database Schema) ปรับเปลี่ยนโครงสร้าง เราจะใช้แนวทาง **`Data Mapping`** สกัดกั้นโมเดลข้อมูลดิบจากเซิร์ฟเวอร์ แปลงมาเป็น Domain Model ที่ปลอดภัยและเป็นมิตรฝั่งหน้าแอปพลิเคชัน:

🛠️ ขั้นตอนการสกัดล้างข้อมูล (Data Translation Flow)

📡 JSON ขาเข้าดิบ (Raw JSON Map)
➡️ [ mapping.dart ] ➡️
🧩 Pure Domain Model (Immutable Object)

// คลาสโครงสร้างข้อมูลที่มีคุณสมบัติคงที่ (Immutable Domain Model)

class BookingSummary {

final int id;

final String destinationName;

final double price;

BookingSummary({required this.id, required this.destinationName, required this.price});

// ตัวถอดค่าจาก JSON Map สรุปแปลงผลอย่างแม่นยำ

factory BookingSummary.fromJson(Map<String, dynamic> json) =>

BookingSummary(id: json['id'], destinationName: json['destination_name'], price: json['price'].toDouble());

}

🔗 ศึกษารูปแบบสถาปัตยกรรมโมเดลระบายข้อมูลเพิ่มเติม: https://docs.flutter.dev/app-architecture/case-study/data-layer#domain-models 🚀
Slide 13 • Rules of Engagement

กฎเหล็ก: ทำไม UI และ ViewModel ห้ามคุยกับ Service โดยตรง? 🛡️

ในสถาปัตยกรรมแบบแยกเลเยอร์ของแอป Compass **`ApiClient`** หรือ Service ติดต่อเครือข่ายจะต้องถูกกำหนดให้เป็นตัวแปรประเภท **`private`** เสมอ เพื่อห้ามไม่ให้ส่วนแสดงผล (UI Layer) ข้ามระดับลงมาเรียกใช้งาน Service โดยตรงเด็ดขาด:

🛑 ผลเสียร้ายแรงหาก UI ข้ามระดับ:

  • สูญเสียการทำ Caching: ข้อมูลดิบจะไม่ผ่านระบบประเมินดักกรองของคลังเก็บข้อมูล
  • เทสระบบยากขึ้น: ไม่สามารถเขียนชุดทดสอบแบบจำลอง (Mock/Fake Tests) แยกสลับเฉพาะเลเยอร์ได้
  • บำรุงรักษาระบบติดขัด: หาก API โครงสร้างเปลี่ยน โค้ดฝั่งหน้าจอ View ทั้งแอปจะล่มทันที
🔗 เจาะลึกการจัดโครงสร้าง Layered Architecture (Flutter Docs): https://docs.flutter.dev/app-architecture/case-study/data-layer 🚀
Layered Architecture Diagram
📐 รูปแบบทิศทางการไหลเวียนข้อมูลที่ถูกต้อง: หน้าจอ UI เข้าถึง Repositories เพื่อประมวลผล ส่วน Repositories ทำหน้าที่ประสาน Service ด้านล่างอย่างเป็นความลับ
Slide 14 • Safe Token Persistence

สถาปัตยกรรมกักเก็บ Token อย่างมั่นคงบนอุปกรณ์มือถือ 🔐

การจัดเก็บ Token ยืนยันสิทธิ์ลงในเครื่องพกพา หากเก็บแบบลักไก่ผ่านระบบธรรมดา เช่น `SharedPreferences` (บน Android) หรือคุกกี้ทั่วไป จะเสี่ยงต่อการโดนฉกชิงถอนรหัสลับความปลอดภัยได้โดยง่ายจากการเจาะช่องโหว่ (เช่น Rooting หรือ XSS):

🔑 คีย์ที่เราจะเก็บ (ได้จาก OIDC Server สัปดาห์ที่ 12): 🪪 id_token 🎫 access_token ♻️ refresh_token

❌ ระบบจัดเก็บที่ไม่ปลอดภัย

หน่วยความจำปกติไม่เคยทำการเข้ารหัสข้อมูลดิบ ทำให้ผู้มีสิทธิ์ผู้บริหารเครื่อง (Root/Jailbreak) หรือโปรแกรมประสงค์ร้ายแอบดึงไฟล์ข้อมูลออกไปสแกนและเข้าสิทธิ์ทำงานแทนผู้ใช้ได้ทันที

✅ ระบบการจัดเก็บระดับสากล

ระบบความปลอดภัยเชิงฮาร์ดแวร์ ได้แก่ Keychain (สำหรับ iOS) และ Keystore (สำหรับ Android) จะทำการกักเก็บข้อมูลความลับของ Token ทุกสายไว้หลังกำแพงการเข้ารหัสฮาร์ดแวร์โดยสมบูรณ์

💡 ทางเลือกที่ดีที่สุดของทีม Flutter: นำแพ็กเกจระดับความน่าเชื่อถือสูงอย่าง `flutter_secure_storage` มาเขียนครอบการประมวลผล เพื่อเรียกใช้คำสั่งความมั่นคงร่วมกันแบบข้ามค่ายได้ทันที
Slide 15 • Implementing Secure Storage

ขั้นตอนปฏิบัติ: เขียน Secure Storage จัดเก็บ JWT 🧪

ติดตั้งไลบรารีและสร้างคลาส Service เป็นผู้ทำหน้าที่หลักในการจัดเก็บ อ่านค่า และทำลายความลับโทเค็นผ่านหน่วยความจำมั่นคงของโทรศัพท์พกพาโดยอัตโนมัติ:

# 1. รันคำสั่งเพิ่มคลังความมั่นคงเข้าระบบโปรเจกต์ Flutter:

$ flutter pub add flutter_secure_storage

// lib/data/services/secure_storage_service.dart

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorageService {

final _storage = const FlutterSecureStorage();

// บันทึกและเข้ารหัส Token ลงเครื่องอย่างถาวร

Future<void> write(String key, String value) async => await _storage.write(key: key, value: value);

// ดึงคีย์ลับเพื่อเอาออกมาใช้อ่านยืนยันระบบ

Future<String?> read(String key) async => await _storage.read(key: key);

// ทำลายข้อมูลในระบบเพื่อใช้ลบตอนล็อกเอาต์ออกจากหน้าจอ

Future<void> delete(String key) async => await _storage.delete(key: key);

}

🔗 ดูโครงร่างคลาสฉบับทางการบนฐานข้อมูลผับเดฟ: https://pub.dev/packages/flutter_secure_storage 🚀
Slide 16 • Network Interceptors Concept

แนบ Token อัตโนมัติด้วยระบบ dio Interceptors 📡

เพื่อไม่ให้นักศึกษาต้องพิมพ์แนบ Access Token ลงในส่วนหัวของคำขอ API ทุกๆ เส้นทางด้วยตนเอง เราจะนำกลไก **`Interceptors`** ของแพ็กเกจ `dio` มาตั้งดักจับเพื่อสแกนและแนบข้อมูลสิทธิ์เข้าถึงลงไปให้โดยอัตโนมัติก่อนส่งข้อความออกอินเทอร์เน็ต:

// lib/data/services/api_interceptors.dart

class AuthInterceptor extends Interceptor {

final SecureStorageService _storage;

AuthInterceptor(this._storage);

@override

void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {

final accessToken = await _storage.read('access_token');

if (accessToken != null) {

// ดักจับคำขอขาออก และสลัก Bearer Token ลงใน Header ทันที

options.headers['Authorization'] = 'Bearer $accessToken';

}

return handler.next(options); // ปล่อยขบวนคำขอให้เดินทางต่อ

}

}

🔗 เจาะลึกการดักจับ Interceptors ของแพ็กเกจ dio: https://pub.dev/packages/dio 🚀
Slide 17 • Advanced Auto-Refresh Interception

ปฏิบัติการขั้นโปร: กู้สิทธิ์อัตโนมัติหลังตรวจรับรหัส HTTP 401 ⚙️⚡

เมื่อ Access Token หมดอายุ เซิร์ฟเวอร์ Django หลังบ้านจะปฏิเสธคำขอและตอบกลับรหัส **`HTTP 401 Unauthorized`** ระบบ Interceptor ขั้นสูงของเราจะทำการดักจับข้อผิดพลาดนั้น สั่งหยุดการรันไว้ชั่วคราว แล้วยิงขอ Token คู่ใหม่โดยใช้ Refresh Token มาสับเปลี่ยนทันทีเพื่อประสบการณ์การทำงานที่ไม่มีวันสะดุด:

// ตรวจดักจับข้อผิดพลาดระหว่างทาง (401 Interception)

@override

void onError(DioException err, ErrorInterceptorHandler handler) async {

if (err.response?.statusCode == 401) {

final refreshToken = await _storage.read('refresh_token');

if (refreshToken != null) {

// ยิงไปที่ OIDC Token Endpoint ของ django-oidc-provider (สัปดาห์ที่ 12)

final refreshResponse = await _refreshDio.post('http://localhost:8000/token/', data: {'grant_type': 'refresh_token', 'refresh_token': refreshToken, 'client_id': 'flutter-web-app'});

final newAccess = refreshResponse.data['access_token'];

await _storage.write('access_token', newAccess);

// แนบตัวเข้าถึงตัวใหม่ และสั่งยิงคำขอดั้งเดิมออกไปซ่อมรอบสองทันที

err.requestOptions.headers['Authorization'] = 'Bearer $newAccess';

final retryResponse = await _refreshDio.fetch(err.requestOptions);

return handler.resolve(retryResponse); // สรุปสมานรอยต่อให้สำเร็จหมดจด

}

}

return handler.next(err);

}

🔁 อัปเดตจากสัปดาห์ที่ 12: endpoint กู้คืนสิทธิ์คือ OIDC Token Endpoint POST /token/ ของ django-oidc-provider (grant_type=refresh_token) — ไม่ใช่ /api/token/refresh/ แบบ Simple JWT อีกต่อไป
🔗 เจาะลึกวงจรการกู้สิทธิ์ Token ที่ตรวจพบจากระบบทดสอบของจริงในโครงการ Compass: https://github.com/flutter/samples/tree/main/compass_app 🚀
Slide 18 • Week 3 Summary & Preview

สรุปบทเรียนและภารกิจการเตรียมพร้อมสู่สัปดาห์ที่ 4 🏁

ในวันนี้เราได้สร้าง **Repositories & Services** ที่เป็นหัวใจของการเชื่อมต่อข้อมูล และได้เขียนโปรแกรมวางความมั่นคงปลอดภัยบนอุปกรณ์พกพาในการปกป้องและกู้คืน Access/Refresh Tokens ได้เสร็จสมบูรณ์เรียบร้อยแล้ว

🔥 ภารกิจจัดเตรียมความพร้อมก่อนเข้าสู่สัปดาห์ที่ 4:

  • ทบทวนขั้นตอนและวิธีการตรวจสอบรูปแบบข้อมูลเบื้องต้นของคลาส Form, TextFormField และ GlobalKey
  • ทำความคุ้นเคยกับคำสั่งในการดึงข้อมูลส่งค่าอัปเดต และแสดงข้อความตอบรับผ่าน Snackbars
  • รันระบบ API บนคอมพิวเตอร์และเชื่อมมือถือส่วนตัวเพื่อจำลองสถานการณ์ความมั่นคงในห้องปฏิบัติการจริงนอกเวลา
🔗 คู่มือและข้อมูลสนับสนุนการจัดทำสถาปัตยกรรมระดับโปร (Flutter App Architecture): https://docs.flutter.dev/app-architecture/guide 🚀