Working with Multipart Form Data using Retrofit

Photo by lilzidesigns on Unsplash

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 blog post to share my learnings.

Introduction

Let's 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”)
  • Ratings — apiField(”ratings”)

We can imagine this data is stored into these data class objects

//SurveyResponse.kt
data class SurveyResponse(
val name:String? = null,
val email:String? = null,
val phoneNumber:String? = null,
val image:String? =null,
var ratings:List<Rating>? =null
)
//Rating.kt
data class Rating(
val ratingNameId:Int?=null
val ratingScoreId:Int?=null
)

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 ;ratings[index][apiField] where 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 Image URI after selecting/taking a picture during the Survey and stored in the image 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 image 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 variable we declared above 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 @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 data prepared for the textual data and the rating 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 ResponseapiService.postSurveyResponse(map,requestFile)}

Conclusion

On completed you will have the capability of sending dynamic pieces of form data specified as multipart/formdata.

Further Reading

--

--

--

Software | Design | Products | Hiking

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

AWS CloudWatch ile Logları Kontrol Etme ( VPC Trafiği İzleme )

Docker for beginners. Part 3: Webapps with Docker

IT services: ‘Off Premises’& ‘On Premises’

Test-driven development — I’m feeling lucky

We do not want to write tests :)

CS371p Spring 2021 Final Entry

Design Clean SOLID Architecture for Analytics

My five highlights of working on OpenFaaS

Sorting Rows with Date in DataTable — Linq

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
David Amunga

David Amunga

Software | Design | Products | Hiking

More from Medium

How to make a simple Countdown Timer in Android

Language Change Programmatically in Kotlin, Grow Your Apps Audience Using Localization…

App Shortcuts on Android

Room Migrations — “Testing”