Ask other apps for photos, files and more using ActivityResultContracts
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 Uri
s 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 ActivityResultContract
s API relies on a powerful mechanism of Android called Intent
s.
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 ActivitResultContract
s 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
- A comprehensive guide to Android runtime permissions using Jetpack Compose on composables.co
- A list of all available ActivityResultContracts on developer.android.com
Here is how I can help you
- View to Composable: Includes more than 100+ code snippets of how to do things you already know from Android Views to Jetpack Compose. Join 750+ developers