Kotlin使用Retrofit Multipart上传图片到服务器异常解决方案
Kotlin 基于 Retrofit 实现 Multipart 图片上传正确实现
参数为null、请求格式异常问题基本集中在依赖配置、接口定义、Part构造三个环节,按以下步骤配置即可正常运行:
1. 基础依赖配置
先确保引入正确版本的依赖,不要混用不同大版本的Retrofit/OkHttp:
// app模块build.gradle.kts 依赖示例 dependencies { implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") }
2. 上传接口定义
注意:所有RequestBody、MultipartBody.Part 必须使用
com.squareup.okhttp3包下的类,不要引入其他同名类。
interface UploadApi { // 必须给方法加@Multipart注解,不能用@Body、@Field传Multipart参数 @Multipart @POST("your/upload/api/path") // 替换为自身业务的上传接口路径 suspend fun uploadImage( // 同传的普通文本参数,用@Part标注参数名,传入RequestBody类型 @Part("userId") userId: RequestBody, // 图片参数直接传MultipartBody.Part,不要额外给@Part加key值 @Part imagePart: MultipartBody.Part ): Response<UploadResultBean> // 替换为自身业务的返回实体类 }
接口定义高频错误写法
- 给
MultipartBody.Part类型的参数额外指定@Part的key值,会导致表单key重复,服务端无法识别 - 直接传File、Uri、ByteArray类型作为上传参数,未转换为RequestBody/MultipartBody.Part,会被序列化器处理为null
- 手动在接口/拦截器中添加固定的
Content-Type: multipart/form-data请求头,会覆盖Retrofit自动生成的带boundary分隔符的合法头,直接导致请求格式解析失败
3. 构造上传参数的正确方法
封装通用转换方法,覆盖本地文件、系统Uri两种常见取图场景:
import android.content.Context import android.net.Uri import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File // 普通文本参数转RequestBody fun String.toPlainTextRequestBody(): RequestBody { return this.toRequestBody("text/plain".toMediaTypeOrNull()) } // 本地图片文件转MultipartPart,partKey需要和服务端要求的参数名完全一致 fun File.toImageMultipartPart(partKey: String = "image"): MultipartBody.Part { val imageRequestBody = this.asRequestBody("image/*".toMediaTypeOrNull()) return MultipartBody.Part.createFormData(partKey, this.name, imageRequestBody) } // Android系统Uri转MultipartPart(适配分区存储场景) fun Uri.toImageMultipartPart(context: Context, partKey: String = "image"): MultipartBody.Part? { val contentResolver = context.contentResolver val inputStream = contentResolver.openInputStream(this) ?: return null val fileBytes = inputStream.readBytes() inputStream.close() // 读取原始文件名 val fileName = contentResolver.query(this, null, null, null, null)?.use { cursor -> val nameColumnIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME) cursor.moveToFirst() cursor.getString(nameColumnIndex) } ?: "upload_image_${System.currentTimeMillis()}.jpg" val imageRequestBody = fileBytes.toRequestBody("image/*".toMediaTypeOrNull()) return MultipartBody.Part.createFormData(partKey, fileName, imageRequestBody) }
4. 初始化与请求发起示例
// 初始化OkHttp,可添加日志拦截器排查请求参数问题 val okHttpClient = OkHttpClient.Builder() .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) .build() // 初始化Retrofit,baseUrl必须以/结尾 val retrofit = Retrofit.Builder() .baseUrl("https://your.server.base.url/") // 替换为自身服务端域名 .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() val uploadApi = retrofit.create(UploadApi::class.java) // 协程作用域内发起请求,注意声明网络权限、切换IO线程 lifecycleScope.launch(Dispatchers.IO) { runCatching { val userIdBody = "10086".toPlainTextRequestBody() // 本地文件场景 val localImage = File("/storage/emulated/0/Pictures/demo.jpg") val imagePart = localImage.toImageMultipartPart() // 相册选图Uri场景替换为:val imagePart = selectedUri.toImageMultipartPart(context) ?: return@launch uploadApi.uploadImage(userIdBody, imagePart) }.onSuccess { response -> if (response.isSuccessful) { // 上传成功逻辑 } else { // 上传失败,读取错误信息 val errorInfo = response.errorBody()?.string() } }.onFailure { it.printStackTrace() } }
常见问题排查
- 图片参数为null:确认MultipartPart构造时传入的partKey和服务端要求完全一致(大小写敏感),确认没有直接传File/Uri/ByteArray给接口
- 请求格式异常:检查是否手动添加了固定的multipart/form-data请求头,检查自定义拦截器是否修改了请求体内容
- 服务端提示文件损坏:检查读取文件流时是否提前关闭流、字节数组读取长度是否完整
内容的提问来源于stack exchange,提问作者Ahmed Mamdouh
相关产品推荐
相关产品推荐

