I am building a app like kobocollect in flutter. Form data submitting well but image,video,audio,file is not submitting like kobocollect app. Is there are any other api to work with for media?
My code:
Future<void> submit() async {
final now = DateTime.now();
final offset = now.timeZoneOffset;
final offsetHours = offset.inHours;
final offsetMinutes = offset.inMinutes.remainder(60);
final offsetString =
'${offsetHours >= 0 ? '+' : '-'}${offsetHours.abs().toString().padLeft(2, '0')}:${offsetMinutes.abs().toString().padLeft(2, '0')}';
answers['end'] = '${now.toIso8601String().substring(0, 23)}$offsetString';
print(answers['end']);
final uuid = Uuid();
final instanceID = 'uuid:${uuid.v4()}';
answers['meta/instanceID'] = instanceID;
// KoBoToolbox credentials
const username = 'zahid08'; // Replace with your actual username
const password = 'zahid8135'; // Replace with your actual password
const submissionURL = 'https://kc.kobotoolbox.org/api/v1/submissions';
// Show loading indicator
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Submitting form...'),
duration: Duration(seconds: 10),
),
);
try {
// Get form metadata using formUid
final formMetaData = await getFormMetaData(
widget.formUid,
username,
password,
);
if (formMetaData == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to retrieve form metadata.'),
backgroundColor: Colors.red,
),
);
return;
}
// ๐ฅ KEY FIX: Format data structure like your working test code
final formattedData = formatAnswersForSubmission(answers);
// Add required form metadata
formattedData['id'] = formMetaData['formId']!;
formattedData['submission']['formhub'] = {
'uuid': formMetaData['formUUID']!,
};
debugPrint('Formatted submission data: ${jsonEncode(formattedData)}');
// Submit data to KoBoToolbox
final response = await http.post(
Uri.parse(submissionURL),
headers: {
'Content-Type': 'application/json',
'Authorization':
'Basic ${base64Encode(utf8.encode('$username:$password'))}',
},
body: jsonEncode(formattedData), // Send single object, not array
);
if (response.statusCode == 201) {
debugPrint('Data submission successful: ${response.body}');
// Extract submission UUID for potential media uploads
final responseBody = jsonDecode(response.body);
final submissionUUID = responseBody['uuid'];
// Upload media files if any exist
if (submissionUUID != null) {
await uploadMediaFiles(answers, submissionUUID, username, password);
}
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Form submitted successfully!'),
backgroundColor: Colors.green,
),
);
// Navigate back or reset form
Navigator.pop(context);
} else {
debugPrint(
'Submission failed [${response.statusCode}]: ${response.body}',
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Submission failed: ${response.statusCode}'),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
debugPrint('Error during submission: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error during submission: $e'),
backgroundColor: Colors.red,
),
);
}
}
// ๐ฅ NEW METHOD: Format answers to match KoBoToolbox expected structure
Map<String, dynamic> formatAnswersForSubmission(
Map<String, dynamic> answers,
) {
final Map<String, dynamic> formattedData = {'submission': {}};
answers.forEach((key, value) {
if (key.contains('/')) {
// Handle nested keys like "meta/instanceID" or "group/subfield"
final parts = key.split('/');
final mainKey = parts[0];
final subKey = parts[1];
// Create nested structure
formattedData['submission'][mainKey] ??= {};
formattedData['submission'][mainKey][subKey] = value;
} else {
// Handle flat keys
formattedData['submission'][key] = value;
}
});
return formattedData;
}
// ๐ฅ NEW METHOD: Upload media files (images, videos, audio, etc.)
Future<void> uploadMediaFiles(
Map<String, dynamic> answers,
String submissionUUID,
String username,
String password,
) async {
// Define which fields might contain media files
final mediaFields = <String>[];
// Check for media file paths in answers
answers.forEach((key, value) {
if (value is String && value.isNotEmpty) {
// Check if the value looks like a file path
if (value.contains('/') &&
(value.endsWith('.jpg') ||
value.endsWith('.jpeg') ||
value.endsWith('.png') ||
value.endsWith('.mp4') ||
value.endsWith('.mp3') ||
value.endsWith('.wav') ||
value.endsWith('.pdf') ||
value.endsWith('.doc') ||
value.endsWith('.docx'))) {
mediaFields.add(key);
}
}
});
if (mediaFields.isEmpty) {
debugPrint('No media files to upload');
return;
}
final auth = base64Encode(utf8.encode('$username:$password'));
for (var field in mediaFields) {
final filePath = answers[field];
if (filePath != null && filePath.toString().isNotEmpty) {
try {
final file = File(filePath.toString());
if (await file.exists()) {
debugPrint('Uploading media: $filePath');
final url = Uri.parse(
'https://kc.kobotoolbox.org/api/v1/submissions/$submissionUUID/attachments',
);
var request =
http.MultipartRequest('POST', url)
..headers['Authorization'] = 'Basic $auth'
..fields['field'] =
field // Critical: must include the field name
..files.add(
await http.MultipartFile.fromPath(
'file', // Must be 'file'
filePath.toString(),
filename: filePath.toString().split('/').last,
),
);
final response = await request.send();
if (response.statusCode == 201) {
debugPrint(
'โ
Media file uploaded successfully for field: $field',
);
} else {
final responseBody = await response.stream.bytesToString();
debugPrint(
'โ Media upload failed [${response.statusCode}]: $responseBody',
);
}
} else {
debugPrint('โ File not found: $filePath');
}
} catch (e) {
debugPrint('โ Error uploading media for field $field: $e');
}
}
}
}
// ๐ฅ UPDATED METHOD: Find form by UID instead of title
Future<Map<String, String>?> getFormMetaData(
String formUid,
String username,
String password,
) async {
final url = Uri.parse(
'https://kc.kobotoolbox.org/api/v1/forms?format=json',
);
final auth = base64Encode(utf8.encode('$username:$password'));
try {
final response = await http.get(
url,
headers: {'Authorization': 'Basic $auth'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
for (var form in data) {
// ๐ฅ FIXED: Check both possible UID fields
if (form['kpi_asset_uid'] == formUid || form['uuid'] == formUid) {
debugPrint('Form metadata found for UUID: $formUid');
return {
'formId': form['id_string'].toString(),
'formUUID': form['uuid'].toString(),
};
}
}
debugPrint("Form with UUID '$formUid' not found.");
debugPrint(
"Available forms: ${data.map((f) => '${f['title']} (${f['kpi_asset_uid'] ?? f['uuid']})').join(', ')}",
);
return null;
} else {
debugPrint('Failed to retrieve form metadata: ${response.statusCode}');
debugPrint('Response body: ${response.body}');
return null;
}
} catch (e) {
debugPrint('Error retrieving form metadata: $e');
return null;
}
}