บทที่ 12: API Integration (HTTP Requests)
สารบัญเนื้อหา
บทที่ 12: API Integration (HTTP Requests)
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เชื่อมต่อแอป Flutter กับ Backend Server (Node.js) ได้
- เข้าใจการทำงานของ REST API (GET, POST, PATCH, DELETE)
- แปลงข้อมูล JSON เป็น Dart Object (Serialization)
- ส่งข้อมูลแนบ Token เพื่อยืนยันตัวตน (Authorization Header)
- จัดการ Loading State และ Error Handling อย่างมืออาชีพ
📚 ทฤษฎีและแนวคิด
1. HTTP Request Cycle
การคุยกันระหว่าง "หน้าบ้าน" (Flutter) กับ "หลังบ้าน" (Node.js) เป็นแบบ Request-Response:
- Request (คำขอ): Flutter ส่งซองจดหมายไปหา Server หน้าซองระบุที่อยู่ (URL) และวิธีส่ง (Method) ข้างในซองใส่ข้อมูล (Body)
- Processing (ประมวลผล): Server แกะซอง ทำงานตามสั่ง (บันทึกข้อมูล/ค้นหาข้อมูล)
- Response (คำตอบ): Server ส่งซองจดหมายกลับมา พร้อมรหัสสถานะ (Status Code) และข้อมูล (JSON)
2. HTTP Methods ที่ต้องรู้
- GET: ขอดูข้อมูล (เช่น ขอดูรายชื่อ Note ทั้งหมด)
- POST: สร้างข้อมูลใหม่ (เช่น สร้าง Note ใหม่, Login)
- PATCH: แก้ไขข้อมูลบางส่วน (เช่น เปลี่ยนแค่ชื่อเรื่อง)
- DELETE: ลบข้อมูลทิ้ง
3. JSON Serialization
- JSON (JavaScript Object Notation): ภาษากลางที่ใช้คุยกัน หน้าตาเป็น String ยาวๆ
- Dart Object: ข้อมูลในรูปแบบโปรแกรมที่เอาไปใช้งานต่อได้
- หน้าที่เรา: ต้องเขียนโค้ดแปลง "String ยาวๆ" ให้เป็น "Object" (Decode) และแปลง "Object" กลับเป็น "String" (Encode)
💻 การเขียนโค้ด (Implementation)
1. ติดตั้ง Package
เราใช้ http package ซึ่งเป็นมาตรฐานที่สุดใน Flutter
# pubspec.yaml
dependencies:
http: ^1.2.0
2. ตั้งค่าการเชื่อมต่อ (Config)
ไฟล์: mobile/lib/services/api_config.dart
เราควรแยก URL ไว้ที่เดียว เผื่ออนาคตเปลี่ยน Server จะได้แก้ที่เดียวจบ
// สำหรับ Android Emulator ใช้ 10.0.2.2 แทน localhost
// สำหรับ iOS Simulator ใช้ localhost ได้เลย
// สำหรับเครื่องจริง ต้องใช้ IP Address ของคอม (เช่น 192.168.1.50) หรือ Deploy ขึ้น Cloud
const kApiBase = "https://QUOM-backendnpm.onrender.com";
// const kApiBase = "http://localhost:4010"; // สำหรับ Dev
3. การสร้าง Model Class
ไฟล์: mobile/lib/models/note.dart
เราต้องสร้าง Class มาเตรียมรับข้อมูลจาก JSON
class Note {
final String id;
final String title;
final String quote;
final List<String> tags;
Note({
required this.id,
required this.title,
required this.quote,
required this.tags,
});
// Factory Constructor: แปลง JSON Map -> Note Object
factory Note.fromJson(Map<String, dynamic> json) {
return Note(
id: json['_id'] ?? '', // ถ้าไม่มีให้เป็น string ว่าง
title: json['title'] ?? 'No Title',
quote: json['quote']?['text'] ?? '', // เข้าถึง object ซ้อน object
// แปลง List<dynamic> เป็น List<String>
tags: List<String>.from(json['tags'] ?? []),
);
}
}
4. สร้าง API Service
ไฟล์: mobile/lib/services/notes_api.dart
นี่คือ "พ่อครัว" ที่คอยจัดการเรื่องการรับส่งข้อมูลทั้งหมด
import 'dart:convert'; // สำหรับ jsonDecode, jsonEncode
import 'package:http/http.dart' as http;
import '../models/note.dart';
import 'api_config.dart';
import 'session.dart'; // import Session เพื่อเอา Token
class NotesApi {
อธิบายทีละบรรทัด (Imports):
-
import 'dart:convert'- Library มาตรฐานของ Dart ใช้แปลง JSON String <-> Map Object
-
import 'package:http/http.dart' as http- นำเข้า http package และตั้งชื่อเล่นว่า
http(เวลาเรียกใช้จะเขียนhttp.get(...)แทนที่จะพิมพ์ยาวๆ)
- นำเข้า http package และตั้งชื่อเล่นว่า
-
import 'api_config.dart'- นำเข้าไฟล์ config เพื่อเอาตัวแปร
kApiBaseมาใช้
- นำเข้าไฟล์ config เพื่อเอาตัวแปร
อธิบาย:
// ฟังก์ชันช่วยสร้าง Header พร้อม Token // ทุกครั้งที่จะคุยกับ Server ต้องยื่นบัตรผ่าน (Token) static Map<String, String> _headers() { final token = Session.token; if (token == null) { throw Exception("กรุณาเข้าสู่ระบบก่อนใช้งาน"); } return { "Content-Type": "application/json", "Authorization": "Bearer $token", }; }
// --- 1. GET: ดึงรายการ Notes --- static Future<List<Note>> list() async { final uri = Uri.parse("$kApiBase/api/notes");
// ส่ง Request ไป
final response = await http.get(uri, headers: _headers());
// ตรวจสอบว่า Server ตอบกลับมาว่า OK ไหม (200 = OK)
if (response.statusCode != 200) {
throw Exception("ไม่สามารถดึงข้อมูลได้: ${response.statusCode}");
}
// แปลง String ("{...}") เป็น Map
final data = jsonDecode(response.body) as Map<String, dynamic>;
// ดึง array "notes" ออกมา
final notesList = data['notes'] as List<dynamic>;
// วนลูปแปลง JSON แต่ละก้อน เป็น Note Object
return notesList.map((e) => Note.fromJson(e)).toList();
}
// --- 2. POST: สร้าง Note ใหม่ --- static Future<void> create(String title, List<String> tags) async { final uri = Uri.parse("$kApiBase/api/notes");
final response = await http.post(
uri,
headers: _headers(),
// แปลง Object เป็น JSON String ส่งไป
body: jsonEncode({
"title": title,
"tags": tags,
"language": "auto"
}),
);
if (response.statusCode != 200) {
throw Exception("บันทึกไม่สำเร็จ");
}
} }
### 5. การนำมาใช้หน้า UI (Loading State)
ไฟล์: `mobile/lib/screens/home_screen.dart`
หน้าจอเรามี 3 สถานะหลัก:
1. **Loading:** กำลังหมุนๆ รอข้อมูล
2. **Success:** ได้ข้อมูลมาโชว์
3. **Error:** เกิดข้อผิดพลาด (เน็ตหลุด, Server ล่ม)
```dart
class _HomeScreenState extends State<HomeScreen> {
// ตัวแปร State
List<Note> _notes = [];
bool _isLoading = true; // เริ่มต้นด้วยการโหลด
String? _errorMessage;
@override
void initState() {
super.initState();
_fetchData(); // เรียกข้อมูลทันทีที่เปิดหน้า
}
Future<void> _fetchData() async {
try {
final notes = await NotesApi.list();
setState(() {
_notes = notes;
_isLoading = false; // โหลดเสร็จแล้ว
});
} catch (e) {
// ถ้ามี Error (เช่น เน็ตหลุด)
setState(() {
_errorMessage = "เกิดข้อผิดพลาด: $e";
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
// 1. ถ้ากำลังโหลด แสดงวงกลมหมุนๆ
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
// 2. ถ้ามี Error แสดงปุ่มให้กด Retry
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.red),
Text(_errorMessage!),
ElevatedButton(
onPressed: _fetchData, // กดเพื่อลองใหม่
child: Text("ลองใหม่"),
)
],
),
);
}
// 3. ถ้าสำเร็จ แสดงรายการ
return ListView.builder(
itemCount: _notes.length,
itemBuilder: (context, index) {
final note = _notes[index];
return ListTile(
title: Text(note.title),
subtitle: Text(note.quote),
);
},
);
}
}
📝 แบบฝึกหัด
โจทย์ที่ 1: ระบบค้นหา (Search)
สิ่งที่ต้องทำ:
- แก้
NotesApi.listให้รับ parameterString? query - ถ้ามี query ให้ต่อท้าย URL เป็น
?q=... - สร้าง TextField ในหน้า Home เมื่อพิมพ์ให้เรียก API ใหม่
โจทย์ที่ 2: Pull to Refresh
สิ่งที่ต้องทำ:
- หุ้ม
ListViewด้วยRefreshIndicator - ใน property
onRefreshให้เรียกฟังก์ชัน_fetchData - ทดสอบลากลงเพื่อโหลดข้อมูลใหม่
โจทย์ที่ 3: ปุ่มลบ (Delete)
สิ่งที่ต้องทำ:
- เพิ่มฟังก์ชัน
delete(String id)ในNotesApi(ใช้http.delete) - เพิ่มปุ่มถังขยะในรายการ Note
- เมื่อกด ให้เรียก API ลบ และโหลดรายการใหม่
📌 สรุป
การเชื่อมต่อ API คือหัวใจของ Modern Apps
- Request: เราเป็นฝ่ายเริ่มบทสนทนา (GET/POST)
- Header: เราต้องแสดงบัตรผ่าน (Token) เสมอ
- Decode: แปลงภาษาเครื่อง (JSON) เป็นภาษาเรา (Object)
- State Management: หน้า UI ต้องจัดการ state (Loading/Error/Success) ให้ดีเพื่อให้ User ไม่งง
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 13: Local Storage (SharedPreferences) บางข้อมูลเราไม่อยากเก็บที่ Server (เช่น การตั้งค่าธีม) เราจะเก็บไว้ในเครื่องอย่างไร? ไปดูกัน!