State of Compose 2023 results are in! Click here to learn more
Published on

Ask other apps for photos, files and more using ActivityResultContracts

Authors

Featured in Android Weekly #555Featured in Jetpack Compose Newsletter #147

This tutorial will teach you how to open other apps and ask for data using the ActivityResultContract in Jetpack Compose. This API is often used for scenarios where you want to ask other apps for common actions, such as taking a photo, selecting a contact, or selecting files.

The great thing about this API is that you do not need to request any permission in your app. All implementation is handled by the calling application.

What is the ActivityResultContract API

The startActivityForResult() function and onActivityResult() callback were deprecated in recent versions of Android. Its successor is the ActivityResultContracts API. As opposed to startActivityForResult API, ActivityResultContracts are type safe.

The way ActivityResultContracts works is by registering a launcher using the rememberLauncherForActivityResult() composable function. You pass the action you want to perform and the callback that will be called as soon as you receive the result. Calling this function will return an ActivityLauncher object, which you can use to start the activity with any required arguments.

Here is a quick sample of how you can use this API to select a Contact stored in your device:

@Composable
fun ContactPicker(
    onContactSelected: (Uri) -> Unit,
    onCancelled: () -> Unit,
) {
    val launcher = rememberLauncherForActivityResult(PickContact()) { uri ->
        if (uri != null) {
            onContactSelected(uri)
        } else {
            onCancelled()
        }
    }
    IconButton(onClick = { launcher.launch() }) {
        Icon(Icons.Rounded.AccountCircle, contentDescription = "Pick Contact")
    }
}

This will bring up the contacts app installed to the device and let the user select a contact. Once the user selects a contact or cancels the operation (that's when finish() is called on the contact app's activity) the callback passed in your rememberLauncherForActivityResult() is called. Within the callback you will receive a Uri if the user has selected a contact, or null if they decided to exit without selecting one.

How to use the ActivityResultContract API

ActivityResultContracts are part of the androidx.activity dependency but as we are using Jetpack Compose, we need to use the android-compose version of it. The compose version of the dependency is what brings in the rememberLauncherForActivityResult() function, along with all available contracts.

In your app/build.gradle file, include the AndroidX Activity-Compose dependency:

dependencies {
    implementation 'androidx.activity:activity-compose:1.6.1'
}

The cool thing about ActivityResultContracts is that it comes with a plethora of predefined actions you can use straight away such as picking files, selecting photos, selecting contacts from the address book, taking photos and much more.

You can find the full list of available actions by clicking here.

Here is how to use each one of them:

How to create new files using the CreateDocument action

Use this action to let the user choose a location to create a new file. You can then use the uri provided to populate the file without having to declare or request any permissions.

Here is how to save the text "Hello Android" into a file called hello.txt:

val contentResolver = LocalContext.current.contentResolver

val launcher = rememberLauncherForActivityResult(CreateDocument("text/plain")) { selectedUri ->
    if (selectedUri != null) {
        contentResolver.openOutputStream(selectedUri)?.use {
            val bytes = "Hello Android".toByteArray()
            it.write(bytes)
        }
    } else {
        println("No file was selected")
    }
}
LaunchedEffect(Unit) {
    launcher.launch("hello.txt")
}

How to pick a file to read its contents using the GetContent action

Use the GetContent contract when you want to let your user select a single file. Specify the mimetype you need via the launcher.

Once the user selects a file, you will have access to read it temporarily:

val launcher = rememberLauncherForActivityResult(GetContent()) { selectedUri ->
    if (selectedUri != null) {
        println("File selected = $selectedUri")
    } else {
        println("No file was selected")
    }
}
LaunchedEffect(Unit) {
    launcher.launch("image/jpeg")
}

Need to select multiple files? GetMultipleContents is also available. You can use it the same way as GetContent but it returns a list of selected Uris instead.

tl;dr Mimetypes

Mimetypes is a label that describes the combination of a file type and extension. It is not an Android or Jetpack Compose specific concept but you will see it being referenced all over Android, especially when you need to handle files such as audio and images.

A mimetype is described as a type/subtype. You may use a wildcard (*) as the subtype to specify you want any kind of file matching the type.

Common mimetypes include:

  • image/jpeg: JPEG image files
  • audio/mpeg: MP3 audio files
  • text/plain: plain text files
  • video/mp4: MP4 video files
  • image/*: any image file
  • audio/*: any audio file
  • video/*: any video file

Learn more about mimetypes on developer.mozilla.org.

How to pick a file for read/write using the OpenDocument action

Use this action to let the user pick a file from their device. You can then write into this uri or read its contents.

The following sample lets the user select an image from their device. The image will be displayed on the screen as soon as the user selects it. I am using Coil-compose to display the image on the screen, as it handles asynchronous image loading, mapping the uri to a Bitmap and also recycling it when not needed:

var selectedUri by remember { mutableStateOf<Uri?>(null) }
val context = LocalContext.current

val launcher = rememberLauncherForActivityResult(OpenDocument()) { uri ->
    selectedUri = uri
}
AsyncImage(
    modifier = Modifier.fillMaxSize(),
    contentScale = ContentScale.Crop,
    model = ImageRequest.Builder(context)
        .data(selectedUri)
        .build(),
    contentDescription = null
)
LaunchedEffect(Unit) {
    launcher.launch(arrayOf("image/*"))
}

Need to open multiple documents? OpenMultipleDocuments() is also available.

How to select a directory using the OpenDocumenTree action

Use this action to let the user pick a directory from their device. The returned Uri is the uri of the selected directory. According to the documentation "Apps can fully manage documents within the returned directory.".

Some directories might not be accessible starting Android 11.

val launcher = rememberLauncherForActivityResult(OpenDocumentTree()) { directoryUri ->
    if (directoryUri != null) {
        println("Selected $directoryUri")
    } else {
        println("No directory selected")
    }
}
LaunchedEffect(Unit) {
    launcher.launch(null)
}

How to pick a contact using the PickContact action

Use this action to let the user select a contact. The returned uri points to the selected contact.

The contact uri returned is part of the Contacts Provider API.

val launcher = rememberLauncherForActivityResult(PickContact()) { contactUri ->
    if (contactUri != null) {
        // TODO use the Contacts Provider API to query information about the contact
        println("Contact selected = $contactUri")
    } else {
        println("No contact selected")
    }
}
LaunchedEffect(Unit) {
    launcher.launch()
}

How to select images and videos using the PickVisualMedia action (Android 13 Photo Picker)

Android 13 introduced the Photo Picker feature. You can use it to let users choose specific photos and videos which can be read by your app instead of giving you access to all their files of the device.

If the Photo Picker feature is not available on the Android version you are running, the OpenDocument() action will be used instead. You can specify what kind of media you need (images, videos or both) via the launcher:

val launcher = rememberLauncherForActivityResult(PickVisualMedia()) { imageUri ->
    if (imageUri != null) {
        println("Images Selected = $imageUri")
    } else {
        println("No image selected")
    }
}

LaunchedEffect(Unit) {
    launcher.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
}

Need to select multiple photos and videos? PickMultipleVisualMedia is also available. Pass the number of media you need via the constructor.

How to select permissions using the RequestPermission action

Use the RequestPermission contract to request the runtime permission you need. As soon as the user grants or denies the permission, the provided callback will be called:

Click here to learn how to properly handle permissions in your Android app

Here is how to request the CAMERA permission:

val launcher = rememberLauncherForActivityResult(RequestPermission()) { granted ->
    if (granted) {
        println("Camera permission granted")
    } else {
        println("Permission denied")
    }
}

LaunchedEffect(Unit) {
    launcher.launch(android.Manifest.permission.CAMERA)
}

Need to select multiple permissions at once? RequestMultiplePermissions is also available.

How take photos and videos using TakePhoto and CaptureVideo

These actions can be used to ask the Camera app to take a photo or video. You need to provide the Uri of a file to store the data of the captured media:

Keep in mind that both TakePhoto and CaptureVideo require the CAMERA permission to work.

val launcher = rememberLauncherForActivityResult(TakePicture()) { captured ->
    if (captured) {
        println("Photo captured.")
    } else {
        println("Photo was not captured.")
    }
}
LaunchedEffect(Unit) {
    launcher.launch(photoUri)
}

The following sample demonstrates how to use request the CAMERA permission, then ask the user for a file to save a new video, then opens the camera app to record a video:

var videoCaptured by remember { mutableStateOf<Uri?>(null) }

val captureVideoLauncher = rememberLauncherForActivityResult(CaptureVideo()) { captured ->
    if (captured) {
        println("Video captured . Uri = $videoCaptured")
    } else {
        videoCaptured = null
    }
}

val selectFileLauncher =
    rememberLauncherForActivityResult(CreateDocument("video/mp4")) { videoUri ->
        if (videoUri != null) {
            videoCaptured = videoUri
            captureVideoLauncher.launch(videoUri)
        }
    }

val permissionLauncher = rememberLauncherForActivityResult(RequestPermission()) { granted ->
    if (granted) {
        selectFileLauncher.launch("video_${System.currentTimeMillis()}.mp4")
    }
}

LaunchedEffect(Unit) {
    permissionLauncher.launch(Manifest.permission.CAMERA)
}

How to take a thumbnail photo using the TakePicturePreview

The TakePicturePreview action can be used to take a small photo.

Not entirely sure what you can do with this one other than sampling. You could potentially use this to take a small photo, blur it and scaling it up to create a nice looking visual (as a background).

Keep in mind that TakePicturePreview requires the CAMERA permission to work.

Make sure to recycle the loaded Bitmap like the sample below:

var capturedBitmap by remember { mutableStateOf<Bitmap?>(null) }

val launcher = rememberLauncherForActivityResult(TakePicturePreview()) { bitmap ->
    capturedBitmap?.recycle()
    capturedBitmap = bitmap
}
DisposableEffect(Unit) {
    onDispose {
        capturedBitmap?.recycle()
    }
}

if (capturedBitmap != null) {
    Image(
        capturedBitmap!!.asImageBitmap(),
        contentDescription = null,
        contentScale = ContentScale.Crop
    )
}

LaunchedEffect(Unit) {
    launcher.launch()
}

How to start activity for result in Jetpack Compose

The ActivityResultContracts API relies on a powerful mechanism of Android called Intents.

It is the API that allows apps to launch other apps and return data back to them. Before the ActivityResultContracts API, you would need to use Activity.startActivityForResult() to start the other app, and then override the onActivityResult() function to receive the result.

You can use the StartActivityForResult() action to start any kind of Intent you need, that might not be part of the existing predefined ActivityResultContracts:

val launcher = rememberLauncherForActivityResult(StartActivityForResult()) { activityResult ->
    when (activityResult.resultCode) {
        Activity.RESULT_OK -> {
            val data = activityResult.data
            println("Result was OK. Data received = $data")
        }
        Activity.RESULT_CANCELED -> println("Result was CANCELED")
    }
}

LaunchedEffect(Unit) {
    launcher.launch(Intent("com.example.yourpackage.CUSTOM_ACTION"))
}

Make sure to include the namespace of your package if you are creating a new public Intent action. Actions are public and are exposed in the Android system.

Working with IntentSender? StartIntentSenderForResult is also available.

How to create your own custom ActivityResultContract

You can create your custom ActivityResultContracts to reuse the same code in multiple places in your own app.

A custom ActivityResultContract is a good candidate if you are building a library or SDK, and you want to expose a simple entry point API to the consumers of your library.

Custom ActivityResultContracts need to implement the ActivityResultContract<I,O> abstract class. I stands for the input, and O stands for the returned data:

The following sample showcases how to

class CustomActivityContract : ActivityResultContract<String, Float?>() {
    override fun createIntent(context: Context, input: String): Intent {
        return Intent("com.example.yourpackage.CUSTOM_ACTION").apply {
            putExtra("com.example.yourpackage.EXTRA_PARAMETER", input)
        }
    }

    override fun parseResult(resultCode: Int, intent: Intent?): Float? {
        return when (resultCode) {
            Activity.RESULT_OK -> {
                val requiredIntent = requireNotNull(intent)
                requiredIntent.getFloatExtra("com.example.yourpackage.EXTRA_VALUE", -1f)
            }
            else -> null
        }
    }
}

You can then pass your custom action into the rememberLauncherForActivityResult() like any other action.

When you should use ActivityResultContract

As a developer you want to focus on features and capabilities that makes your app unique. Your application should focus only on the functionality that is vital and/or unique to your app.

Consider asking other apps using ActivitResultContracts to do work for you that you do not want to implement yourself.

Building and maintaining functionality in your app that is not related to your business might end up being more expensive that you might think. Because of this using other apps to handle the load for you might be the simplest way to focus on the parts your apps shine.

If you are developing a library or an SDK, consider providing a custom ActivityResultContract to reduce the friction of entry to your SDK.


In this tutorial you learnt all about ActivityResultContract. More specifically you learn what ActivityResultContracts are, how they let you launch other applications to request for data using the predefined actions. All available ActivityResultContract were covered along with considerations when to use them.


Related resources


Here is how I can help you