June 12, 2026
Flutter File Uploads Made Easy: Working with Multipart APIs
File uploads are one of the most common requirements in real-world Flutter applications — whether it’s uploading profile images, documents…
CodeX Lancers
2 min read
File uploads are one of the most common requirements in real-world Flutter applications — whether it's uploading profile images, documents, or media files.
At our company, we've implemented file upload systems across multiple production apps, and one thing is clear: Most developers don't struggle with the concept — they struggle with clean implementation.
In this article, we'll break down Multipart file uploads in Flutter using the latest stable packages, in a simple and production-ready way.
Why Multipart Requests Matter
When sending data to APIs:
- JSON works for text data
- But files (images, PDFs, videos) require multipart/form-data
This format allows sending:
- Normal fields (userId, email, etc.)
- Files (binary data)
in a single request.
Real-World Use Cases
We've used multipart uploads in:
- Profile image upload
- KYC verification documents
- E-commerce product images
- Chat attachments
- Resume uploads
Dependencies (Latest Versions)
dependencies:
http: ^1.6.0
image_picker: ^1.2.1
path: ^1.9.1
dependencies:
http: ^1.6.0
image_picker: ^1.2.1
path: ^1.9.1
Step 1: Picking an Image (Latest image_picker usage)
With the latest image_picker, the API remains stable, but we ensure null safety + proper typing.
import 'package:image_picker/image_picker.dart';
final ImagePicker _picker = ImagePicker();
Future<XFile?> pickImage() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
return image;
} catch (e) {
print("Error picking image: $e");
return null;
}
}import 'package:image_picker/image_picker.dart';
final ImagePicker _picker = ImagePicker();
Future<XFile?> pickImage() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 80,
);
return image;
} catch (e) {
print("Error picking image: $e");
return null;
}
}Step 2: Multipart Upload Using http
The http ^1.6.0 package still uses MultipartRequest, but we'll structure it more cleanly and safely.
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
Future<void> uploadFile(XFile file) async {
try {
var uri = Uri.parse("https://yourapi.com/upload");
var request = http.MultipartRequest("POST", uri);
// Optional fields
request.fields['userId'] = "12345";
// File name handling using latest path package
String fileName = path.basename(file.path);
// Attach file
request.files.add(
await http.MultipartFile.fromPath(
'file',
file.path,
filename: fileName,
),
);
// Send request
var streamedResponse = await request.send();
// Convert response stream
var response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
print("Upload successful");
print("Response: ${response.body}");
} else {
print("Upload failed: ${response.statusCode}");
print("Response: ${response.body}");
}
} catch (e) {
print("Upload error: $e");
}
}import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
Future<void> uploadFile(XFile file) async {
try {
var uri = Uri.parse("https://yourapi.com/upload");
var request = http.MultipartRequest("POST", uri);
// Optional fields
request.fields['userId'] = "12345";
// File name handling using latest path package
String fileName = path.basename(file.path);
// Attach file
request.files.add(
await http.MultipartFile.fromPath(
'file',
file.path,
filename: fileName,
),
);
// Send request
var streamedResponse = await request.send();
// Convert response stream
var response = await http.Response.fromStream(streamedResponse);
if (response.statusCode == 200) {
print("Upload successful");
print("Response: ${response.body}");
} else {
print("Upload failed: ${response.statusCode}");
print("Response: ${response.body}");
}
} catch (e) {
print("Upload error: $e");
}
}Step 3: UI Integration
ElevatedButton(
onPressed: () async {
final file = await pickImage();
if (file != null) {
await uploadFile(file);
}
},
child: const Text("Upload File"),
)ElevatedButton(
onPressed: () async {
final file = await pickImage();
if (file != null) {
await uploadFile(file);
}
},
child: const Text("Upload File"),
)Important Validation (Production Ready)
Always validate file size before uploading:
if (file.lengthSync() > 10 * 1024 * 1024) {
print("File too large (Max 10MB allowed)");
return;
}if (file.lengthSync() > 10 * 1024 * 1024) {
print("File too large (Max 10MB allowed)");
return;
}What Changed in Latest Approach?
With updated packages, best practice improvements are:
✔ Proper response conversion
http.Response.fromStream(streamedResponse)http.Response.fromStream(streamedResponse)✔ Safe filename extraction
path.basename(file.path)path.basename(file.path)Common Mistakes Developers Still Make
1. Not converting streamed response
Multipart responses come as streams — ignoring this causes unreadable output.
2. Missing filename in latest APIs
Some servers reject files without explicit filenames.
3. No error handling around picker
Image picker can fail or be cancelled.
4. Uploading oversized files
Always validate before sending.
Best Practices We Follow
At scale, we always ensure:
- Validate file before upload
- Use proper filename handling
- Convert response streams properly
- Handle cancel cases in UI
- Keep upload logic separated from UI
- Use HTTPS only
Final Thoughts
File upload in Flutter isn't complicated — but doing it the right way makes a big difference in real applications.
Once you understand how multipart requests work, everything becomes predictable: picking a file, attaching it to a request, and sending it securely to the server.
With Flutter's latest stable packages like http, image_picker, and path, we can build a clean and reliable upload system without adding unnecessary complexity.
At our company, this exact approach is used across multiple production apps where performance and reliability matter the most.