简体   繁体   中英

Uploading a file in an array of object using Retrofit 2

I use Retrofit2 and I need to upload various files using file in an array of objects Media like this:

{
   "state" = "done",
   "medias" = [
     {
      "file" = THE_FILE1
     },
     {
      "file" = THE_FILE2
     },
     {
      "file" = THE_FILE3
     }
   ]
}

This is the function of my Interface :

@Multipart
@POST("api/exercice/{id}")
fun submitExercice(
    @Path("id") id: Int,
    @Header("Authorization") token: String,
    @Body data: AnswerExercice
): Call<Void>

And this is my object Media :

    data class AnswerExercice(
    val state: String = "done",
    val medias: List<Media>
) : Serializable {
    data class Media(
        @Part val file: MultipartBody.Part
    ) : Serializable
}

But I have this error:

@Body parameters cannot be used with form or multi-part encoding. (parameter #3)

What am i not doing well?

This is what the API documentation say: 达

The result have to be like this: 在此处输入图像描述

Solution 1
If you like to send your data exactly like the structure you mentioned, you should convert files content to Base64 and wrap them in a serializable class and post it as the body. Here is the sample of wrapper class:

data class AnswerExerciceBase64(val state: String, val medias: List<Media>) : Serializable

data class Media(val file: Base64File) : Serializable

class Base64File(file: File) : Serializable {

    val name: String
    val content: String

    init {
        name = file.name
        content = Base64.encodeToString(FileInputStream(file).readBytes(), Base64.DEFAULT)
    }
}

And your Api is like this:

@POST("api/exercice/{id}")
fun submitExercice(
        @Path("id") id: Int,
        @Header("Authorization") token: String,
        @Body data: AnswerExerciceBase64
): Call<Void>

Then posted data to server will be like below:

{
    "state": "this is state",
    "medias": [{
        "file": {
            "content": "Base64 file content",
            "name": "f1.txt"
        }
    }, {
        "file": {
            "content": "Base64 file content",
            "name": "f2.txt"
        }
    }, {
        "file": {
            "content": "Base64 file content",
            "name": "f3.txt"
        }
    }]
}

This approach is so close to what you want but you should know you must decode files content on the server-side by yourself, so you need more effort on the server-side.


Solution 2
It's better to use multipart/form-data to upload files and data. Based on "Is it possible to have a nested MultipartEntities or FormBodyPart in a multipart POST?" question and its answer, multipart/form-data has a flat structure and there is no hierarchy, so you can't have desired data structure but you can still pass all of the inputs to Api through a single object.
According to this article , you can send multiple files in a List, so if your Api be like this

@Multipart
@POST("post")
fun submitExercice(@Part data: List<MultipartBody.Part>): Call<ResponseBody>

then you will be able to upload multiple files. You just need to create a List of MultipartBody.Part and add your files to it like below:

list.add(MultipartBody.Part.createFormData(name, fileName, RequestBody.create(mediaType, file)))

Now you must add the state parameter to this list. You can do it like this:

list.add(MultipartBody.Part.createFormData("state", state))

I developed a class that handles all this stuff. You can use it.

class AnswerExerciceList(state: String) : ArrayList<MultipartBody.Part>() {

    init {
        add(MultipartBody.Part.createFormData("state", state))
    }

    fun addFile(name: String, fileName: String, mediaType: MediaType?, file: File) {
        add(MultipartBody.Part.createFormData(name, fileName,
                RequestBody.create(mediaType, file)))
    }
}

You can create an instance of this class, add your files and then pass it to the submitExercice Api method as input.

Update
This answer is based on your Api documnetation. I tested my answer and the example that you mentioned in your question via https://postman-echo.com and result was the same. Please try the following code snippet: Api

@Multipart
@POST("api/exercice/{id}")
fun submitExercice(@Path("id") id: Int,
                   @Header("Authorization") authorization: String,
                   @Part("answer") answer: String,
                   @Part medias: List<MultipartBody.Part>,
                   @Part("state") state: String): Call<ResponseBody>

Media Class

data class Media(val urlVidel: String, val file: File?, val mediaType: MediaType?) {
    companion object {
        fun mediaListToMultipart(mediaList: List<Media>): List<MultipartBody.Part> {
            val list = ArrayList<MultipartBody.Part>()
            for (i in mediaList.indices) {
                mediaList[i].let {
                    if (!TextUtils.isEmpty(it.urlVidel))
                        list.add(MultipartBody.Part.createFormData("medias[$i][urlVideo]", it.urlVidel))
                    if (it.file != null) {
                        val requestFile = RequestBody.create(
                                it.mediaType,
                                it.file
                        )
                        list.add(MultipartBody.Part.createFormData("medias[$i][file]", it.file.getName(), requestFile))
                    }
                }
            }
            return list
        }
    }
}

and then call Api like this:

ApiHelper.Instance.submitExercice(1, "Authorization Token", "Answer", Media.mediaListToMultipart(mediaList), "State").enqueue(callback)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM