Skip to main content

3 posts tagged with "Dio"

View All Tags

Giới Thiệu Thư Viện Dio và Lý Do Nên Dùng Thay HTTP trong Flutter

· 8 min read

Khi phát triển ứng dụng Flutter, việc gọi API là một phần không thể thiếu. Trong khi package http là lựa chọn cơ bản, Dio là một giải pháp mạnh mẽ và linh hoạt hơn nhiều. Bài viết này sẽ giới thiệu về Dio và giải thích tại sao bạn nên sử dụng nó thay vì http package.


1️⃣ Dio Là Gì?

Dio là một HTTP client mạnh mẽ cho Dart/Flutter, được phát triển bởi Flutter China. Dio cung cấp nhiều tính năng nâng cao mà package http cơ bản không có.

1.1 Cài Đặt Dio

Thêm Dio vào file pubspec.yaml:

dependencies:
dio: ^5.4.0

Sau đó chạy:

flutter pub get

1.2 Import Dio

import 'package:dio/dio.dart';

2️⃣ So Sánh Dio vs HTTP Package

2.1 HTTP Package - Giải Pháp Cơ Bản

import 'package:http/http.dart' as http;
import 'dart:convert';

// Gọi API với http package
Future<void> fetchData() async {
final response = await http.get(
Uri.parse('https://api.example.com/data'),
headers: {'Content-Type': 'application/json'},
);

if (response.statusCode == 200) {
final data = json.decode(response.body);
print(data);
} else {
print('Error: ${response.statusCode}');
}
}

Hạn chế của HTTP package:

  • ❌ Không có interceptors
  • ❌ Không hỗ trợ retry tự động
  • ❌ Không có request/response transformers
  • ❌ Xử lý lỗi thủ công
  • ❌ Không có timeout configuration dễ dàng
  • ❌ Không hỗ trợ cancel requests

2.2 Dio - Giải Pháp Mạnh Mẽ

import 'package:dio/dio.dart';

// Gọi API với Dio
Future<void> fetchData() async {
final dio = Dio();

try {
final response = await dio.get('https://api.example.com/data');
print(response.data);
} catch (e) {
print('Error: $e');
}
}

Ưu điểm của Dio:

  • ✅ Interceptors mạnh mẽ
  • ✅ Retry logic tự động
  • ✅ Request/Response transformers
  • ✅ Xử lý lỗi tốt hơn
  • ✅ Timeout configuration dễ dàng
  • ✅ Hỗ trợ cancel requests
  • ✅ Hỗ trợ file upload/download
  • ✅ Progress callbacks

3️⃣ Các Tính Năng Nổi Bật Của Dio

3.1 Interceptors

Dio cho phép bạn thêm interceptors để xử lý requests và responses trước/sau khi gửi/nhận:

final dio = Dio();

// Thêm interceptor để log requests
dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));

// Thêm interceptor tùy chỉnh
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// Thêm token vào header
options.headers['Authorization'] = 'Bearer your_token';
return handler.next(options);
},
onResponse: (response, handler) {
// Xử lý response
return handler.next(response);
},
onError: (error, handler) {
// Xử lý lỗi
return handler.next(error);
},
));

3.2 Retry Logic

Dio hỗ trợ retry tự động khi request thất bại:

import 'package:dio/retry.dart';

final dio = Dio();

dio.interceptors.add(
RetryInterceptor(
dio: dio,
options: const RetryOptions(
retries: 3,
retryInterval: Duration(seconds: 2),
),
),
);

3.3 Request/Response Transformers

Transform dữ liệu trước khi gửi/nhận:

final dio = Dio();

dio.transformer = BackgroundTransformer()..jsonDecodeCallback = parseJson;

// Custom JSON parser
Map<String, dynamic> parseJson(String text) {
// Custom parsing logic
return jsonDecode(text);
}

3.4 Timeout Configuration

Cấu hình timeout dễ dàng:

final dio = Dio(BaseOptions(
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
sendTimeout: Duration(seconds: 3),
));

3.5 Cancel Requests

Hủy request khi không cần thiết:

final cancelToken = CancelToken();

// Gọi API với cancel token
dio.get(
'https://api.example.com/data',
cancelToken: cancelToken,
);

// Hủy request
cancelToken.cancel('Request cancelled');

3.6 File Upload/Download

Dio hỗ trợ upload và download file với progress callback:

// Upload file
FormData formData = FormData.fromMap({
'file': await MultipartFile.fromFile('/path/to/file'),
});

await dio.post(
'https://api.example.com/upload',
data: formData,
onSendProgress: (sent, total) {
print('Progress: ${(sent / total * 100).toStringAsFixed(0)}%');
},
);

// Download file
await dio.download(
'https://api.example.com/file',
'/path/to/save/file',
onReceiveProgress: (received, total) {
print('Progress: ${(received / total * 100).toStringAsFixed(0)}%');
},
);

4️⃣ Lý Do Nên Dùng Dio Thay HTTP

4.1 Code Sạch Hơn và Dễ Bảo Trì

Với HTTP package:

Future<Map<String, dynamic>> fetchUser(int userId) async {
final uri = Uri.parse('https://api.example.com/users/$userId');
final response = await http.get(
uri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
);

if (response.statusCode == 200) {
return json.decode(response.body);
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else if (response.statusCode == 404) {
throw NotFoundException();
} else {
throw ServerException();
}
}

Với Dio:

Future<Map<String, dynamic>> fetchUser(int userId) async {
try {
final response = await dio.get('/users/$userId');
return response.data;
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
throw UnauthorizedException();
} else if (e.response?.statusCode == 404) {
throw NotFoundException();
} else {
throw ServerException();
}
}
}

4.2 Xử Lý Lỗi Tốt Hơn

Dio cung cấp DioException với nhiều thông tin hữu ích:

try {
await dio.get('/api/data');
} on DioException catch (e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
print('Connection timeout');
break;
case DioExceptionType.sendTimeout:
print('Send timeout');
break;
case DioExceptionType.receiveTimeout:
print('Receive timeout');
break;
case DioExceptionType.badResponse:
print('Bad response: ${e.response?.statusCode}');
break;
case DioExceptionType.cancel:
print('Request cancelled');
break;
default:
print('Other error: ${e.message}');
}
}

4.3 Interceptors Cho Toàn Bộ App

Với Dio, bạn có thể cấu hình interceptors một lần và áp dụng cho tất cả requests:

class ApiClient {
static final Dio _dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
headers: {'Content-Type': 'application/json'},
));

static void init() {
// Thêm token vào mọi request
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = TokenManager.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
));

// Log requests
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));

// Retry logic
_dio.interceptors.add(
RetryInterceptor(
dio: _dio,
options: const RetryOptions(retries: 3),
),
);
}

static Dio get dio => _dio;
}

4.4 Type Safety Tốt Hơn

Dio hỗ trợ generic types cho responses:

// Định nghĩa model
class User {
final int id;
final String name;
final String email;

User({required this.id, required this.name, required this.email});

factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
}

// Sử dụng với Dio
Future<User> fetchUser(int id) async {
final response = await dio.get<Map<String, dynamic>>('/users/$id');
return User.fromJson(response.data!);
}

4.5 Hỗ Trợ Nhiều Format Dữ Liệu

Dio tự động xử lý JSON, FormData, và các format khác:

// JSON (mặc định)
await dio.post('/api/data', data: {'key': 'value'});

// FormData
await dio.post('/api/upload', data: FormData.fromMap({
'file': multipartFile,
'name': 'filename',
}));

// URL encoded
await dio.post(
'/api/data',
data: {'key': 'value'},
options: Options(contentType: Headers.formUrlEncodedContentType),
);

5️⃣ Ví Dụ Thực Tế: Tạo API Client Với Dio

import 'package:dio/dio.dart';

class ApiService {
late Dio _dio;

ApiService() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
headers: {
'Content-Type': 'application/json',
},
));

_setupInterceptors();
}

void _setupInterceptors() {
// Log interceptor
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
error: true,
));

// Auth interceptor
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = _getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
onError: (error, handler) {
if (error.response?.statusCode == 401) {
// Refresh token hoặc đăng nhập lại
_handleUnauthorized();
}
return handler.next(error);
},
));
}

String? _getToken() {
// Lấy token từ storage
return 'your_token_here';
}

void _handleUnauthorized() {
// Xử lý khi unauthorized
}

// GET request
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
try {
return await _dio.get(path, queryParameters: queryParameters);
} on DioException catch (e) {
throw _handleError(e);
}
}

// POST request
Future<Response> post(String path, {dynamic data}) async {
try {
return await _dio.post(path, data: data);
} on DioException catch (e) {
throw _handleError(e);
}
}

// PUT request
Future<Response> put(String path, {dynamic data}) async {
try {
return await _dio.put(path, data: data);
} on DioException catch (e) {
throw _handleError(e);
}
}

// DELETE request
Future<Response> delete(String path) async {
try {
return await _dio.delete(path);
} on DioException catch (e) {
throw _handleError(e);
}
}

Exception _handleError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return TimeoutException('Request timeout');
case DioExceptionType.badResponse:
return ServerException('Server error: ${error.response?.statusCode}');
case DioExceptionType.cancel:
return CancelException('Request cancelled');
default:
return NetworkException('Network error: ${error.message}');
}
}
}

// Custom exceptions
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
}

class ServerException implements Exception {
final String message;
ServerException(this.message);
}

class CancelException implements Exception {
final String message;
CancelException(this.message);
}

class NetworkException implements Exception {
final String message;
NetworkException(this.message);
}

6️⃣ Kết Luận

Dio là lựa chọn tốt hơn so với HTTP package vì:

Tính năng phong phú: Interceptors, retry, transformers
Code sạch hơn: Dễ đọc, dễ bảo trì
Xử lý lỗi tốt hơn: DioException với nhiều thông tin
Linh hoạt: Hỗ trợ nhiều use cases
Cộng đồng lớn: Nhiều tài liệu và ví dụ
Được maintain tích cực: Cập nhật thường xuyên

💡 Lời khuyên: Nếu bạn đang bắt đầu dự án Flutter mới, hãy sử dụng Dio ngay từ đầu. Nếu đang dùng HTTP package, việc migrate sang Dio cũng không quá khó khăn và sẽ mang lại nhiều lợi ích.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, API Integration, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, Dio và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Hướng Dẫn Call API Bằng Dio Có Kèm Interceptor và Retry Logic

· 9 min read

Trong bài viết này, chúng ta sẽ học cách sử dụng Dio để gọi API trong Flutter với các tính năng nâng cao như interceptors và retry logic. Đây là những kỹ thuật quan trọng để xây dựng ứng dụng Flutter chuyên nghiệp.


1️⃣ Cài Đặt và Cấu Hình Cơ Bản

1.1 Cài Đặt Dio

Thêm vào pubspec.yaml:

dependencies:
dio: ^5.4.0
dio_retry: ^2.0.0 # Cho retry logic

1.2 Tạo Dio Instance

import 'package:dio/dio.dart';

class ApiClient {
late Dio _dio;

ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
sendTimeout: Duration(seconds: 3),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
}

Dio get dio => _dio;
}

2️⃣ Interceptors - Xử Lý Requests và Responses

2.1 Log Interceptor

Ghi log tất cả requests và responses:

import 'package:dio/dio.dart';

void setupLogInterceptor(Dio dio) {
dio.interceptors.add(LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseHeader: true,
responseBody: true,
error: true,
logPrint: (obj) {
// Sử dụng logger của bạn (ví dụ: logger package)
print(obj);
},
));
}

2.2 Authentication Interceptor

Tự động thêm token vào mọi request:

class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Lấy token từ storage
final token = TokenStorage.getToken();

if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}

handler.next(options);
}
}

// Sử dụng
dio.interceptors.add(AuthInterceptor());

2.3 Refresh Token Interceptor

Tự động refresh token khi hết hạn:

class RefreshTokenInterceptor extends Interceptor {
final Dio refreshDio;

RefreshTokenInterceptor(this.refreshDio);

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// Nếu lỗi 401 (Unauthorized)
if (err.response?.statusCode == 401) {
try {
// Refresh token
final newToken = await _refreshToken();

if (newToken != null) {
// Lưu token mới
TokenStorage.saveToken(newToken);

// Retry request với token mới
final opts = err.requestOptions;
opts.headers['Authorization'] = 'Bearer $newToken';

final response = await refreshDio.request(
opts.path,
options: Options(
method: opts.method,
headers: opts.headers,
),
data: opts.data,
queryParameters: opts.queryParameters,
);

return handler.resolve(response);
}
} catch (e) {
// Nếu refresh token thất bại, đăng xuất
await AuthService.logout();
return handler.reject(err);
}
}

return handler.next(err);
}

Future<String?> _refreshToken() async {
try {
final refreshToken = TokenStorage.getRefreshToken();
final response = await refreshDio.post(
'/auth/refresh',
data: {'refresh_token': refreshToken},
);

return response.data['access_token'];
} catch (e) {
return null;
}
}
}

// Sử dụng
final refreshDio = Dio();
dio.interceptors.add(RefreshTokenInterceptor(refreshDio));

2.4 Error Handling Interceptor

Xử lý lỗi tập trung:

class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
String errorMessage = 'Đã xảy ra lỗi';

switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
errorMessage = 'Kết nối timeout. Vui lòng thử lại.';
break;
case DioExceptionType.badResponse:
final statusCode = err.response?.statusCode;
switch (statusCode) {
case 400:
errorMessage = 'Yêu cầu không hợp lệ';
break;
case 401:
errorMessage = 'Chưa đăng nhập hoặc token đã hết hạn';
break;
case 403:
errorMessage = 'Không có quyền truy cập';
break;
case 404:
errorMessage = 'Không tìm thấy tài nguyên';
break;
case 500:
errorMessage = 'Lỗi server. Vui lòng thử lại sau.';
break;
default:
errorMessage = 'Lỗi server: $statusCode';
}
break;
case DioExceptionType.cancel:
errorMessage = 'Request đã bị hủy';
break;
case DioExceptionType.unknown:
errorMessage = 'Không có kết nối internet';
break;
default:
errorMessage = err.message ?? 'Đã xảy ra lỗi';
}

// Hiển thị thông báo lỗi (sử dụng snackbar, dialog, etc.)
ErrorHandler.showError(errorMessage);

handler.next(err);
}
}

// Sử dụng
dio.interceptors.add(ErrorInterceptor());

2.5 Request/Response Transformer Interceptor

Transform dữ liệu trước khi gửi/nhận:

class DataTransformerInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// Transform request data
if (options.data is Map) {
// Thêm timestamp, device info, etc.
options.data = {
...options.data as Map,
'timestamp': DateTime.now().toIso8601String(),
'device_id': DeviceInfo.getDeviceId(),
};
}

handler.next(options);
}

@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// Transform response data
if (response.data is Map) {
final data = response.data as Map;

// Unwrap nested data nếu cần
if (data.containsKey('data')) {
response.data = data['data'];
}
}

handler.next(response);
}
}

// Sử dụng
dio.interceptors.add(DataTransformerInterceptor());

3️⃣ Retry Logic - Tự Động Thử Lại Khi Thất Bại

3.1 Sử dụng dio_retry Package

import 'package:dio/dio.dart';
import 'package:dio_retry/dio_retry.dart';

void setupRetryInterceptor(Dio dio) {
dio.interceptors.add(
RetryInterceptor(
dio: dio,
options: RetryOptions(
retries: 3, // Số lần retry
retryInterval: Duration(seconds: 2), // Khoảng thời gian giữa các lần retry
exponentialBackoff: true, // Tăng dần thời gian chờ
retryableExtraStatuses: [401, 403], // Retry với các status code này
),
),
);
}

3.2 Custom Retry Logic

Tự xây dựng retry logic:

class RetryInterceptor extends Interceptor {
final int maxRetries;
final Duration retryDelay;

RetryInterceptor({
this.maxRetries = 3,
this.retryDelay = const Duration(seconds: 2),
});

@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (_shouldRetry(err)) {
final retryCount = err.requestOptions.extra['retryCount'] ?? 0;

if (retryCount < maxRetries) {
// Tăng retry count
err.requestOptions.extra['retryCount'] = retryCount + 1;

// Đợi trước khi retry
await Future.delayed(retryDelay * (retryCount + 1));

try {
// Retry request
final response = await dio.request(
err.requestOptions.path,
options: Options(
method: err.requestOptions.method,
headers: err.requestOptions.headers,
),
data: err.requestOptions.data,
queryParameters: err.requestOptions.queryParameters,
extra: err.requestOptions.extra,
);

return handler.resolve(response);
} catch (e) {
// Nếu vẫn lỗi, tiếp tục retry hoặc reject
if (retryCount + 1 < maxRetries) {
return onError(err, handler);
}
}
}
}

handler.next(err);
}

bool _shouldRetry(DioException err) {
// Chỉ retry với một số loại lỗi nhất định
return err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.sendTimeout ||
(err.response?.statusCode != null &&
err.response!.statusCode! >= 500);
}
}

// Sử dụng
dio.interceptors.add(RetryInterceptor(maxRetries: 3));

4️⃣ Ví Dụ Hoàn Chỉnh: API Service Với Interceptors và Retry

import 'package:dio/dio.dart';
import 'package:dio_retry/dio_retry.dart';

class ApiService {
late Dio _dio;
static final ApiService _instance = ApiService._internal();

factory ApiService() => _instance;

ApiService._internal() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
sendTimeout: Duration(seconds: 3),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));

_setupInterceptors();
}

void _setupInterceptors() {
// 1. Log Interceptor
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
error: true,
));

// 2. Auth Interceptor
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
final token = _getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
return handler.next(options);
},
));

// 3. Retry Interceptor
_dio.interceptors.add(
RetryInterceptor(
dio: _dio,
options: RetryOptions(
retries: 3,
retryInterval: Duration(seconds: 2),
exponentialBackoff: true,
),
),
);

// 4. Error Interceptor
_dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) {
_handleError(error);
return handler.next(error);
},
));
}

String? _getToken() {
// Lấy token từ secure storage
// return SecureStorage.getToken();
return null;
}

void _handleError(DioException error) {
// Xử lý lỗi tập trung
// Có thể hiển thị snackbar, log, etc.
print('API Error: ${error.message}');
}

// GET request
Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.get<T>(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

// POST request
Future<Response<T>> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.post<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

// PUT request
Future<Response<T>> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.put<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

// DELETE request
Future<Response<T>> delete<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.delete<T>(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _mapException(e);
}
}

Exception _mapException(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return TimeoutException('Request timeout');
case DioExceptionType.badResponse:
return ServerException(
'Server error: ${error.response?.statusCode}',
error.response?.statusCode,
);
case DioExceptionType.cancel:
return CancelException('Request cancelled');
default:
return NetworkException('Network error: ${error.message}');
}
}
}

// Custom exceptions
class TimeoutException implements Exception {
final String message;
TimeoutException(this.message);
}

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

class CancelException implements Exception {
final String message;
CancelException(this.message);
}

class NetworkException implements Exception {
final String message;
NetworkException(this.message);
}

5️⃣ Sử Dụng API Service

// Trong repository hoặc service
class UserRepository {
final ApiService _apiService = ApiService();

Future<User> getUser(int id) async {
try {
final response = await _apiService.get<Map<String, dynamic>>('/users/$id');
return User.fromJson(response.data!);
} catch (e) {
throw Exception('Failed to get user: $e');
}
}

Future<List<User>> getUsers() async {
try {
final response = await _apiService.get<List<dynamic>>('/users');
return (response.data as List)
.map((json) => User.fromJson(json))
.toList();
} catch (e) {
throw Exception('Failed to get users: $e');
}
}

Future<User> createUser(User user) async {
try {
final response = await _apiService.post<Map<String, dynamic>>(
'/users',
data: user.toJson(),
);
return User.fromJson(response.data!);
} catch (e) {
throw Exception('Failed to create user: $e');
}
}
}

6️⃣ Best Practices

6.1 Thứ Tự Interceptors

Thứ tự interceptors quan trọng:

// 1. Log interceptor (đầu tiên để log tất cả)
dio.interceptors.add(LogInterceptor());

// 2. Auth interceptor (thêm token)
dio.interceptors.add(AuthInterceptor());

// 3. Retry interceptor (retry khi lỗi)
dio.interceptors.add(RetryInterceptor());

// 4. Error interceptor (xử lý lỗi cuối cùng)
dio.interceptors.add(ErrorInterceptor());

6.2 Sử Dụng Singleton Pattern

Tạo một instance Dio duy nhất cho toàn bộ app:

class ApiService {
static final ApiService _instance = ApiService._internal();
factory ApiService() => _instance;
ApiService._internal();
}

6.3 Cancel Token Cho Long-Running Requests

final cancelToken = CancelToken();

// Gọi API
_apiService.get('/data', cancelToken: cancelToken);

// Hủy khi không cần thiết (ví dụ: khi widget dispose)
@override
void dispose() {
cancelToken.cancel('Widget disposed');
super.dispose();
}

7️⃣ Kết Luận

Với Dio, interceptors và retry logic, bạn có thể:

Tự động thêm authentication vào mọi request
Xử lý lỗi tập trung và nhất quán
Tự động retry khi request thất bại
Log requests/responses để debug
Transform data trước khi gửi/nhận

💡 Lời khuyên: Sử dụng interceptors để tách biệt concerns. Mỗi interceptor nên có một nhiệm vụ cụ thể. Điều này giúp code dễ đọc, dễ test và dễ bảo trì.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, API Integration, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, Dio và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.

Hướng Dẫn Upload Hình Lên Server Với Multipart (Dio)

· 11 min read

Upload hình ảnh là một tính năng phổ biến trong ứng dụng di động. Bài viết này sẽ hướng dẫn bạn cách upload hình ảnh lên server trong Flutter sử dụng Dio với Multipart, bao gồm chọn ảnh, compress, và theo dõi tiến trình upload.


1️⃣ Cài Đặt Dependencies

1.1 Thêm Packages

Thêm vào pubspec.yaml:

dependencies:
dio: ^5.4.0
image_picker: ^1.0.7
path_provider: ^2.1.1
image: ^4.1.3 # Cho compress image

Sau đó chạy:

flutter pub get

1.2 Cấu Hình Permissions

Android (android/app/src/main/AndroidManifest.xml)

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

iOS (ios/Runner/Info.plist)

<key>NSPhotoLibraryUsageDescription</key>
<string>App cần truy cập thư viện ảnh để chọn ảnh</string>
<key>NSCameraUsageDescription</key>
<string>App cần truy cập camera để chụp ảnh</string>

2.1 Sử Dụng Image Picker

import 'package:image_picker/image_picker.dart';
import 'dart:io';

class ImagePickerService {
final ImagePicker _picker = ImagePicker();

// Chọn ảnh từ gallery
Future<File?> pickImageFromGallery() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85, // Chất lượng ảnh (0-100)
maxWidth: 1920, // Giới hạn kích thước
maxHeight: 1080,
);

if (image != null) {
return File(image.path);
}
return null;
} catch (e) {
print('Error picking image: $e');
return null;
}
}

// Chụp ảnh từ camera
Future<File?> pickImageFromCamera() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 85,
maxWidth: 1920,
maxHeight: 1080,
);

if (image != null) {
return File(image.path);
}
return null;
} catch (e) {
print('Error taking picture: $e');
return null;
}
}

// Chọn nhiều ảnh
Future<List<File>> pickMultipleImages() async {
try {
final List<XFile> images = await _picker.pickMultiImage(
imageQuality: 85,
maxWidth: 1920,
maxHeight: 1080,
);

return images.map((xFile) => File(xFile.path)).toList();
} catch (e) {
print('Error picking multiple images: $e');
return [];
}
}
}

3️⃣ Compress Ảnh Trước Khi Upload

3.1 Compress Image Service

import 'dart:io';
import 'package:image/image.dart' as img;
import 'package:path_provider/path_provider.dart';

class ImageCompressor {
// Compress image và trả về file mới
static Future<File?> compressImage(File imageFile, {int quality = 85}) async {
try {
// Đọc file ảnh
final bytes = await imageFile.readAsBytes();
final image = img.decodeImage(bytes);

if (image == null) return null;

// Resize nếu cần (giữ tỷ lệ)
img.Image resizedImage;
if (image.width > 1920 || image.height > 1080) {
resizedImage = img.copyResize(
image,
width: image.width > image.height ? 1920 : null,
height: image.height > image.width ? 1080 : null,
maintainAspect: true,
);
} else {
resizedImage = image;
}

// Encode lại với chất lượng đã chỉ định
final compressedBytes = img.encodeJpg(resizedImage, quality: quality);

// Lưu vào temporary directory
final tempDir = await getTemporaryDirectory();
final compressedFile = File(
'${tempDir.path}/compressed_${DateTime.now().millisecondsSinceEpoch}.jpg',
);

await compressedFile.writeAsBytes(compressedBytes);

return compressedFile;
} catch (e) {
print('Error compressing image: $e');
return null;
}
}

// Get file size in MB
static Future<double> getFileSizeInMB(File file) async {
final bytes = await file.length();
return bytes / (1024 * 1024);
}
}

3.2 Sử Dụng Compressor

Future<File?> prepareImageForUpload(File originalImage) async {
// Kiểm tra kích thước file
final originalSize = await ImageCompressor.getFileSizeInMB(originalImage);
print('Original size: ${originalSize.toStringAsFixed(2)} MB');

// Nếu file > 2MB, compress
if (originalSize > 2.0) {
final compressedImage = await ImageCompressor.compressImage(
originalImage,
quality: 85,
);

if (compressedImage != null) {
final compressedSize = await ImageCompressor.getFileSizeInMB(compressedImage);
print('Compressed size: ${compressedSize.toStringAsFixed(2)} MB');
return compressedImage;
}
}

return originalImage;
}

4️⃣ Upload Ảnh Với Dio Multipart

4.1 Upload Service Cơ Bản

import 'package:dio/dio.dart';
import 'dart:io';

class ImageUploadService {
final Dio dio;

ImageUploadService(this.dio);

// Upload single image
Future<Map<String, dynamic>> uploadImage(
File imageFile, {
String? fileName,
Map<String, dynamic>? additionalData,
}) async {
try {
// Tạo FormData
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imageFile.path,
filename: fileName ?? 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
),
// Thêm các field khác nếu cần
if (additionalData != null) ...additionalData,
});

// Upload
final response = await dio.post(
'/upload/image',
data: formData,
);

return response.data as Map<String, dynamic>;
} catch (e) {
print('Error uploading image: $e');
rethrow;
}
}
}

4.2 Upload Với Progress Tracking

class ImageUploadService {
final Dio dio;

ImageUploadService(this.dio);

// Upload với progress callback
Future<Map<String, dynamic>> uploadImageWithProgress(
File imageFile, {
String? fileName,
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imageFile.path,
filename: fileName ?? 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
),
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/image',
data: formData,
onSendProgress: (sent, total) {
if (onProgress != null) {
onProgress(sent, total);
}
final progress = (sent / total * 100).toStringAsFixed(0);
print('Upload progress: $progress%');
},
);

return response.data as Map<String, dynamic>;
} catch (e) {
print('Error uploading image: $e');
rethrow;
}
}
}

4.3 Upload Multiple Images

Future<Map<String, dynamic>> uploadMultipleImages(
List<File> imageFiles, {
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
// Tạo list MultipartFile
final multipartFiles = await Future.wait(
imageFiles.asMap().entries.map((entry) async {
final index = entry.key;
final file = entry.value;
return await MultipartFile.fromFile(
file.path,
filename: 'image_$index.jpg',
);
}),
);

final formData = FormData.fromMap({
'images': multipartFiles, // Server sẽ nhận array
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/images',
data: formData,
onSendProgress: (sent, total) {
if (onProgress != null) {
onProgress(sent, total);
}
},
);

return response.data as Map<String, dynamic>;
} catch (e) {
print('Error uploading images: $e');
rethrow;
}
}

5️⃣ Ví Dụ Hoàn Chỉnh: Upload Service

import 'package:dio/dio.dart';
import 'dart:io';
import 'package:image_picker/image_picker.dart';

class ImageUploadService {
final Dio dio;
final ImagePicker _picker = ImagePicker();

ImageUploadService(this.dio);

// Chọn và upload ảnh từ gallery
Future<UploadResult> pickAndUploadFromGallery({
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
bool compress = true,
}) async {
try {
// Chọn ảnh
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
maxWidth: 1920,
maxHeight: 1080,
);

if (image == null) {
return UploadResult(
success: false,
error: 'Không có ảnh được chọn',
);
}

final imageFile = File(image.path);

// Compress nếu cần
File? fileToUpload = imageFile;
if (compress) {
fileToUpload = await ImageCompressor.compressImage(imageFile);
if (fileToUpload == null) {
fileToUpload = imageFile; // Fallback to original
}
}

// Upload
final result = await uploadImage(
fileToUpload!,
onProgress: onProgress,
additionalData: additionalData,
);

return UploadResult(
success: true,
data: result,
);
} catch (e) {
return UploadResult(
success: false,
error: e.toString(),
);
}
}

// Upload image file
Future<Map<String, dynamic>> uploadImage(
File imageFile, {
String? fileName,
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(
imageFile.path,
filename: fileName ?? 'image_${DateTime.now().millisecondsSinceEpoch}.jpg',
),
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/image',
data: formData,
onSendProgress: onProgress,
);

return response.data as Map<String, dynamic>;
} on DioException catch (e) {
throw UploadException(
message: e.message ?? 'Upload failed',
statusCode: e.response?.statusCode,
);
}
}

// Upload multiple images
Future<Map<String, dynamic>> uploadMultipleImages(
List<File> imageFiles, {
Function(int sent, int total)? onProgress,
Map<String, dynamic>? additionalData,
}) async {
try {
final multipartFiles = await Future.wait(
imageFiles.asMap().entries.map((entry) async {
final index = entry.key;
final file = entry.value;
return await MultipartFile.fromFile(
file.path,
filename: 'image_$index.jpg',
);
}),
);

final formData = FormData.fromMap({
'images': multipartFiles,
if (additionalData != null) ...additionalData,
});

final response = await dio.post(
'/upload/images',
data: formData,
onSendProgress: onProgress,
);

return response.data as Map<String, dynamic>;
} on DioException catch (e) {
throw UploadException(
message: e.message ?? 'Upload failed',
statusCode: e.response?.statusCode,
);
}
}
}

// Result class
class UploadResult {
final bool success;
final Map<String, dynamic>? data;
final String? error;

UploadResult({
required this.success,
this.data,
this.error,
});
}

// Custom exception
class UploadException implements Exception {
final String message;
final int? statusCode;

UploadException({required this.message, this.statusCode});

@override
String toString() => message;
}

6️⃣ UI Component: Upload Widget

6.1 Image Upload Widget

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'dart:io';

class ImageUploadWidget extends StatefulWidget {
final ImageUploadService uploadService;
final Function(String imageUrl)? onUploadSuccess;
final Function(String error)? onUploadError;

const ImageUploadWidget({
Key? key,
required this.uploadService,
this.onUploadSuccess,
this.onUploadError,
}) : super(key: key);

@override
_ImageUploadWidgetState createState() => _ImageUploadWidgetState();
}

class _ImageUploadWidgetState extends State<ImageUploadWidget> {
File? _selectedImage;
bool _isUploading = false;
double _uploadProgress = 0.0;

Future<void> _pickImage() async {
final result = await widget.uploadService.pickAndUploadFromGallery(
onProgress: (sent, total) {
setState(() {
_uploadProgress = sent / total;
});
},
compress: true,
);

if (result.success) {
setState(() {
_selectedImage = null; // Clear selected image
_isUploading = false;
_uploadProgress = 0.0;
});

final imageUrl = result.data?['url'] as String?;
if (imageUrl != null && widget.onUploadSuccess != null) {
widget.onUploadSuccess!(imageUrl);
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Upload thành công!')),
);
} else {
setState(() {
_isUploading = false;
_uploadProgress = 0.0;
});

if (widget.onUploadError != null) {
widget.onUploadError!(result.error ?? 'Upload thất bại');
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result.error ?? 'Upload thất bại'),
backgroundColor: Colors.red,
),
);
}
}

Future<void> _selectImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 85,
);

if (image != null) {
setState(() {
_selectedImage = File(image.path);
});
}
}

Future<void> _uploadImage() async {
if (_selectedImage == null) return;

setState(() {
_isUploading = true;
_uploadProgress = 0.0;
});

try {
final result = await widget.uploadService.uploadImage(
_selectedImage!,
onProgress: (sent, total) {
setState(() {
_uploadProgress = sent / total;
});
},
);

setState(() {
_isUploading = false;
_uploadProgress = 0.0;
_selectedImage = null;
});

final imageUrl = result['url'] as String?;
if (imageUrl != null && widget.onUploadSuccess != null) {
widget.onUploadSuccess!(imageUrl);
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Upload thành công!')),
);
} catch (e) {
setState(() {
_isUploading = false;
_uploadProgress = 0.0;
});

if (widget.onUploadError != null) {
widget.onUploadError!(e.toString());
}

ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Upload thất bại: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
}

@override
Widget build(BuildContext context) {
return Column(
children: [
// Preview image
if (_selectedImage != null)
Container(
height: 200,
width: double.infinity,
margin: EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
_selectedImage!,
fit: BoxFit.cover,
),
),
),

// Progress indicator
if (_isUploading)
Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
LinearProgressIndicator(value: _uploadProgress),
SizedBox(height: 8),
Text(
'${(_uploadProgress * 100).toStringAsFixed(0)}%',
style: TextStyle(fontSize: 12),
),
],
),
),

// Buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: _isUploading ? null : _selectImage,
icon: Icon(Icons.image),
label: Text('Chọn ảnh'),
),
SizedBox(width: 16),
if (_selectedImage != null)
ElevatedButton.icon(
onPressed: _isUploading ? null : _uploadImage,
icon: Icon(Icons.cloud_upload),
label: Text('Upload'),
),
],
),
],
);
}
}

7️⃣ Sử Dụng Với State Management (Riverpod)

7.1 Upload Provider

import 'package:flutter_riverpod/flutter_riverpod.dart';

final imageUploadServiceProvider = Provider<ImageUploadService>((ref) {
final dio = ref.watch(dioProvider);
return ImageUploadService(dio);
});

final uploadStateProvider = StateNotifierProvider<UploadNotifier, UploadState>((ref) {
final service = ref.watch(imageUploadServiceProvider);
return UploadNotifier(service);
});

class UploadState {
final File? selectedImage;
final bool isUploading;
final double progress;
final String? imageUrl;
final String? error;

UploadState({
this.selectedImage,
this.isUploading = false,
this.progress = 0.0,
this.imageUrl,
this.error,
});

UploadState copyWith({
File? selectedImage,
bool? isUploading,
double? progress,
String? imageUrl,
String? error,
}) {
return UploadState(
selectedImage: selectedImage ?? this.selectedImage,
isUploading: isUploading ?? this.isUploading,
progress: progress ?? this.progress,
imageUrl: imageUrl ?? this.imageUrl,
error: error,
);
}
}

class UploadNotifier extends StateNotifier<UploadState> {
final ImageUploadService service;

UploadNotifier(this.service) : super(UploadState());

Future<void> selectImage() async {
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);

if (image != null) {
state = state.copyWith(selectedImage: File(image.path));
}
}

Future<void> uploadImage() async {
if (state.selectedImage == null) return;

state = state.copyWith(isUploading: true, progress: 0.0, error: null);

try {
final result = await service.uploadImage(
state.selectedImage!,
onProgress: (sent, total) {
state = state.copyWith(progress: sent / total);
},
);

state = state.copyWith(
isUploading: false,
imageUrl: result['url'] as String?,
selectedImage: null,
);
} catch (e) {
state = state.copyWith(
isUploading: false,
error: e.toString(),
);
}
}
}

8️⃣ Best Practices

8.1 Compress Images

  • ✅ Luôn compress ảnh trước khi upload
  • ✅ Giới hạn kích thước (ví dụ: max 2MB)
  • ✅ Giữ chất lượng hợp lý (85-90%)

8.2 Error Handling

  • ✅ Xử lý lỗi network
  • ✅ Xử lý lỗi server
  • ✅ Hiển thị thông báo rõ ràng cho người dùng

8.3 Progress Tracking

  • ✅ Hiển thị progress bar
  • ✅ Cho phép cancel upload
  • ✅ Thông báo khi upload hoàn thành

8.4 Security

  • ✅ Validate file type
  • ✅ Validate file size
  • ✅ Sanitize file name

9️⃣ Kết Luận

Upload hình ảnh với Dio Multipart giúp bạn:

Upload dễ dàng: Sử dụng FormData và MultipartFile
Theo dõi tiến trình: Progress callback
Upload nhiều ảnh: Hỗ trợ multiple files
Compress ảnh: Giảm kích thước trước khi upload
Error handling: Xử lý lỗi tốt

💡 Lời khuyên: Luôn compress ảnh trước khi upload để tiết kiệm băng thông và thời gian. Sử dụng progress indicator để cải thiện trải nghiệm người dùng.


🎓 Học Sâu Hơn Về Flutter

Muốn master Flutter, File Upload, và các best practices? Tham gia các khóa học tại Hướng Nghiệp Dữ Liệu:

📚 Khóa Học Liên Quan:


📝 Bài viết này được biên soạn bởi đội ngũ Hướng Nghiệp Dữ Liệu. Để cập nhật thêm về Flutter, file upload và các best practices trong phát triển ứng dụng di động, hãy theo dõi blog của chúng tôi.