Flutter & Dio: Uploading Images and Files with Multipart Form
File uploads are a common requirement in many modern applications, from user profile pictures to document sharing. In Flutter, handling network requests efficiently and robustly is crucial. Dio is a powerful HTTP client for Dart that simplifies complex tasks like file uploads, especially when dealing with multipart form data. This article will guide you through the process of uploading images and general files from a Flutter application to a server using Dio and the multipart/form-data content type.
Prerequisites
Before diving in, ensure you have:
- Flutter SDK installed and configured.
- Basic understanding of Flutter widget tree and state management.
- An active internet connection.
- A backend endpoint configured to accept multipart file uploads (e.g., Node.js with Multer, Flask with
request.files, etc.). For this article, we'll assume such an endpoint exists and is listening for POST requests at a specific URL.
Setting Up Dio and File Picker
First, add
dio and
file_picker (to select files from the device) to your
pubspec.yaml file:
dependencies:
flutter:
sdk: flutter
dio: ^5.0.0 # Use the latest stable version
file_picker: ^6.1.1 # Use the latest stable version
After adding them, run
flutter pub get in your project directory.
Understanding Multipart Form Data
When you need to send files along with other textual data (like user IDs, descriptions, etc.) in a single HTTP request,
multipart/form-data is the standard content type. It allows you to encapsulate different parts (each representing a field or a file) within a single request body, separated by boundaries. Dio provides convenient ways to construct this type of request.
Implementing File Upload in Flutter
Let's create a simple Flutter application with a button to pick a file and another to upload it.
1. Create the UI
We'll use a
StatefulWidget to manage the selected file and upload state.
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import 'package:file_picker/file_picker.dart';
import 'dart:io';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter File Upload',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State createState() => _MyHomePageState();
}
class _MyHomePageState extends State {
File? _selectedFile;
double _uploadProgress = 0;
final Dio _dio = Dio(); // Create a Dio instance
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('File Upload with Dio'),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_selectedFile == null
? const Text('No file selected.')
: Text('Selected file: ${_selectedFile!.path.split('/').last}'),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _pickFile,
child: const Text('Pick File'),
),
const SizedBox(height: 20),
_selectedFile != null
? ElevatedButton(
onPressed: _uploadFile,
child: const Text('Upload File'),
)
: Container(),
const SizedBox(height: 20),
_uploadProgress > 0 && _uploadProgress < 1
? LinearProgressIndicator(value: _uploadProgress)
: Container(),
_uploadProgress == 1
? const Text('Upload complete!')
: Container(),
_uploadProgress == -1 // Custom value for error
? const Text('Upload failed!', style: TextStyle(color: Colors.red))
: Container(),
],
),
),
),
);
}
// ... (methods will be added here)
}
2. Picking a File
We'll use the
file_picker package to allow users to select any type of file.
Future _pickFile() async {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.any, // You can specify FileType.image, FileType.video, etc.
);
if (result != null && result.files.single.path != null) {
setState(() {
_selectedFile = File(result.files.single.path!);
_uploadProgress = 0; // Reset progress when a new file is picked
});
} else {
// User canceled the picker
setState(() {
_selectedFile = null;
_uploadProgress = 0;
});
}
}
3. Constructing FormData and Uploading
This is the core logic where Dio comes into play. We'll create a
FormData object, add our file, and any other data, then send it via a POST request.
Future _uploadFile() async {
if (_selectedFile == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a file first.')),
);
return;
}
setState(() {
_uploadProgress = 0; // Start progress at 0
});
try {
String fileName = _selectedFile!.path.split('/').last;
FormData formData = FormData.fromMap({
"file": await MultipartFile.fromFile(
_selectedFile!.path,
filename: fileName,
),
"description": "This is a file uploaded from Flutter.", // Example of adding other fields
"userId": 123,
});
// Replace with your actual backend endpoint URL
String uploadUrl = "YOUR_BACKEND_UPLOAD_URL_HERE";
Response response = await _dio.post(
uploadUrl,
data: formData,
onSendProgress: (received, total) {
if (total != -1) {
setState(() {
_uploadProgress = received / total;
print('Upload progress: ${_uploadProgress * 100}%'); // For debugging
});
}
},
);
if (response.statusCode == 200 || response.statusCode == 201) {
setState(() {
_uploadProgress = 1; // Mark as complete
_selectedFile = null; // Clear selected file after successful upload
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('File uploaded successfully!')),
);
print("File uploaded successfully: ${response.data}");
} else {
setState(() {
_uploadProgress = -1; // Mark as failed
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('File upload failed: ${response.statusCode}')),
);
print("File upload failed with status: ${response.statusCode}");
}
} on DioException catch (e) {
setState(() {
_uploadProgress = -1; // Mark as failed
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Upload error: ${e.message}')),
);
print("Upload error: $e");
} catch (e) {
setState(() {
_uploadProgress = -1; // Mark as failed
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An unexpected error occurred: $e')),
);
print("An unexpected error occurred: $e");
}
}
Important Notes for Backend
Ensure your backend is configured to accept POST requests to the specified
uploadUrl.
- The backend should expect the file under the field name
"file" (matching what you set in FormData.fromMap).
- It should also be able to parse
multipart/form-data and extract other fields like "description" and "userId".
- Example for Node.js with
express and multer (install with npm install express multer):
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
const port = 3000;
// Set up storage for uploaded files
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/') // Files will be saved in the 'uploads/' directory
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname) // Unique filename
}
});
const upload = multer({ storage: storage });
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded.');
}
console.log('File received:', req.file);
console.log('Other fields:', req.body); // Access other fields like description, userId
res.status(200).json({
message: 'File uploaded successfully!',
filename: req.file.filename,
description: req.body.description,
userId: req.body.userId
});
});
app.listen(port, () => console.log(`Server running on http://localhost:${port}/`));
Advanced Considerations
- Multiple File Uploads: You can add multiple
MultipartFile instances to the FormData object with different field names or an array of files if your backend supports it.
- Request Cancellation: Dio allows you to cancel ongoing requests using
CancelToken.
- Error Handling: Implement more granular error handling based on status codes or specific Dio exceptions.
- Loading States: Enhance the UI with proper loading indicators, disabling buttons during upload, etc.
- File Type Validation: Before uploading, you can add client-side validation for file types and sizes.
Conclusion
Uploading images and files in Flutter using Dio and multipart form data is a straightforward and robust process. By leveraging
file_picker for selecting files and Dio's
FormData for constructing the request, you can efficiently send various types of files along with additional data to your backend. Remember to configure your backend endpoint correctly to handle these multipart requests. This setup provides a solid foundation for integrating file upload capabilities into your Flutter applications.