You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Kotlin使用MultipartBody上传图片至服务器失败排查

图片上传至服务器失败,服务器接收为null的排查与解决

问题背景

尝试将用户存储中的图片发送至服务器,但图片上传失败,服务器端接收的图片为null。其他数据可正常接收。具体流程是从Fragment页面获取图片Uri并传入ViewModel,将其转换为真实路径并创建文件,按要求将图片调整为1:1比例后转为MultipartBody.Part格式,但服务器端无法接收到图片,不清楚问题所在。

相关代码

ViewModel代码

class AddProductViewModel(application: Application) : AndroidViewModel(application) {

    var _apiLiveData = MutableLiveData<Resource<AddProductResponse>>()
    val apiLiveData: MutableLiveData<Resource<AddProductResponse>> get() = _apiLiveData

    fun addProduct(
        productName: String,
        productType: String,
        price: String,
        tax: String,
        imageUri: Uri? = null
    ) {

        _apiLiveData.postValue(Resource.Loading)

        viewModelScope.launch {
            val imagePart = if (imageUri != null) {
                val imageFilePath = context.getRealPathFromURI(imageUri)
                fileToMultipartBodyPart(imageFilePath!!)
            } else {
                null
            }

            val apiInterface =
                RetrofitInstance.getRetrofitInstance().create(ApiInterface::class.java)
            apiInterface.addProductData(
                toRequestBody(productName),
                toRequestBody(productType),
                toRequestBody(price),
                toRequestBody(tax),
                listOfNotNull(imagePart)
            )
                .enqueue(
                    object : Callback<AddProductResponse> {
                        override fun onResponse(
                            call: Call<AddProductResponse>,
                            response: Response<AddProductResponse>
                        ) {
                            if (response.isSuccessful) {
                                if (response.body()!!.message == "Product added Successfully!") {
                                    _apiLiveData.postValue(Resource.Success(response.body()!!))
                                } else
                                    _apiLiveData.postValue(
                                        Resource.Failure(
                                            false,
                                            0,
                                            response.body()!!.message
                                        )
                                    )
                            }
                        }

                        override fun onFailure(call: Call<AddProductResponse>, t: Throwable) {
                            _apiLiveData.postValue(Resource.Failure(true, 0, "Network Error"))
                        }
                    }
                )
        }
    }

    private val context: Context
        get() = getApplication<Application>()

    private fun toRequestBody(value: String): RequestBody {
        return RequestBody.create("text/plain".toMediaTypeOrNull(), value)
    }

    private fun Context.getRealPathFromURI(uri: Uri): String? {
        val projection = arrayOf(MediaStore.Images.Media.DATA)
        val cursor: Cursor? = contentResolver.query(uri, projection, null, null, null)
        return cursor?.use {
            val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
            it.moveToFirst()
            it.getString(columnIndex)
        }
    }

    private fun fileToMultipartBodyPart(filePath: String): MultipartBody.Part {
        // Load the original image file
        val originalFile = File(filePath)

        // Calculate the desired width and height for the 1:1 aspect ratio
        val desiredWidth = 500 // Set your desired width
        val desiredHeight = 500 // Set your desired height

        // Resize the image
        val resizedBitmap = Bitmap.createScaledBitmap(
            BitmapFactory.decodeFile(originalFile.absolutePath),
            desiredWidth,
            desiredHeight,
            false
        )

        // Save the resized bitmap to a new file
        val resizedFile = File(context.cacheDir, "resized_image.jpg")
        val fileOutputStream = FileOutputStream(resizedFile)
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream)
        fileOutputStream.close()

        // Create a RequestBody from the resized file
        val requestFile = RequestBody.create("multipart/form-data".toMediaTypeOrNull(), resizedFile)

        // Create MultipartBody.Part
        return MultipartBody.Part.createFormData("files", resizedFile.name, requestFile)
    }
}

ApiInterface代码

interface ApiInterface {

    @GET("get")
    fun getProductData(): Call<List<ProductResponse>>

    @Multipart
    @POST("add")
    fun addProductData(
        @Part("product_name") productName: RequestBody,
        @Part("product_type") productType: RequestBody,
        @Part("price") price: RequestBody,
        @Part("tax") tax: RequestBody,
        @Part files: List<MultipartBody.Part>? = null
    ): Call<AddProductResponse>
}

Fragment代码片段

uploadImage.setOnClickListener {
    pickImageFromGallery()
}

submitBtn.setOnClickListener {
    if (editTextProductName.text!!.isEmpty() || editTextProductTax.text!!.isEmpty()
        || editTextProductSellingPrice.text!!.isEmpty()
    ) {
        Toast.makeText(context, "One or more fields empty!!", Toast.LENGTH_LONG).show()
    } else {
        if (selectedImageUri != null) {
            MultipartBody.Builder().setType(MultipartBody.FORM).apply {
                addFormDataPart("product_name", editTextProductName.text.toString())
                addFormDataPart("product_type", productTypeSpinner.selectedItem.toString())
                addFormDataPart("price", editTextProductSellingPrice.text.toString())
                addFormDataPart("tax", editTextProductTax.text.toString())
            }.build()

            viewModel.addProduct(
                editTextProductName.text.toString(),
                productTypeSpinner.selectedItem.toString(),
                editTextProductSellingPrice.text.toString(),
                editTextProductTax.text.toString(),
                selectedImageUri// Pass the MultipartBody parts
            )
        }
    }
}

private fun pickImageFromGallery() {
    val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
    startActivityForResult(intent, PICK_IMAGE_REQUEST)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == PICK_IMAGE_REQUEST && resultCode == Activity.RESULT_OK && data != null) {
        selectedImageUri = data.data
        Toast.makeText(requireActivity(), selectedImageUri.toString(), Toast.LENGTH_LONG).show()
    }
}

问题排查与修复方案

1. 废弃的真实路径获取方法

MediaStore.Images.Media.DATA 在Android 10及以上已被废弃,且无法兼容所有Uri类型(比如第三方应用返回的内容Uri),会导致获取路径失败,后续文件处理为空。

修复:直接通过ContentResolver读取Uri对应的文件流,跳过真实路径获取:

private fun uriToMultipartBodyPart(uri: Uri): MultipartBody.Part? {
    return try {
        val inputStream = context.contentResolver.openInputStream(uri)
        val originalBitmap = BitmapFactory.decodeStream(inputStream)
        inputStream?.close()

        // 调整为1:1比例
        val desiredSize = 500
        val resizedBitmap = Bitmap.createScaledBitmap(originalBitmap, desiredSize, desiredSize, true)

        // 保存到缓存文件(加时间戳避免重名覆盖)
        val resizedFile = File(context.cacheDir, "resized_image_${System.currentTimeMillis()}.jpg")
        val outputStream = FileOutputStream(resizedFile)
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
        outputStream.close()

        // 使用正确的图片MIME类型
        val requestFile = RequestBody.create("image/jpeg".toMediaTypeOrNull(), resizedFile)
        MultipartBody.Part.createFormData("files", resizedFile.name, requestFile)
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

在addProduct方法中替换原逻辑:

val imagePart = imageUri?.let { uriToMultipartBodyPart(it) }

2. RequestBody媒体类型错误

当前代码用multipart/form-data作为图片RequestBody的类型,正确的应该是图片对应的MIME类型(如image/jpeg),否则服务器无法识别文件,导致解析为null。

修复:如上面的代码所示,将图片RequestBody的类型改为image/jpeg。

3. Retrofit接口List参数兼容性问题

部分服务器对List<MultipartBody.Part>的解析支持不佳,如果仅上传单张图片,建议改为单个MultipartBody.Part参数:

修改ApiInterface:

@Multipart
@POST("add")
fun addProductData(
    @Part("product_name") productName: RequestBody,
    @Part("product_type") productType: RequestBody,
    @Part("price") price: RequestBody,
    @Part("tax") tax: RequestBody,
    @Part files: MultipartBody.Part? = null
): Call<AddProductResponse>

调用时直接传imagePart,而非listOfNotNull(imagePart)。

4. Fragment冗余代码清理

Fragment中创建的MultipartBody.Builder未实际使用,属于冗余代码,直接删除即可。

5. 权限检查

Android 13及以上版本需要申请READ_MEDIA_IMAGES权限,确保Manifest中已声明,并在运行时动态申请,否则无法读取图片文件。

验证步骤

  1. 打印调整后的图片文件大小,确认文件已正确生成(大小不为0)。
  2. 用OkHttp拦截器或Charles抓包,查看Multipart请求体中是否包含图片字段。
  3. 核对服务器端接收参数的字段名,确保和客户端createFormData("files", ...)中的名称完全一致。

内容的提问来源于stack exchange,提问作者Thomas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 13:22:02