Picture-Perfect Code: Uploading Images to the Server with Kotlin and Jetpack Compose using a RESTful API.

WanjiruCodes
6 min readNov 4, 2023

--

If you are here, the bold assumption is that you already have your hands on your image’s uri and you’re ready to send them on a magical journey to the cloud! If that’s not the case, fret not, simply refer to below link on how to land your uri, then you’ll be all caught up.

Throughout this article, we’ll embark on a step-by-step journey of making network requests using Retrofit and mastering image uploads. By the time you’ve completed this piece, you’ll be well-versed in the art of uploading images using Kotlin and Jetpack Compose.

Prerequisites:

  • Project setup is complete.
  • Retrofit has been integrated.
  • Dependency injection is configured.
  • The Flow API has been successfully integrated and implemented.

Ready to roll! But before we dive in, make sure your AndroidManifest.xml is all permissioned up. 🚀

<uses-permission android:name="android.permission.INTERNET" />

This includes the internet permission in our app because, after all, uploads can’t happen without a connection. 💁🏻‍♀️

Now, for the exciting part, we’re going to craft our very own Retrofit API service. It’s like sending postcards to an HTTP server, and these postcards will be designed to match the server’s expectations. Let’s compose some magic! 📨✨

interface ImageService {
@Multipart
@POST("your/api/endpoint")
suspend fun uploadImage(
@Part image: MultipartBody.Part,
@Part("image") requestBody: RequestBody
): UploadResponse
}

Above, we create a MultipartBody.Part for the file we want to upload. Additionally, we create an instance of RequestBody for additional data you want to send along with the file.

UploadResponse is a class or data model used to represent the response received from the server after making an API request to upload an image. It typically contains the data returned by the server in response to the image upload request. The structure and contents of the UploadResponse class would depend on the specific API and the server's response format.

Below is a sample:

data class UploadResponse(
val status: Boolean = false,
val message: String = ""
)

Next up, we create the ImageRepository interface. Inside it, we define a coroutine function uploadImage(image: MultipartBody.Part) for uploading an image.

This function takes a parameter named image, which is expected to be a MultipartBody.Part. This part should represent the image file to be uploaded.

This function returns a Flow, which is part of the Kotlin Flow API used for handling asynchronous and stream-like operations. The emitted data is wrapped in a Resource class, which is commonly used for API responses to handle different states like Success, Error, Loading, etc. In this case, the Resource is wrapping an UploadResponse.

interface ImageRepository {
....

suspend fun uploadImage(image: MultipartBody.Part): Flow<Resource<UploadResponse>>

....
}

The Resource class structure would depend on the specifics of your application and the server’s API.

Next up, we create the ImageRepositoryImpl class which is an implementation of the ImageRepository interface, responsible for handling the upload of an image using the ImageService and providing the result as a Flow of Resource<UploadResponse>.

@Singleton
class ImageRepositoryImpl @Inject constructor(
private val imageService: ImageService
) : ImageRepository {

...

override suspend fun uploadImage(image: MultipartBody.Part): Flow<Resource<UploadResponse>> {
val requestBody = "image".toRequestBody("multipart/form-data".toMediaTypeOrNull())

return flow {
try {
emit(Resource.Loading(true))
val uploadImageResponse = imageService.uploadImage(
image = image,
requestBody = requestBody
)

emit(Resource.Success(data = uploadImageResponse))
emit(Resource.Loading(false))
}
catch (e: IOException) {
emit(Resource.Error(message = "Couldn't upload image."))
}
catch (e: HttpException) {
emit(Resource.Error(message = "${ e.message}"))
}
}
}

....

}

Let’s break this down 🤗🤗 :

Resource.Loading(true): It emits a loading state with true to indicate that the upload process is starting.

imageService.uploadImage(image = image, requestBody = requestBody): It calls the uploadImage function of the ImageService, passing the image and requestBody.

If the upload is successful, it emits a success state with the uploadImageResponse.

Resource.Loading(false): It emits a loading state with false to indicate that the upload process is completed.

If an IOException or HttpException occurs during the upload process, it emits an error state with an appropriate error message.

In the next step, we create an Android ViewModel to manage the upload process of an image using the imageRepository. It utilizes Kotlin coroutines, the Flow API, and Hilt for dependency injection.

@HiltViewModel
class UploadViewModel @Inject constructor(
private val imageRepository: ImageRepository
) : ViewModel() {
.
.
.

private val _sharedFlowUploadImage = MutableSharedFlow<UploadImageState>()
val sharedFlowUploadImage = _sharedFlowUploadImage.asSharedFlow()

fun uploadImage(image: MultipartBody.Part) {
viewModelScope.launch {
_sharedFlowUploadImage.emit(UploadImageState(isLoading = true))

imageRepository.uploadImage(image).collect { result ->
when (result) {
is Resource.Success -> {
Log.i("upload", "success")

result.data?.let { uploadImageInfo ->
_sharedFlowUploadImage.emit(
UploadImageState(
isLoading = false,
uploadResponse = uploadImageInfo
)
)
}
}

is Resource.Error -> {
Log.i("upload", "fail")

_sharedFlowUploadImage.emit(
UploadImageState(
error = result.message,
isLoading = false,
)
)
}

is Resource.Loading -> {
_sharedFlowUploadImage.emit(
UploadImageState(
isLoading = result.isLoading
)
)
}
}
}
}
}

.
.
.

}

data class UploadImageState(
val uploadResponse: UploadResponse = UploadResponse(),
var isLoading: Boolean = false,
val error: String? = null
)

Let’s break down the key parts of the code:

constructor(private val imageRepository: ImageRepository): The UploadViewModel is constructed with an instance of ImageRepository, which is used to perform image uploads.

_sharedFlowUploadImage and sharedFlowUploadImage are shared flows used to emit and observe states related to the image upload process.

uploadImage(image: MultipartBody.Part): This function initiates the image upload process. It takes a MultipartBody.Part as a parameter, representing the image to be uploaded.

Inside the viewModelScope.launch block, the upload process is initiated using coroutines.

imageRepository.uploadImage(image).collect { result -> ... }: The imageRepository.uploadImage function is called, and the result is collected using the Flow API.

In the collect block, the code processes different types of Resource states emitted by the repository:

  • Resource.Success: If the upload is successful, it emits a success state and updates the _sharedFlowUploadImage with the upload response.
  • Resource.Error: If there is an error, it emits an error state and includes the error message.
  • Resource.Loading: It emits loading states to indicate the progress of the upload operation.

We’ve triumphed !! We can now ring up the uploadImage function from our user interface😁.

Plot twist! Our uploadImage function accepts a MultipartBody.Part as a parameter. On the other hand what we have is a URI. How do we transform the URI into a MultipartBody? This is how:

fun createMultipartBody(uri: Uri, multipartName: String): MultipartBody.Part {
val documentImage = Util.decodeFile(uri.path!!)
val file = File(uri.path!!)
val os: OutputStream = BufferedOutputStream(FileOutputStream(file))
documentImage.compress(Bitmap.CompressFormat.JPEG, 100, os)
os.close()
val requestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
return MultipartBody.Part.createFormData(name = multipartName, file.name, requestBody)
}

Above function takes in the uri and creates a MultipartBody.Part for use in making our multipart/form-data HTTP request. Breakdown? Yeah sure🤗…

It decodes an image file from the provided Uri using a utility function.

Creates a File object from the same Uri path.

Initializes an output stream to write data to the file.

Compresses the decoded image to JPEG format and writes it to the file.

Closes the output stream to ensure the file is properly saved.

Creates a RequestBody from the file with a "multipart/form-data" media type.

Constructs a MultipartBody.Part with a field name, the file name, and the request body for use in an HTTP request.

We did it! 👏👏 . Now that we’ve got all our pieces in place, here’s a sneak peek at how our grand finale will unfold:

uploadViewModel.uploadImage(
Util.createMultipartBody(
uri = Uri.parse(imageUri),
multipartName = "image"
)
)

This call should be made when you’re ready to upload the image. This can be triggered whether you’re selecting a picture from the gallery or capturing one with the camera. The approach remains consistent; wherever you have the URI ready, you can invoke the function from that point.

As an illustration, when dealing with a gallery upload, here’s a snippet that demonstrates how it should appear:

val galleryLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
val data: Intent? = it.data
if (data != null) {
imageUri = Uri.parse(data.data.toString())

if (imageUri.toString().isNotEmpty()) {
Log.d("myImageUri", "$imageUri ")

uploadViewModel.uploadImage(
Util.createMultipartBodyFromUri(
context = context,
uri = Uri.parse(imageUri),
multipartName = "image"
)
)

}
}
}
}

Likewise, when working with a camera capture, here’s a snippet that showcases how it should be presented:

val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture(),
onResult = { success ->
if (success) imageUri = uri
if (imageUri.toString().isNotEmpty()) {
Log.d("myImageUri", "$imageUri ")

uploadViewModel.uploadImage(
Util.createMultipartBodyFromUri(
context = context,
uri = Uri.parse(imageUri),
multipartName = "image"
)
)

}
}
)

Let the uploading fiesta begin! 🤝🏼🤝🏼. We’ve equipped ourselves with the essential knowledge and code snippets needed to facilitate seamless image uploads within our application. Whether you’re selecting images from the gallery or capturing them with the camera, the principles remain the same. By following these steps, you can ensure that your users can effortlessly share their visual content.

May your uploads be swift, your files untroubled, and your users ecstatic! 🤗🤗

--

--