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:

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 keysUploadcare Public and Secret keys

Create an Android application

Open Android Studio and create a new project called uploader with an Empty Activity.

Create a new Android projectCreate a new Android project

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.

Sync project dependenciesSync project dependencies

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 a PUBLIC_KEY and SECRET_KEY (ensure to replace these keys with your keys from the Uploadcare dashboard)
  • Creates an onEvent function that maps the SingleImageChanged event to the uploadSingleImage function
  • Creates an uploadSingleImage function that uses the Uploadcare FileUploader 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 the PhotoScreen 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 a Column 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 using rememberAsyncImagePainter from coil library.

This will create a file uploader that can upload a single file:

Single file uploader in Android

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 AndroidFile uploader for multiple files in Android

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.

Fetching all images from UploadcareFetching all images from Uploadcare

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.

Build file handling in minutesStart for free

Ready to get started?

Join developers who use Uploadcare to build file handling quickly and reliably.

Sign up for free