File uploading in Android with Kotlin and Uploadcare
In this tutorial, you will learn how to upload files in your Android applications using Kotlin and the Uploadcare Android SDK. You will create a file uploader that can upload multiple files and fetch files from the Uploadcare server using the Android SDK.
The complete code for this tutorial can be found here.
Prerequisites
Before you begin, you need:
- An Uploadcare account
- Android Studio installed on your computer
- Basic knowledge of Kotlin and Android development
Retrieve a Public API key
Head to your Uploadcare Project Dashboard -> API keys and copy the Public and Secret API keys.
Uploadcare Public and Secret keysCreate an Android application
Open Android Studio and create a new project called uploader with an Empty Activity.
Create a new Android projectTo install the Uploadcare Android SDK, update your dependencies in the build.gradle.kts (Module:app) file:
implementation("com.uploadcare.android.library:uploadcare-android:4.3.1")Next, add the following dependencies for loading images and for managing state and lifecycle events in a Jetpack Compose UI:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.3")
implementation("io.coil-kt:coil-compose:2.6.0")Sync the project dependencies and IDE by clicking on the Sync now button.
Sync project dependenciesCreate a file uploader using the Android SDK
In the app/kotlin+java/com.example.uploader directory, create a new package directory called features, and inside of it, create a file named UIEvent.kt with the content:
package com.example.uploader.features
import android.content.Context
import android.net.Uri
sealed class UIEvent {
data class SingleImageChanged(val uri: Uri, val context: Context) : UIEvent()
}This file will contain all the different events that happen in the application.
Currently, there is an event named SingleImageChanged that will trigger a function when an image is uploaded.
Next, create a UIState.kt file for storing the UIState of the application with the content:
package com.example.uploader.features
data class UIState(
val isUploading: Boolean = false,
val images: MutableList<ImageResults> = ArrayList()
)
data class ImageResults(val uid: String, val imageUrl: String)The code above creates a state with an isUploading boolean variable to track upload requests,
and an images ArrayList containing a uid and imageUrl string.
Let’s create a UIViewModel class with an uploadSingleImage function that uploads an image to Uploadcare;
in the features directory, create a UIViewModel.kt file with the content:
package com.example.uploader.features
class UIViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UIState())
val uiState: StateFlow<UIState> = _uiState.asStateFlow()
private val client = UploadcareClient("PUBLIC_KEY", "SECRET_KEY")
fun onEvent(event: UIEvent) {
when (event) {
is UIEvent.SingleImageChanged -> {
uploadSingleImage(event.context, event.uri)
}
}
}
private fun uploadSingleImage(context: Context, uri: Uri) {
_uiState.update { it.copy(isUploading = true) }
val images = _uiState.value.images
val uploader = FileUploader(client, uri, context).store(true)
uploader.uploadAsync(
object : UploadFileCallback {
override fun onFailure(e: UploadcareApiException) {
Log.i("ERROR", e.message.toString())
}
override fun onProgressUpdate(
bytesWritten: Long,
contentLength: Long,
progress: Double
) {}
override fun onSuccess(result: UploadcareFile) {
val imageResult =
ImageResults(
uid = result.uuid,
imageUrl = result.originalFileUrl.toString()
)
images.add(imageResult)
_uiState.update { it.copy(isUploading = false, images = images) }
}
}
)
}
}The code above:
- Creates a
UIViewModelclass to be used in the application - Creates a mutable
_uiStatethat will be used for storing files uploaded - Initializes the
UploadcareClientAndroid SDK using aPUBLIC_KEYandSECRET_KEY(ensure to replace these keys with your keys from the Uploadcare dashboard) - Creates an
onEventfunction that maps theSingleImageChangedevent to theuploadSingleImagefunction - Creates an
uploadSingleImagefunction that uses the UploadcareFileUploaderinstance configured to store the files to Uploadcare - When the image is uploaded successfully, an
ImageResultsobject with the uploaded file’s UUID and URL will be created - Retrieves the current list of images from
_uiState, adds the new image result, and updates_uiStateto include the new image
To use the SingleImageChanged event in the UI in Jetpack Compose, update the MainActivity.kt file to be:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { UploaderTheme { PhotoScreen() } }
}
}
@Composable
fun PhotoScreen(viewModel: UIViewModel = viewModel()) {
// Collects and observes the UI state using the ViewModel
val uiState by viewModel.uiState.collectAsState()
val context = LocalContext.current // Retrieves the current context
// Remembers the launcher for picking a single image from media and handles its result
val singleImagePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
uri?.let { // Handles the selected image URI
UIEvent.SingleImageChanged(it, context) // Fires event for single image upload
}?.let { viewModel.onEvent(it) } // Passes the event to the ViewModel
}
)
Surface(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// Displays a title for the file uploader
Text(
text = "UC file uploader in Android",
style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold)
)
Spacer(modifier = Modifier.size(20.dp)) // Adds spacing between elements
// Button for picking a photo, handling upload state, and launching image picker
Button(
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(8.dp),
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF0073E6),
contentColor = Color.White,
),
onClick = {
if (!uiState.isUploading) {
singleImagePickerLauncher.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
}
}
) {
// Displays appropriate content based on upload state
if (!uiState.isUploading) {
Row {
Icon(imageVector = Icons.Filled.Add, contentDescription = null)
Spacer(modifier = Modifier.width(16.dp))
Text(text = "upload a file", style = TextStyle(fontSize = 18.sp))
}
} else {
CircularProgressIndicator(
color = Color.White,
)
}
}
// Displays the uploaded images in a grid if available, or a message if none uploaded
if (uiState.images.isNotEmpty()) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
) {
items(uiState.images.size) { index ->
NetworkImage(imageUrl = uiState.images[index].imageUrl)
}
}
} else {
Spacer(modifier = Modifier.height(40.dp))
if (!uiState.isUploading) {
Text(
text = "No uploaded Images yet",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
}
}
}
// Composable function for displaying a network image fetched by URL
@Composable
fun NetworkImage(imageUrl: String) {
val imageModifier = Modifier.padding(8.dp).size(150.dp)
Image(
painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.crossfade(true)
.build(),
),
contentDescription = "Network Image",
contentScale = ContentScale.Crop,
modifier = imageModifier
)
}
// Preview function for displaying a preview of the PhotoScreen composable
@Preview
@Composable
fun PhotoPickerScreenPreview() {
PhotoScreen()
}Let’s break down the code above for better understanding:
- Creates a
MainActivitythat sets the content to thePhotoScreencomposable function - Creates a
PhotoScreencomposable function that: - Collects and observes the UI state using the
viewModel - Retrieves the current context using
LocalContext.current - Remembers the launcher for picking a single image from media and handles its result
- Creates a
Surfacecomposable function that contains aColumnfor displaying the UI elements - Creates a
Textcomponent to display a title for the file uploader - Creates a
Spacercomponent to add spacing between elements - Creates a
Buttoncomponent for picking a photo, handling upload state, and launching the image picker - Creates a
ImageGridcomponent to display uploaded images in a grid format - Creates a
Textcomponent to display a message if no images are uploaded - The
NetworkImagecomposable function function loads and display images from URLs usingrememberAsyncImagePainterfromcoillibrary.
This will create a file uploader that can upload a single file:
Implement a file uploader for multiple files
To create a file uploader for multiple files; first, add update the UIEvent.kt to include a MultipleImageChanged event:
sealed class UIEvent {
data class SingleImageChanged(val uri: Uri, val context: Context) : UIEvent()
data class MultipleImageChanged(val uris: List<Uri>, val context: Context) : UIEvent()
}Then add an uploadMultipleImages function to UIViewModel.kt:
private fun uploadMultipleImages(context: Context, uris: List<Uri>) {
_uiState.value = UIState(isUploading = true)
val images = _uiState.value.images
val uploader = MultipleFilesUploader(client, uris, context).store(true)
uploader.uploadAsync(
object : UploadFilesCallback {
override fun onFailure(e: UploadcareApiException) {
Log.i("ERROR", e.message.toString())
}
override fun onProgressUpdate(
bytesWritten: Long,
contentLength: Long,
progress: Double
) {
// Upload progress info.
}
override fun onSuccess(result: List<UploadcareFile>) {
result.forEach {
images.add(
ImageResults(uid = it.uuid, imageUrl = it.originalFileUrl.toString())
)
}
_uiState.update { it.copy(isUploading = false, images = images) }
}
}
)
}The code above uses the MultipleFilesUploader to upload multiple files asynchronously to Uploadcare servers.
Next, update the onEvent function in UIViewModel.kt
to also listen for a MultipleImageChanged event that will use the uploadMultipleImages function:
fun onEvent(event: UIEvent) {
when (event) {
is UIEvent.SingleImageChanged -> {
uploadSingleImage(event.context, event.uri)
}
is UIEvent.MultipleImageChanged -> {
uploadMultipleImages(event.context, event.uris)
}
}
}Next, update the MainActivity.kt file to show a multiple files uploader button;
first, add a multipleImagePickerLauncher to the PhotoScreen composable function:
val multipleImagePickerLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickMultipleVisualMedia(),
onResult = { uris -> viewModel.onEvent(UIEvent.MultipleImageChanged(context, uris)) }
)Next, add a button to the Surface function in PhotoScreen for uploading multiple files:
Spacer(modifier = Modifier.height(20.dp))
Button(
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(8.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = Color(0xFF0073E6),
contentColor = Color.White,
),
onClick = {
if (!uiState.isUploading) {
multipleImagePickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
}
) {
if (!uiState.isUploading) {
Row {
Icon(imageVector = Icons.Filled.Add, contentDescription = null)
Spacer(modifier = Modifier.width(16.dp))
Text(text = "Pick multiple photos", style = TextStyle(fontSize = 18.sp))
}
} else {
CircularProgressIndicator(
color = Color.White,
)
}
}This should provide you with a UI that looks like this for uploading multiple files:
File uploader for multiple files in AndroidFetch all files uploaded to Uploadcare
Using the SDK, you can retrieve all files uploaded to a project.
To do this, let’s add a getImages function that will fetch all images using the SDK.
First, update UIEvent.kt to include a GetImages event:
sealed class UIEvent {
// previous events
data object GetImages : UIEvent()
}Next, update the UIViewModel.kt to have a getImages function:
private fun getImages() {
_uiState.value = UIState(isUploading = true)
val images = ArrayList<ImageResults>()
client
.getFiles()
.asListAsync(
object : UploadcareAllFilesCallback {
override fun onFailure(e: UploadcareApiException) {
Log.i("ERROR", e.message.toString())
}
override fun onSuccess(result: List<UploadcareFile>) {
result.forEach {
images.add(
ImageResults(uid = it.uuid, imageUrl = it.originalFileUrl.toString())
)
}
_uiState.value = UIState(images = images, isUploading = false)
}
}
)
}Then update the onEvent function:
fun onEvent(event: UIEvent) {
when (event) {
// previous events
is UIEvent.GetImages -> {
getImages()
}
}
}In the MainActivity.kt, update the PhotoScreen composable function to show a button to get all images:
Spacer(modifier = Modifier.height(20.dp))
Button(
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(8.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = Color(0xFF0073E6),
contentColor = Color.White,
),
onClick = {
if (!uiState.isUploading) {
viewModel.onEvent(UIEvent.GetImages)
}
}
) {
if (!uiState.isUploading) {
Row {
Icon(imageVector = Icons.Filled.Add, contentDescription = null)
Spacer(modifier = Modifier.width(16.dp))
Text(text = "get images", style = TextStyle(fontSize = 18.sp))
}
} else {
CircularProgressIndicator(
color = Color.White,
)
}
}Clicking the get all files button will fetch all images uploaded.
Ideally, the application should fetch all images when it launches.
To do this, add a LaunchEffect to call the GetImages event in the PhotoScreen composable function when the application launches.
LaunchedEffect(Unit){
viewModel.onEvent(UIEvent.GetImages)
}This will fetch all images when the application launches.
Fetching all images from UploadcareConclusion
In this tutorial, you’ve successfully created a single and multiple file uploader to retrieve all files uploaded using Kotlin and the Uploadcare Android SDK in a mobile application.
The Uploadcare Android SDK also provides different file operations for managing your files with Uploadcare, such as multipart uploading for large files, paginated resource fetching, and lots more.
View the SDK documentation for more details on file operations available in the Android SDK.