Background
In the past few years i’ve been heavily involved in building Android applications especially using Retrofit, the popular open source HTTP Client for Android and Java.
You can send a
POST
/ PUT
request by either submitting a body depending on the API Content Type Form Data , Form URL Encoded or using JSON. To submit a JSON object you can use the @SerializedName
to specify how to send each field data in the request. However I encountered an interesting challenge when an API Endpoint required pushing Multipart Form Data with an Image. To make it more interesting I was required to submit an uploaded image with the data. I had to figure out how to build the request that accepted data as multipart/formdata
and personally found the lack of comprehensive resources on the internet lacking. So i wrote this blogpost to share in my learnings.Introduction
Lets suppose that we’re trying to send in a survey completed survey to a server in an Android app that accepts a document(image) and some textual data. The Survey Share service takes the following Input :-
- Name - apiField(”name”)
- Email - apiField(”email”)
- Phone Number - apiField(”phoneNumber”)
- Image - apiField(”image”)
- Array of Ratings - apiField(”ratings”)
We can imagine this data is stored into these data class objects
//SurveyResponse.kt data class SurveyResponse( val name:String, val email:String, val phoneNumber:String, val image:String var ratings:List<Rating> ) //Rating.kt data class Rating( val ratingNameId:Int val ratingScoreId:Int )
For us to begin sending this data , we’ll need to create a
MutableMap
collection that consists of the API Field name as the key and the respective value from the SurveyResponse object. This will look like this.val map: MutableMap<String, RequestBody> = mutableMapOf()
The RequestBody value will have to be created for every value submitted. We can create an extension function to handle this for us.
// Extensions.kt fun createPartFromString(stringData: String): RequestBody { return stringData.toRequestBody("text/plain".toMediaTypeOrNull()) }
This assumes that all data submitted is plain text object hence the
text/plain
specification.Submit Text Data
We already know what data to represent the key in the
map
collection. We can now prepare the map value which has a RequestBody
field . This will look like below:val name = createPartFromString(name) val email = createPartFromString(email) val phoneNumber = createPartFromString(phoneNumber)
Once we are done. We can submit to the map object like below:
map.put("name", name) map.put("email", email) map.put("phoneNumber", phoneNumber)
Submit List of Data
Once we are done submitting the simple text data the next hurdle is preparing the
map
for the List ofratings
data specified in each SurveyResponse. A Rating consists of a ratingNameId
and a ratingScoreId
fields required to be submitted. To prepare the request we have to loop through all the ratings like below:
for((index,rating) in ratings.withIndex()){ .... .... }
We use the Collection extension
withIndex()
that returns wraps each element of the collection into an IndexedValue
that contains the index of that element in the list and the element itself. We can then proceed to prepare the map
items as below:for((index,rating) in ratings.withIndex()){ map["ratings[${index}][ratingNameId]"] = createPartFromString("${rating.ratingNameId}") map["ratings[${index}][ratingScoreId]"] = createPartFromString("${rating.ratingScoreId}") }
To correctly submit the request we prepare the
key
as a multidimensional array like ratings[index][apiField]
With the index
of the collection as the first element followed by the API Field name
. The value
part is extracted from the individual rating fields as a RequestBody using ourcreatePartFromString
method.Sending an Image
For images we can assume that you will save the the Image URI after selecting/taking a picture during the Survey and stored in the
imageURI
field in the SurveyResponse object. For this we will have to create a separate variable called MultiBody.Part
. We can initialize it like below:var multipartImage: MultipartBody.Part? = null
We first extract the file to send from the imageURI field. like below
val fileUri = Uri.parse(surveyResponse.imageUri) String path = getPath(context, uri); if (path != null && isLocal(path)) { return new File(path); } } val file = LocalStorageProvider.getFile(activity, fileUri)
LocalStorageProvider is a popular utility class that helps in getting access to the Phone’s local storage using the Android 4.4 Storage access framework. You can add it to your projects in
utils
folder and access it here. Once we’ve extracted the file we can prepare the RequestBody like below.
val requestFile: RequestBody = RequestBody.create( "image/jpg".toMediaType(), file )
We use mediaType
image/jpg
assuming the image was saved in that format. We can then prepare the multipartImage
object we initialized like below:multipartImage = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
Once we are done we are now ready to build the Retrofit Call
Prepare the Retrofit Interface
We can finally prepare the call using our
APIService
service as below:interface APIService{ @Multipart @POST("survey") fun postSurveyResponse( @PartMap() partMap: MutableMap<String,RequestBody>, @Part file: MultipartBody.Part ):Response<Unit>
We annotate the call method with
@Multipart
to denote the Request Body. Doing this we can now submit our data in the parameters as @PartMap
and @Part
. @PartMap
will contain our map
of data prepared for the textual data and the ratings list. @Part
will denote the request body for the file that is to be sent. You can now finally send the Survey data by calling.fun sendSurveyResponse(){ // Prepare Survey Response apiService.postSurveyResponse(map,requestFile) }
Conclusion
On completed you will have the capability of sending dynamic pieces of form data specified as
multipart/formdata
.