Skip to main content

One post tagged with "File Upload"

View All Tags

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.