Picture-Perfect Code: Uploading Images to the Server with Kotlin and Jetpack Compose using a RESTful API.
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 ofRequestBody
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 withtrue
to indicate that the upload process is starting.
imageService.uploadImage(image = image, requestBody = requestBody)
: It calls theuploadImage
function of theImageService
, passing the image andrequestBody
.If the upload is successful, it emits a success state with the
uploadImageResponse.
Resource.Loading(false)
: It emits a loading state withfalse
to indicate that the upload process is completed.If an
IOException
orHttpException
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)
: TheUploadViewModel
is constructed with an instance ofImageRepository
, which is used to perform image uploads.
_sharedFlowUploadImage
andsharedFlowUploadImage
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 aMultipartBody.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 -> ... }
: TheimageRepository.uploadImage
function is called, and the result is collected using the Flow API.In the
collect
block, the code processes different types ofResource
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 sameUri
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! 🤗🤗