บทที่ 12mobile|12 KB

บทที่ 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:

  1. Request (คำขอ): Flutter ส่งซองจดหมายไปหา Server หน้าซองระบุที่อยู่ (URL) และวิธีส่ง (Method) ข้างในซองใส่ข้อมูล (Body)
  2. Processing (ประมวลผล): Server แกะซอง ทำงานตามสั่ง (บันทึกข้อมูล/ค้นหาข้อมูล)
  3. 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(...) แทนที่จะพิมพ์ยาวๆ)
  • import 'api_config.dart'

    • นำเข้าไฟล์ config เพื่อเอาตัวแปร kApiBase มาใช้

อธิบาย:

// ฟังก์ชันช่วยสร้าง 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)

สิ่งที่ต้องทำ:

  1. แก้ NotesApi.list ให้รับ parameter String? query
  2. ถ้ามี query ให้ต่อท้าย URL เป็น ?q=...
  3. สร้าง TextField ในหน้า Home เมื่อพิมพ์ให้เรียก API ใหม่

โจทย์ที่ 2: Pull to Refresh

สิ่งที่ต้องทำ:

  1. หุ้ม ListView ด้วย RefreshIndicator
  2. ใน property onRefresh ให้เรียกฟังก์ชัน _fetchData
  3. ทดสอบลากลงเพื่อโหลดข้อมูลใหม่

โจทย์ที่ 3: ปุ่มลบ (Delete)

สิ่งที่ต้องทำ:

  1. เพิ่มฟังก์ชัน delete(String id) ใน NotesApi (ใช้ http.delete)
  2. เพิ่มปุ่มถังขยะในรายการ Note
  3. เมื่อกด ให้เรียก 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 (เช่น การตั้งค่าธีม) เราจะเก็บไว้ในเครื่องอย่างไร? ไปดูกัน!