« Using Activity result contracts in Jetpack Compose

Using Activity Result Contracts in Jetpack Compose

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.

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:

  1. contract — this is the action we want to take.

  2. 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. Here we will be using the ActivityResultContracts.GetContent() for selecting an image from the gallery, and ActivityResultContracts.TakePicture() to take a photo

Add buttons

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.

1@Composable
2fun ImagePicker(
3 modifier: Modifier = Modifier,
4) {
5 Box(
6 modifier = modifier,
7 ) {
8 Column(
9 modifier = Modifier
10 .align(Alignment.BottomCenter)
11 .padding(bottom = 32.dp),
12 horizontalAlignment = Alignment.CenterHorizontally,
13 ) {
14 Button(
15 onClick = { /* TODO */ },
16 ) {
17 Text(
18 text = "Select Image"
19 )
20 }
21 Button(
22 modifier = Modifier.padding(top = 16.dp),
23 onClick = { /* TODO */ },
24 ) {
25 Text(
26 text = "Take photo"
27 )
28 }
29 }
30 }
31}

app_screen.png

Adding the image picker contract

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.

1@Composable
2fun ImagePicker(
3 modifier: Modifier = Modifier,
4) {
5 // 1
6 val imagePicker = rememberLauncherForActivityResult(
7 contract = ActivityResultContracts.GetContent(),
8 onResult = { uri ->
9 // TODO
10 }
11 )
12
13 Box(
14 modifier = modifier,
15 ) {
16 Column(
17 modifier = Modifier
18 .align(Alignment.BottomCenter)
19 .padding(bottom = 32.dp),
20 horizontalAlignment = Alignment.CenterHorizontally,
21 ) {
22 Button(
23 onClick = {
24 // 2
25 imagePicker.launch("image/*")
26 },
27 ) {
28 Text(
29 text = "Select Image"
30 )
31 }
32 Button(
33 modifier = Modifier.padding(top = 16.dp),
34 onClick = {
35 // TODO
36 },
37 ) {
38 Text(
39 text = "Take photo"
40 )
41 }
42 }
43 }
44}

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 remembered 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.

1@Composable
2fun ImagePicker(
3 modifier: Modifier = Modifier,
4) {
5 // 1
6 var hasImage by remember {
7 mutableStateOf(false)
8 }
9 // 2
10 var imageUri by remember {
11 mutableStateOf<Uri?>(null)
12 }
13
14 val imagePicker = rememberLauncherForActivityResult(
15 contract = ActivityResultContracts.GetContent(),
16 onResult = { uri ->
17 // 3
18 hasImage = uri != null
19 imageUri = uri
20 }
21 )
22
23 Box(
24 modifier = modifier,
25 ) {
26 // 4
27 if (hasImage && imageUri != null) {
28 // 5
29 AsyncImage(
30 model = imageUri,
31 modifier = Modifier.fillMaxWidth(),
32 contentDescription = "Selected image",
33 )
34 }
35 Column(
36 modifier = Modifier
37 .align(Alignment.BottomCenter)
38 .padding(bottom = 32.dp),
39 horizontalAlignment = Alignment.CenterHorizontally,
40 ) {
41 Button(
42 onClick = {
43 imagePicker.launch("image/*")
44 },
45 ) {
46 Text(
47 text = "Select Image"
48 )
49 }
50 Button(
51 modifier = Modifier.padding(top = 16.dp),
52 onClick = {
53 // TODO
54 },
55 ) {
56 Text(
57 text = "Take photo"
58 )
59 }
60 }
61 }
62}

photo_picker.png

  1. We define a variable to indicate whether we have a valid URI to display.
  2. We define a second variable to hold our URI.
  3. 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.
  4. We check if we have a valid image to display.
  5. If so, we display the image using Coil’s AsyncImage composable.

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:

1val cameraLauncher = rememberLauncherForActivityResult(
2 contract = ActivityResultContracts.TakePicture(),
3 onResult = { success ->
4 hasImage = success
5 }
6)

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.

1<?xml version="1.0" encoding="utf-8"?>
2<paths>
3 <files-path
4 name="my_images"
5 path="images/" />
6 <cache-path
7 name="my_images"
8 path="images/" />
9</paths>

File Provider implementation:

1class ComposeFileProvider : FileProvider(
2 R.xml.filepaths
3)

Manifest:

1<provider
2 android:name=".ComposeFileProvider"
3 android:authorities="com.francescsoftware.composeplayground.fileprovider"
4 android:grantUriPermissions="true"
5 android:exported="false">
6 <meta-data
7 android:name="android.support.FILE_PROVIDER_PATHS"
8 android:resource="@xml/filepaths" />
9</provider>

We define a set of folders where we want to store the files to share, 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.

1class ComposeFileProvider : FileProvider(
2 R.xml.filepaths
3) {
4 companion object {
5 fun getImageUri(context: Context): Uri {
6 // 1
7 val directory = File(context.cacheDir, "images")
8 directory.mkdirs()
9 // 2
10 val file = File.createTempFile(
11 "selected_image_",
12 ".jpg",
13 directory
14 )
15 // 3
16 val authority = context.packageName + ".fileprovider"
17 // 4
18 return getUriForFile(
19 context,
20 authority,
21 file,
22 )
23 }
24 }
25}
  1. We get the path to the directory where we want the file to be stored.
  2. We create a temporary file in this directory.
  3. We get the authority for our content provider (which has to match the one we defined in the manifest).
  4. 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:

1Button(
2 modifier = Modifier.padding(top = 16.dp),
3 onClick = {
4 val uri = ComposeFileProvider.getImageUri(context)
5 imageUri = uri
6 cameraLauncher.launch(uri)
7 },
8) {
9 Text(
10 text = "Take photo"
11 )
12}

camera_picker.png

For full working code please visit

https://github.com/anishakd4/ActivityResultContractsJetpackCompose