File uploading in Android with Kotlin and Uploadcare
Trust JaminIn 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.
Create an Android application
Open Android Studio and create a new project called uploader with an Empty Activity.
To 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.
Create 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
UIViewModel
class to be used in the application - Creates a mutable
_uiState
that will be used for storing files uploaded - Initializes the
UploadcareClient
Android SDK using aPUBLIC_KEY
andSECRET_KEY
(ensure to replace these keys with your keys from the Uploadcare dashboard) - Creates an
onEvent
function that maps theSingleImageChanged
event to theuploadSingleImage
function - Creates an
uploadSingleImage
function that uses the UploadcareFileUploader
instance configured to store the files to Uploadcare - When the image is uploaded successfully, an
ImageResults
object 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_uiState
to 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
MainActivity
that sets the content to thePhotoScreen
composable function - Creates a
PhotoScreen
composable 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
Surface
composable function that contains aColumn
for displaying the UI elements - Creates a
Text
component to display a title for the file uploader - Creates a
Spacer
component to add spacing between elements - Creates a
Button
component for picking a photo, handling upload state, and launching the image picker - Creates a
ImageGrid
component to display uploaded images in a grid format - Creates a
Text
component to display a message if no images are uploaded - The
NetworkImage
composable function function loads and display images from URLs usingrememberAsyncImagePainter
fromcoil
library.
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:
Fetch 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.
Conclusion
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.