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