Trong hệ sinh thái Flutter/Dart, lập trình viên có thể sử dụng nhiều thư viện khác nhau để thực hiện các yêu cầu HTTP. Trong đó, HTTP Client là khái niệm và công cụ nền tảng giúp ứng dụng gửi, nhận dữ liệu qua HTTP; còn Dio là một thư viện HTTP Client mạnh mẽ, cung cấp nhiều tính năng nâng cao như interceptor, quản lý timeout, hủy request, tải tệp và xử lý lỗi thuận tiện. Việc hiểu rõ mối quan hệ giữa HTTP Client và Dio giúp lập trình viên lựa chọn công cụ phù hợp, đồng thời xây dựng kiến trúc mạng ổn định và dễ bảo trì.
1. Khái niệm HTTP Client
HTTP Client là thành phần chịu trách nhiệm tạo và xử lý các yêu cầu HTTP từ phía client, chẳng hạn như ứng dụng Flutter, đến server. Một yêu cầu HTTP thường bao gồm các thành phần chính:
- URL: Địa chỉ tài nguyên cần truy cập.
- HTTP method: Phương thức thao tác, phổ biến là GET, POST, PUT, PATCH và DELETE.
- Headers: Thông tin bổ sung như loại dữ liệu, mã xác thực hoặc ngôn ngữ.
- Query parameters: Các tham số được truyền trên URL.
- Request body: Dữ liệu gửi trong phần thân yêu cầu, thường dùng với POST, PUT hoặc PATCH.
Sau khi server xử lý yêu cầu, nó trả về một HTTP response. Response thường bao gồm:
- Status code: Mã trạng thái như 200, 201, 400, 401, 404 hoặc 500.
- Response headers: Thông tin mô tả phản hồi.
- Response body: Dữ liệu trả về, thường ở định dạng JSON.
Ví dụ, một ứng dụng có thể gửi yêu cầu GET đến /users để lấy danh sách người dùng. Nếu yêu cầu thành công, server có thể trả về mã 200 cùng một mảng dữ liệu JSON.
2. HTTP Client trong Dart
Dart cung cấp thư viện dart:io với lớp HttpClient, cho phép lập trình viên thực hiện các yêu cầu HTTP ở mức tương đối thấp. Ngoài ra, package http cũng được sử dụng phổ biến vì có API đơn giản hơn.
Ví dụ với package http:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> fetchUsers() async {
final uri = Uri.parse('https://api.example.com/users');
final response = await http.get(uri);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print(data);
} else {
throw Exception('Không thể tải dữ liệu người dùng');
}
}
Cách tiếp cận này phù hợp với các ứng dụng nhỏ hoặc các nhu cầu đơn giản. Tuy nhiên, khi dự án phát triển, lập trình viên thường phải xử lý nhiều vấn đề hơn: thêm token vào header, ghi log request, tự động làm mới token, giới hạn thời gian chờ, tải tệp, hủy request hoặc chuẩn hóa việc xử lý lỗi. Nếu tự xây dựng toàn bộ các chức năng này, mã nguồn dễ bị lặp lại và khó bảo trì.
3. Dio là gì?
Dio là một thư viện HTTP Client dành cho Dart và Flutter. Dio được xây dựng nhằm đơn giản hóa quá trình làm việc với API, đồng thời cung cấp nhiều khả năng mở rộng cho các ứng dụng có yêu cầu giao tiếp mạng phức tạp.
Một số ưu điểm nổi bật của Dio gồm:
- Hỗ trợ đầy đủ các phương thức HTTP.
- Cấu hình baseUrl, header và timeout tập trung.
- Hỗ trợ interceptor trước và sau request.
- Xử lý query parameters và dữ liệu JSON thuận tiện.
- Hỗ trợ upload, download và theo dõi tiến trình.
- Có thể hủy request thông qua CancelToken.
- Cung cấp hệ thống exception rõ ràng hơn.
- Hỗ trợ cấu hình cách xử lý status code.
- Dễ dàng tích hợp với kiến trúc repository, service hoặc clean architecture.
Có thể xem Dio là một lớp trừu tượng cao hơn so với HTTP Client cơ bản. Dio không thay thế khái niệm HTTP Client; ngược lại, nó là một giải pháp HTTP Client hoàn chỉnh, giúp lập trình viên sử dụng giao thức HTTP hiệu quả hơn.
4. Cài đặt và cấu hình Dio
Để sử dụng Dio trong Flutter, thêm package vào file pubspec.yaml:
dependencies:
dio: ^5.0.0
Sau đó chạy lệnh:
flutter pub get
Một cách cấu hình Dio cơ bản như sau:
import 'package:dio/dio.dart';
final dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
),
);
Việc cấu hình baseUrl giúp các request ngắn gọn hơn. Thay vì viết toàn bộ URL trong từng hàm, lập trình viên chỉ cần truyền đường dẫn tương đối như /users hoặc /products.
5. Thực hiện các phương thức HTTP với Dio
GET request
Future<void> getUsers() async {
try {
final response = await dio.get('/users');
print(response.data);
} on DioException catch (e) {
print('Lỗi lấy dữ liệu: ${e.message}');
}
}
response.data thường đã được Dio giải mã từ JSON thành kiểu dữ liệu Dart tương ứng như Map hoặc List.
POST request
Future<void> createUser() async {
try {
final response = await dio.post(
'/users',
data: {
'name': 'Nguyễn Văn A',
'email': '[email protected]',
},
);
print(response.data);
} on DioException catch (e) {
print('Lỗi tạo người dùng: ${e.message}');
}
}
Query parameters
final response = await dio.get(
'/products',
queryParameters: {
'page': 1,
'limit': 20,
'keyword': 'laptop',
},
);
Dio sẽ chuyển các tham số trên thành dạng URL tương ứng, ví dụ /products?page=1&limit=20&keyword=laptop.
PUT, PATCH và DELETE
await dio.put(
'/users/10',
data: {'name': 'Tên mới'},
);
await dio.patch(
'/users/10',
data: {'status': 'active'},
);
await dio.delete('/users/10');
6. Interceptor: điểm mạnh quan trọng của Dio
Interceptor cho phép can thiệp vào vòng đời của một request. Lập trình viên có thể sử dụng interceptor để:
- Gắn access token vào header.
- Ghi log request và response.
- Xử lý lỗi chung.
- Làm mới token khi nhận mã 401.
- Đo thời gian phản hồi.
Ví dụ:
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer access-token';
print('Gửi request: ${options.method} ${options.uri}');
handler.next(options);
},
onResponse: (response, handler) {
print('Nhận response: ${response.statusCode}');
handler.next(response);
},
onError: (DioException error, handler) {
print('Request thất bại: ${error.message}');
handler.next(error);
},
),
);
Trong ứng dụng thực tế, token không nên được viết trực tiếp trong mã nguồn. Token nên được lấy từ bộ nhớ an toàn hoặc một lớp quản lý phiên đăng nhập. Khi token hết hạn, interceptor có thể gọi API refresh token, cập nhật token mới rồi thử lại request ban đầu. Tuy nhiên, quá trình này cần được thiết kế cẩn thận để tránh gửi nhiều request refresh cùng lúc.
7. Xử lý lỗi trong Dio
Các lỗi mạng có thể xuất phát từ nhiều nguyên nhân khác nhau. Dio phân loại lỗi thông qua DioExceptionType, chẳng hạn:
- connectionTimeout: Không thể kết nối trong thời gian quy định.
- sendTimeout: Gửi dữ liệu quá lâu.
- receiveTimeout: Nhận dữ liệu quá lâu.
- badResponse: Server trả về status code không thành công.
- cancel: Request bị hủy.
- connectionError: Lỗi kết nối mạng.
- unknown: Lỗi không xác định.
Ví dụ xử lý:
Future<void> loadProfile() async {
try {
final response = await dio.get('/profile');
print(response.data);
} on DioException catch (e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
print('Kết nối đến server bị quá thời gian');
break;
case DioExceptionType.receiveTimeout:
print('Server phản hồi quá chậm');
break;
case DioExceptionType.badResponse:
print('Server trả về lỗi: ${e.response?.statusCode}');
break;
case DioExceptionType.connectionError:
print('Vui lòng kiểm tra kết nối Internet');
break;
default:
print('Đã xảy ra lỗi không xác định');
}
}
}
Một kiến trúc tốt không nên để từng màn hình tự xử lý mọi loại lỗi theo cách khác nhau. Thay vào đó, dự án nên có một lớp hoặc cơ chế chung để chuyển lỗi kỹ thuật thành thông báo dễ hiểu đối với người dùng.
8. Upload và download tệp
Dio đặc biệt hữu ích khi ứng dụng cần làm việc với tệp.
Upload tệp
import 'package:dio/dio.dart';
Future<void> uploadAvatar(String filePath) async {
final formData = FormData.fromMap({
'avatar': await MultipartFile.fromFile(filePath),
});
await dio.post('/profile/avatar', data: formData);
}
Download tệp và theo dõi tiến trình
Future<void> downloadFile(String url, String savePath) async {
await dio.download(
url,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
final progress = received / total * 100;
print('Tiến trình: ${progress.toStringAsFixed(0)}%');
}
},
);
}
Các tính năng này thường được dùng trong ứng dụng thương mại điện tử, mạng xã hội, quản lý tài liệu hoặc các ứng dụng có chức năng cập nhật ảnh đại diện.
9. So sánh Dio với HTTP Client cơ bản
| Tiêu chí | HTTP Client cơ bản | Dio |
|---|---|---|
| Mức độ đơn giản | Phù hợp với tác vụ nhỏ | Dễ dùng nhưng nhiều tính năng hơn |
| Interceptor | Thường phải tự xây dựng | Hỗ trợ sẵn |
| Xử lý lỗi | Cần tự chuẩn hóa | Có DioException và nhiều loại lỗi |
| Upload/download | Có thể cần triển khai thêm | Hỗ trợ thuận tiện |
| Hủy request | Phụ thuộc thư viện hoặc tự làm | Có CancelToken |
| Cấu hình tập trung | Có nhưng hạn chế | Mạnh với BaseOptions |
| Khả năng mở rộng | Phù hợp dự án đơn giản | Phù hợp dự án vừa và lớn |
| Độ phức tạp | Thấp | Cao hơn nhưng linh hoạt hơn |
Không thể khẳng định Dio luôn tốt hơn mọi HTTP Client khác. Nếu ứng dụng chỉ gọi một vài API đơn giản, package http có thể là lựa chọn nhẹ và phù hợp. Ngược lại, với dự án cần xác thực, logging, refresh token, upload hoặc nhiều tầng kiến trúc, Dio thường giúp tiết kiệm thời gian và giảm mã nguồn lặp lại.
10. Thiết kế Dio theo kiến trúc tốt
Để sử dụng Dio hiệu quả, không nên tạo một đối tượng Dio mới trong từng màn hình. Thay vào đó, nên xây dựng một lớp quản lý riêng:
class ApiClient {
late final Dio dio;
ApiClient() {
dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
),
);
dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: true,
),
);
}
}
Sau đó, các repository sẽ sử dụng ApiClient để gọi API. Ví dụ:
class UserRepository {
final Dio dio;
UserRepository(this.dio);
Future<Map<String, dynamic>> getUser(int id) async {
final response = await dio.get('/users/$id');
return Map<String, dynamic>.from(response.data);
}
}
Cách tổ chức này giúp tách biệt trách nhiệm:
- ApiClient: Cấu hình kết nối và chính sách chung.
- Repository: Cung cấp các thao tác dữ liệu theo nghiệp vụ.
- Model: Chuyển đổi JSON thành đối tượng Dart.
- UI hoặc state management: Hiển thị trạng thái loading, success và error.
Khi các thành phần được phân tách rõ ràng, việc kiểm thử và thay đổi API sẽ dễ dàng hơn.
11. Những lưu ý khi sử dụng Dio
Thứ nhất, cần đặt timeout hợp lý. Timeout quá dài khiến người dùng phải chờ lâu; timeout quá ngắn có thể làm thất bại các request hợp lệ trong điều kiện mạng chậm.
Thứ hai, không nên ghi log các dữ liệu nhạy cảm như mật khẩu, access token hoặc thông tin cá nhân. LogInterceptor rất hữu ích trong giai đoạn phát triển nhưng cần được cấu hình cẩn thận khi phát hành ứng dụng.
Thứ ba, cần kiểm tra trạng thái mạng và cung cấp thông báo phù hợp. Không phải lỗi nào cũng là lỗi từ server; nhiều trường hợp do thiết bị mất Internet hoặc DNS không hoạt động.
Thứ tư, cần tránh gửi request lặp lại không kiểm soát. Trong các màn hình có thao tác tìm kiếm, nên sử dụng debounce; trong các thao tác tải dữ liệu, nên cân nhắc cache hoặc hủy request cũ.
Thứ năm, token và dữ liệu xác thực phải được lưu trữ an toàn. Không nên lưu thông tin quan trọng trong mã nguồn hoặc các vị trí không được bảo vệ.
Kết luận
HTTP Client là nền tảng giúp ứng dụng giao tiếp với server thông qua giao thức HTTP. Trong Dart và Flutter, lập trình viên có thể sử dụng các giải pháp cơ bản như HttpClient hoặc package http cho những nhu cầu đơn giản. Tuy nhiên, khi ứng dụng cần nhiều tính năng như interceptor, xác thực, quản lý timeout, upload, download, hủy request và xử lý lỗi tập trung, Dio trở thành một lựa chọn mạnh mẽ và thực tế.
Giá trị của Dio không chỉ nằm ở việc rút ngắn cú pháp gọi API. Quan trọng hơn, Dio giúp lập trình viên xây dựng một tầng giao tiếp mạng có cấu trúc, có thể mở rộng và dễ bảo trì. Khi được kết hợp với repository pattern, model, dependency injection và hệ thống quản lý trạng thái, Dio góp phần tạo nên kiến trúc Flutter chuyên nghiệp.
Dù lựa chọn HTTP Client cơ bản hay Dio, lập trình viên vẫn cần quan tâm đến các nguyên tắc chung: xử lý lỗi rõ ràng, bảo vệ dữ liệu nhạy cảm, kiểm soát timeout, tránh request dư thừa và thiết kế mã nguồn có khả năng kiểm thử. Vì vậy, hiểu bản chất của HTTP Client và sử dụng đúng sức mạnh của Dio là một kỹ năng quan trọng đối với bất kỳ lập trình viên Flutter nào.








Khoá học lập trình game con rắn cho trẻ em




