Chuyển tới nội dung chính

RESTful API trong Khóa Học Flutter Level 3

· 10 phút để đọc

RESTful API trong Khóa Học Flutter Level 3​

Trong khóa học Flutter Level 3, việc tích hợp và làm việc với RESTful API là một trong những kỹ năng quan trọng mà học viên sẽ được học và thực hành. RESTful API là một giao thức phổ biến được sử dụng để giao tiếp giữa ứng dụng di động (Flutter) và backend server. Dưới đây là những nội dung chính liên quan đến RESTful API trong khóa học:

1. Tổng quan về RESTful API (Buổi 4)​

  • REST (Representational State Transfer) là một kiến trúc phần mềm được sử dụng để thiết kế các hệ thống mạng, đặc biệt là các ứng dụng web. RESTful API là các API tuân thủ các nguyên tắc của REST.
  • Các phương thức HTTP phổ biến trong RESTful API:
    • GET: Lấy dữ liệu từ server.
    • POST: Gửi dữ liệu mới lên server.
    • PUT: Cập nhật dữ liệu đã có trên server.
    • DELETE: Xóa dữ liệu trên server.
  • Cấu trúc API response: Thông thường, API response sẽ trả về dữ liệu dưới dạng JSON, bao gồm các trường như status, message, và data.

Bài tập: Học viên sẽ thực hành gọi một API công khai (ví dụ: JSONPlaceholder) và hiển thị dữ liệu lên màn hình Flutter.

2. Giao tiếp với RESTful API bằng thư viện http (Buổi 4)​

  • Thư viện http là một thư viện đơn giản và phổ biến trong Flutter để thực hiện các yêu cầu HTTP.
  • Học viên sẽ học cách:
    • Gửi các yêu cầu GET, POST, PUT, DELETE đến server.
    • Xử lý response từ server và chuyển đổi JSON thành các đối tượng Dart.
    • Xử lý lỗi khi gọi API, chẳng hạn như lỗi mạng hoặc lỗi từ server.

Bài tập: Học viên sẽ thực hành gọi API và hiển thị dữ liệu lên màn hình Flutter, đồng thời xử lý các trường hợp lỗi.

3. Sử dụng Dio để gọi API nâng cao (Buổi 5)​

  • Dio là một thư viện mạnh mẽ hơn so với http, hỗ trợ nhiều tính năng nâng cao như interceptors, timeout, retry request, và hỗ trợ upload file.
  • Học viên sẽ học cách:
    • Cấu hình Dio để thêm các header tùy chỉnh (ví dụ: Authorization token).
    • Sử dụng interceptors để tự động thêm token vào mỗi request.
    • Xử lý timeout và tự động retry request khi có lỗi mạng.

Bài tập: Học viên sẽ tích hợp Dio vào ứng dụng Flutter, thêm header cho request và xử lý các tình huống phức tạp hơn.

4. Authentication với API (JWT, OAuth2) (Buổi 6)​

  • JWT (JSON Web Token) là một chuẩn mã hóa để truyền thông tin an toàn giữa client và server. JWT thường được sử dụng để xác thực người dùng.
  • OAuth2 là một giao thức ủy quyền, cho phép ứng dụng truy cập vào tài nguyên của người dùng mà không cần chia sẻ thông tin đăng nhập.
  • Học viên sẽ học cách:
    • Đăng nhập và đăng ký qua API.
    • Lưu trữ token (JWT) trên thiết bị bằng SharedPreferences hoặc Hive.
    • Tự động refresh token khi token hết hạn.

Bài tập: Học viên sẽ xây dựng màn hình đăng nhập và tích hợp API authentication vào ứng dụng Flutter.

5. Pagination và Infinite Scroll với API (Buổi 9)​

  • Pagination là kỹ thuật phân trang dữ liệu, giúp ứng dụng load dữ liệu theo từng phần thay vì load toàn bộ dữ liệu một lúc.
  • Học viên sẽ học cách:
    • Sử dụng các tham số như offset, limit, hoặc cursor-based để phân trang dữ liệu.
    • Triển khai infinite scroll trong Flutter bằng ListView.builder.
    • Sử dụng flutter_bloc để quản lý trạng thái khi load dữ liệu phân trang.

Bài tập: Học viên sẽ thực hiện phân trang danh sách sản phẩm và tích hợp infinite scroll vào ứng dụng.

6. Bảo mật API & Xử lý lỗi nâng cao (Buổi 10)​

  • Bảo mật API là một yếu tố quan trọng khi phát triển ứng dụng di động. Học viên sẽ học các kỹ thuật bảo mật như:
    • Sử dụng HTTPS để mã hóa dữ liệu truyền giữa client và server.
    • SSL Pinning để ngăn chặn các cuộc tấn công Man-in-the-Middle (MITM).
  • Xử lý lỗi nâng cao: Học viên sẽ học cách xử lý các lỗi API phức tạp và hiển thị thông báo lỗi thân thiện với người dùng.

Bài tập: Học viên sẽ cài đặt SSL Pinning trên ứng dụng Flutter và xử lý các lỗi API một cách chuyên nghiệp.

Kết quả​

Qua các buổi học về RESTful API trong khóa học Flutter Level 3, học viên sẽ nắm vững cách tích hợp và làm việc với API trong ứng dụng Flutter. Từ việc gọi API cơ bản đến các kỹ thuật nâng cao như authentication, phân trang, và bảo mật, học viên sẽ có đủ kiến thức để xây dựng các ứng dụng di động tương tác với backend một cách hiệu quả và an toàn.

Kiến thức nền về REST API mà mọi lập trình viên Flutter cần​

Trước khi viết dòng code Flutter nào, bạn cần hiểu rõ giao tiếp giữa app và server.

HTTP method và tính chất​

MethodDùng choIdempotent?Có body?
GETĐọc dữ liệu✅ CóKhông
POSTTạo mới❌ KhôngCó
PUTThay thế toàn bộ✅ CóCó
PATCHCập nhật một phẩn❌ KhôngCó
DELETEXoá✅ CóKhông

Vì sao "idempotent" quan trọng: nếu request bị mất mạng và app gửi lại, GET/PUT/DELETE cho kết quả giống như gửi một lần. POST không — gửi lại có thể tạo hai bản ghi trùng. Đây là lý do thanh toán dùng idempotency key.

Mã trạng thái app phải xử lý​

MãÝ nghĩaApp nên làm gì
200Thành côngHiển thị dữ liệu
201Đã tạoHiển thị bản ghi mới
204Thành công, không có bodyKhông parse JSON
400Dữ liệu gửi lên saiHiển thị lỗi validate
401Chưa đăng nhập / token hết hạnChuyển về màn đăng nhập
403Không có quyềnThông báo không đủ quyền
404Không tìm thấyHiển thị "không có dữ liệu"
429Quá nhiều requestChờ và thử lại (backoff)
5xxLỗi serverThông báo chung, cho phép thử lại

⚠️ Lỗi phổ biến nhất trong code Flutter thực tế: chỉ kiểm tra statusCode == 200. Khi API trả 201 hoặc 204, app báo lỗi dù thực tất thành công. Hãy kiểm tra theo khoảng: statusCode >= 200 && statusCode < 300.

Cấu trúc một lớp API Service đúng chuẩn​

// lib/services/api_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class ApiException implements Exception {
final int? statusCode;
final String message;
ApiException(this.message, {this.statusCode});

@override
String toString() => message;
}

class ApiClient {
final String baseUrl;
final http.Client _client;
String? _token;

ApiClient({required this.baseUrl, http.Client? client})
: _client = client ?? http.Client();

void datToken(String? token) => _token = token;

Map<String, String> get _headers => {
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};

Future<dynamic> _xuLy(http.Response r) async {
// 204 hoặc body rỗng → không parse JSON
if (r.statusCode == 204 || r.body.isEmpty) return null;

// Decode UTF-8 tường minh — tiếng Việt sẽ lỗi nếu dùng r.body
final duLieu = jsonDecode(utf8.decode(r.bodyBytes));

if (r.statusCode >= 200 && r.statusCode < 300) return duLieu;

if (r.statusCode == 401) {
throw ApiException('Phiên đăng nhập đã hết hạn', statusCode: 401);
}
if (r.statusCode == 429) {
throw ApiException('Bạn thao tác quá nhanh, vui lòng thử lại sau',
statusCode: 429);
}

final thongDiep = duLieu is Map && duLieu['loi'] != null
? duLieu['loi'].toString()
: 'Lỗi không xác định (${r.statusCode})';
throw ApiException(thongDiep, statusCode: r.statusCode);
}

Future<dynamic> get(String path, {Map<String, String>? query}) async {
final uri = Uri.parse('$baseUrl$path').replace(queryParameters: query);
try {
return _xuLy(await _client.get(uri, headers: _headers)
.timeout(const Duration(seconds: 20)));
} on ApiException {
rethrow;
} catch (e) {
throw ApiException('Không kết nối được máy chủ. Kiểm tra mạng của bạn.');
}
}

Future<dynamic> post(String path, Map<String, dynamic> body) async {
try {
return _xuLy(await _client
.post(Uri.parse('$baseUrl$path'),
headers: _headers, body: jsonEncode(body))
.timeout(const Duration(seconds: 20)));
} on ApiException {
rethrow;
} catch (e) {
throw ApiException('Không kết nối được máy chủ. Kiểm tra mạng của bạn.');
}
}
}

Ba chi tiết kỹ thuật quan trọng trong đoạn code trên:

Chi tiếtVì sao
utf8.decode(r.bodyBytes)r.body đoán encoding từ header; nếu server trả sai, tiếng Việt bị lỗi font
.timeout(...)Không có timeout, request có thể treo vô hạn khi mạng yếu
rethrow cho ApiExceptionTránh bọc lỗi thành "không kết nối được" làm mất thông tin gốc

Parse JSON an toàn​

// lib/models/san_pham.dart
class SanPham {
final int id;
final String ten;
final double gia;
final String? hinhAnh;

const SanPham({
required this.id,
required this.ten,
required this.gia,
this.hinhAnh,
});

factory SanPham.fromJson(Map<String, dynamic> json) {
return SanPham(
id: (json['id'] as num).toInt(),
ten: json['ten'] as String? ?? 'Không có tên',
// Server có thể trả String hoặc num — xử lý cả hai
gia: json['gia'] is num
? (json['gia'] as num).toDouble()
: double.tryParse(json['gia'].toString()) ?? 0,
hinhAnh: json['hinh_anh'] as String?,
);
}

Map<String, dynamic> toJson() => {
'id': id,
'ten': ten,
'gia': gia,
'hinh_anh': hinhAnh,
};
}

⚠️ Bẫy as num với toInt(): JSON không phân biệt int và double. Server có thể trả 12 hoặc 12.0 cho cùng một trường. Dùng (json['x'] as num).toInt() an toàn cho cả hai; dùng json['x'] as int sẽ crash khi server trả 12.0.

Bốn lỗi triển khai thường gặp​

LỗiHậu quảCách sửa
Gọi API trong build()Gọi lại mỗi lần UI render → bão requestGọi trong initState hoặc state manager
Không xử lý trạng thái loading/lỗiApp treo hoặc màn hình trắngDùng FutureBuilder hoặc AsyncValue
Hard-code API key trong appBị lộ khi giải nén APKProxy qua backend
Không cancel request khi rời màn hìnhGhi đè state sau khi dispose → crashKiểm tra mounted trước setState

📚 Tài liệu khóa học​

Hẹn gặp các bạn vào ngày khai giảng! 🚀