Problem faced with uploading image,video,audio,file

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;
    }
  }

Welcome to the community, @zahid08! You can get the source code of the Collect Android App's here:

1 Like

It looks like you’ve done a solid job replicating KoBoCollect form submission! For media file uploads, KoBoToolbox expects multipart/form-data with the exact field name specified, which it looks like you’ve handled correctly in uploadMediaFiles().

You might also want to double-check that the media field names exactly match what the KoBo form expects. Incorrect or mismatched field names often result in files not linking correctly to submissions.

Also, ensure that the media file upload endpoint is supported by your KoBoToolbox account setup—especially for self-hosted or custom deployments.

It looks like you’ve done a solid job replicating KoBoCollect form submission! For media file uploads, KoBoToolbox expects multipart/form-data with the exact field name specified, which it looks like you’ve handled correctly in uploadMediaFiles().
You might also want to double-check that the media field names exactly match what the KoBo form expects. Incorrect or mismatched field names often result in files not linking correctly to submissions.
Also, ensure that the media file upload endpoint is supported by your KoBoToolbox account setup—especially for self-hosted or custom deployments.