Using Activity Result Contracts in Jetpack Compose
In this short story we will write a simple app that will showcase how to use the Activity Result Contracts in Jetpack Compose. Our app will have 2 buttons, one to pick an image from the gallery, and a second one to take a picture. In both cases, once the image is selected or the picture is taken, it will be displayed on the screen. Let’s get started!
Launching an Activity for result
Until not too long ago the way in Android to start an activity for result was to launch an Intent and then override the onActivityResult
method in your Activity or Fragment and parse the returned payload there. Nowadays the prefered method is to use the Activity Contracts, so that we no longer need to override onActivityResult
.
In the case of Jetpack Compose, we have a dedicated function to build a contract, rememberLauncherForActivityResult. This method, as the name indicates and following the naming convention in Jetpack Compose, remembers a launcher across recompositions, and takes 2 arguments:
- contract — this is the action we want to take, and specifies the inputs and outputs for that action (for instance, to open a file the input would be the mimetype, and the output would be the URI of the selected file).
- onResult — a lambda that receives the result once the action specified by our contract is complete. The result is of the output type in our contract.
Android provides a set of contract templates for common actions, like selecting a file, taking a picture, requesting permissions and so on; you can read the details on the official documentation. For this story, we will be using the ActivityResultContracts.GetContent()
for selecting an image from the gallery, and ActivityResultContracts.TakePicture()
to take a photo.
Building our app
Scaffolding
Let’s start with the scaffolding for our app, we will need 2 buttons to handle the 2 actions we want, Select Image and Take Photo. We will place these buttons at the bottom of the screen, and we will later add the image at the top.
Our starting code for the buttons is show below:
This gives us this simple layout:
Adding the image picker contract
Next we are going to flesh out the Select Image button so that it launches the file picker. For this we will need a contract, in this case GetContent
. This contract’s input is a mimetype, specifying the type of files we are interested in, and the output is a Uri
that allows us to read the selected file. This is how we would define the contract:
- We define our contract launcher to select some content — currently we are not handling the returned URI.
- When we click on the Select Image button we launch our contract, specifying the mimetype for images.
If we run this the image picker launches and we can select an image, but nothing happens when we return to our app; let’s fix that.
Displaying the selected image
The returned result from the file picker is a URI. To display the image we could read that and convert it to a bitmap and then use the Image composable, but we will instead use the Coil image library that will do all the heavy lifting for us.
We will need to keep track of the URI that has been returned, so we will define a remember
ed variable that will host that value, which we will initialize to null
. We will also define a second variable, a Boolean
that will tell us if we have a URI to render or not (we will see why we need this later).
Let’s update our code to handle the returned URI and display the selected image:
- We define a variable to indicate whether we have a valid URI to display.
- We define a second variable to hold our URI.
- When we receive the response from the file picker, we store the returned URI and we set the boolean indicating we have a valid URI if it’s not null.
- We check if we have a valid image to display.
- If so, we display the image using Coil’s
AsyncImage
composable.
With this, we can pick and display an image in our app:
Launching the camera
Launching the camera follows a similar pattern, but it’s slightly more involved because we need to provide the file for the camera to write to. When we open the file picker we are looking for a file that already exists, so all we need to do is accept a URI. For the camera, we first need to create a file, then make that file available to the camera so that it can write its output to it.
The way to do this in Android is using a FileProvider. This is a specialized subclass of ContentProvider that allows other apps to temporarily read or write to a file we own.
Let’s first define the contract we need to launch the camera. Like the previous one, this is already provided as a template, so all we have to do is add a new contract to our app; for this contract, the input will be a URI and the output a boolean that indicates if the action complete successfully:
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture(),
onResult = { success ->
hasImage = success
}
)
Here we can see the purpose of the hasImage
boolean we defined earlier, if the action is cancelled or there is an error we will receive a false
and we skip displaying the image.
Now we need to launch this action, but to do so we will need a URI pointing to the file that the camera app can write to, so let’s first implement our FileProvider. This involves 3 main steps: defining the paths for the files we want to share, implementing the FileProvider and finally adding it to our manifest file:
File paths:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="my_images"
path="images/" />
<cache-path
name="my_images"
path="images/" />
</paths>
File Provider implementation:
class ComposeFileProvider : FileProvider(
R.xml.filepaths
)
Manifest:
<provider
android:name=".ComposeFileProvider"
android:authorities="com.francescsoftware.composeplayground.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
I won’t go over this in details because it’s well documented in the official documentation but the gist is that we define a set of folders where we want to store the files to share (we can define different paths based on the type of folder we want to use, like files or cache), we subclass FileProvider and we provide the files paths we defined earlier, and finally we add a provider section to the manifest with our authority; worth of note is that we need to set grantUriPermissions
to true so that other apps can access the files we share.
This is all we need for our FileProvider, but we will also add a utility method that will create a temporary file and return its URI, so that we can use that when launching our contract:
- We get the path to the directory where we want the file to be stored. Note that this has to match one of the paths defined in our
filepaths.xml
file we described earlier. - We create a temporary file in this directory.
- We get the authority for our content provider (which has to match the one we defined in the manifest).
- Finally we get the URI for this file.
We are now ready to launch the camera. We already have our contract, so all we need to do is flesh out the button’s onClick lambda:
Button(
modifier = Modifier.padding(top = 16.dp),
onClick = {
val uri = ComposeFileProvider.getImageUri(context)
imageUri = uri
cameraLauncher.launch(uri)
},
) {
Text(
text = "Take photo"
)
}
Here we are using our utility function to get the file URI, and then use that to launch our contract. If we run the app, we get this result:
And that’s it, the full code is shown below. I hope you found this useful, and I’ll see you on the next one, happy coding!