« 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:
contract
— this is the action we want to take.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@Composable2fun ImagePicker(3 modifier: Modifier = Modifier,4) {5 Box(6 modifier = modifier,7 ) {8 Column(9 modifier = Modifier10 .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}
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@Composable2fun ImagePicker(3 modifier: Modifier = Modifier,4) {5 // 16 val imagePicker = rememberLauncherForActivityResult(7 contract = ActivityResultContracts.GetContent(),8 onResult = { uri ->9 // TODO10 }11 )1213 Box(14 modifier = modifier,15 ) {16 Column(17 modifier = Modifier18 .align(Alignment.BottomCenter)19 .padding(bottom = 32.dp),20 horizontalAlignment = Alignment.CenterHorizontally,21 ) {22 Button(23 onClick = {24 // 225 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 // TODO36 },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 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.
1@Composable2fun ImagePicker(3 modifier: Modifier = Modifier,4) {5 // 16 var hasImage by remember {7 mutableStateOf(false)8 }9 // 210 var imageUri by remember {11 mutableStateOf<Uri?>(null)12 }1314 val imagePicker = rememberLauncherForActivityResult(15 contract = ActivityResultContracts.GetContent(),16 onResult = { uri ->17 // 318 hasImage = uri != null19 imageUri = uri20 }21 )2223 Box(24 modifier = modifier,25 ) {26 // 427 if (hasImage && imageUri != null) {28 // 529 AsyncImage(30 model = imageUri,31 modifier = Modifier.fillMaxWidth(),32 contentDescription = "Selected image",33 )34 }35 Column(36 modifier = Modifier37 .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 // TODO54 },55 ) {56 Text(57 text = "Take photo"58 )59 }60 }61 }62}
- 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.
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 = success5 }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-path4 name="my_images"5 path="images/" />6 <cache-path7 name="my_images"8 path="images/" />9</paths>
File Provider implementation:
1class ComposeFileProvider : FileProvider(2 R.xml.filepaths3)
Manifest:
1<provider2 android:name=".ComposeFileProvider"3 android:authorities="com.francescsoftware.composeplayground.fileprovider"4 android:grantUriPermissions="true"5 android:exported="false">6 <meta-data7 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.filepaths3) {4 companion object {5 fun getImageUri(context: Context): Uri {6 // 17 val directory = File(context.cacheDir, "images")8 directory.mkdirs()9 // 210 val file = File.createTempFile(11 "selected_image_",12 ".jpg",13 directory14 )15 // 316 val authority = context.packageName + ".fileprovider"17 // 418 return getUriForFile(19 context,20 authority,21 file,22 )23 }24 }25}
- We get the path to the directory where we want the file to be stored.
- 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:
1Button(2 modifier = Modifier.padding(top = 16.dp),3 onClick = {4 val uri = ComposeFileProvider.getImageUri(context)5 imageUri = uri6 cameraLauncher.launch(uri)7 },8) {9 Text(10 text = "Take photo"11 )12}
For full working code please visit
https://github.com/anishakd4/ActivityResultContractsJetpackCompose