Level: Medium
Ktor is an excellent client for api requests in Android, it has many features like web-socket support, asynchronous HTTP requests, and you can extend its functionality with plugins such as authentication and JSON serialization, however uploading an image with a Ktor cleint to a Ktor server wasn’t similar to Retrofit, as trying to upload the image as a MultipartBody i will get an error “Content-Type header is required for multipart processing” if i add the header i get “Header Content-Type is controlled by the engine and cannot be set explicitly” so here is a solution that has worked for me.
Getting the image:
This example will show how to pick an image in a jetpack compose project in android, first lets create a launcher, it will be used to lunch the image picker activity which allows the user to pick an image from their phone, once the user picks an image we will have a Uri that we can use to read the image in the next step
val context = LocalContext.current
val imageData = remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(
contract = (ActivityResultContracts.GetContent()),
onResult = { uri ->
imageData.value = uri
}
)
to trigger the image picker activity add the following line in your button click (or whenever you need to)
launcher.launch("image/jpeg")
Sending the image with Ktor Client:
now that we have the image Uri we need to open an input stream and pass the Uri to start reading the byte array of the image file
val inputStream = context.contentResolver.openInputStream(it)
val imageByteArray = inputStream?.readBytes()
uploadImage(imageByteArray)
now lets implement our uploadImage function which will use the Ktor client to send the image to our server, please note that the “text” parameter here is optional and is just an example of how to send other parameters in the same request
suspend fun uploadImage(text: String, byteArray: ByteArray?): Boolean {
return try {
if (byteArray != null) {
val response: HttpResponse = client.submitFormWithBinaryData(
url = ImageService.Endpoints.AddImage.url,
formData = formData {
//the line below is just an example of how to send other parameters in the same request
append("text", text)
append("image", byteArray, Headers.build {
append(HttpHeaders.ContentType, "image/jpeg")
append(HttpHeaders.ContentDisposition, "filename=image.png")
})
}
) {
onUpload { bytesSentTotal, contentLength ->
println("Sent $bytesSentTotal bytes from $contentLength")
}
}
}
true
} catch (ex: Exception) {
//im using timber for logs, you can always replace this with Log.d
Timber.d("error ${ex.message}")
false
}
}
the above function will return true if the upload was successful or false if there was an error, we can also track the upload progress using the onUpload block
Receiving the image in Ktor server:
create a post request to process your request and save the image
post("/add-image") {
val multipart = call.receiveMultipart()
var fileName: String? = null
var text: String?= null
try{
multipart.forEachPart { partData ->
when(partData){
is PartData.FormItem -> {
//to read additional parameters that we sent with the image
if (partData.name == "text"){
text = partData.value
}
}
is PartData.FileItem ->{
fileName = partData.save(Constants.USER_IMAGES_PATH)
}
is PartData.BinaryItem -> Unit
}
}
val imageUrl = "${Constants.BASE_URL}/uploaded_images/$fileName"
call.respond(HttpStatusCode.OK,imageUrl)
} catch (ex: Exception) {
File("${Constants.USER_IMAGES_PATH}/$fileName").delete()
call.respond(HttpStatusCode.InternalServerError,"Error")
}
}
create the save extension function that is used in the code above, this function creates a random name for our file, attaches the correct file extension, saves the file to our path, writes the image byte data to the newly created file then return its file name
fun PartData.FileItem.save(path: String): String {
val fileBytes = streamProvider().readBytes()
val fileExtension = originalFileName?.takeLastWhile { it != '.' }
val fileName = UUID.randomUUID().toString() + "." + fileExtension
val folder = File(path)
folder.mkdir()
println("Path = $path $fileName")
File("$path$fileName").writeBytes(fileBytes)
return fileName
}
That’s it thanks for reading ! please feel free to post your questions or suggestions to improve the article in the comments.
thanks for the article if you have more video or sources to explain that in more detail. and thanks again
Thanks Taha for the articles. Can you give any hints about if I wanted to upload an image as part of a data class, like with the fruits, as I am familiar with as a beginner? e.g. I have a name, email and want this image appended to that?
Unfortunately it is not possible to directly serialize a File or any type of binary data, one workaround you can do is to encode the binary data as a string using a specific encoding format (e.g., Base64 encoding) in the client end and store it as a
String
property in the data class.However i don’t prefer to do this as we would end up with a large string (larger than the actual image)
im working on an article to explain the image upload as part of the tutorial series and hopefully will finish it by tomorrow.
thanks for the reply, looking forward to the article.
Taha – I have a lot of what are probably stupid questions about how this might look on the side of the compose project (for this example and for things a little more complicated) – possibly such that it would be too much to expect you to answer here, unless you have an article planned. I thought about contacting you to see what’s possible but your contact page doesn’t seem to be working. Let me know what’s best when you’re free. Thanks.
Hello Sam – The contact page is primarily for inquiries related to paid work. However, if you have questions that you’d like me to answer, please feel free to ask them in the comments. I’ll do my best to provide helpful responses and assist you in any way I can.
Serializer for class ‘MultiPartFormDataContent’ is not found. Mark the class as @Serializable or provide the serializer explicitly. Rewrote your code and getting this error. Maybe you need to register something in the ktor client?
Did you annotate the data class with @Serializable ?