Picture-Perfect Code: Exploring Camera & Gallery Features in Jetpack Compose.
Welcome to the grand adventure of conquering Camera and Gallery interactions in the thrilling realm of Jetpack Compose. Whether you’re building a social media app, a photography tool, or any application that involves capturing or selecting images, you’ve come to the right place.
We will go through the essential steps to seamlessly capture images with the camera and select existing images from the gallery while ensuring you navigate the complex terrain of permissions. By the end of this piece, you’ll be equipped with the knowledge and skills to confidently integrate camera and gallery functionality into your Jetpack Compose project.
To get started, ensure you have the right permissions added in your AndroidManifest.xml:
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
On the same file AndroidManifest.xml, we declare a provider as below:
<application
....
>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.app.id.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
....
</application>
The code above declares a content provider which allows applications to share data with other apps.
Next, in your app/src/main/res
folder, create a new Android Resource Directory of Resource Type xml. In the xml directory, create a file named file_paths
as referenced in the <provider>
element in the AndroidManifest.xml. Update file as follows:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-cache-path name="my_images" path="/"/>
</paths>
file_paths.xml in app/src/main/res/xml
Now we proceed to the interesting bits 😋. First we define imageUri
variable and initialize it with an empty Uri
(i.e., Uri.EMPTY
). Next up, we define the file
variable which holds the image file created using a function named createImageFile()
.
val uri = FileProvider.getUriForFile(...)
This code generates a Uri
for the file
using the Android FileProvider. It's used to create a secure content URI for sharing the file with other apps.
var imageUri by remember { mutableStateOf<Uri?>(Uri.EMPTY) }
val file = context.createImageFile()
val uri = FileProvider.getUriForFile(
Objects.requireNonNull(getApplicationContext(context)),
"com.app.id.fileProvider", file
)
fun Context.createImageFile(): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
return File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
externalCacheDir /* directory */
)
}
Next, we define the camera permission state and the camera launcher as below:
val cameraPermissionState
The purpose of this code is to initialize and manage the state of the camera permission.
val cameraLauncher = rememberLauncherForActivityResult(...)
This line sets up a camera launcher using the rememberLauncherForActivityResult
function. It configures an activity result contract to take a picture with the device's camera. When the picture-taking operation is complete, the onResult
block is called with a success
parameter, which indicates whether the operation was successful.
val cameraPermissionState = rememberPermissionState(
permission = Manifest.permission.CAMERA
)
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicture(),
onResult = { success ->
if (success) imageUri = uri
if (imageUri.toString().isNotEmpty()) {
Log.d("myImageUri", "$imageUri ")
}
}
)
Similar to the camera steps, we define the media permission state and the gallery launcher as below:
val mediaPermissionState
holds the permission state information for accessing media resources. In this case, you are specifying a list of permissions that your app needs to access media resources. The list is constructed conditionally based on the Android version (SDK_INT). It ensures compatibility with Android 12 (Android version code-named Tiramisu) and earlier versions.
val galleryLauncher = rememberLauncherForActivityResult(...)
This function sets up a launcher to launch an activity for selecting an image from the gallery, and once an image is selected, it sets the selected image’s URI (imageUri
) and logs it (here you can do what you need to do with the uri)
val mediaPermissionState = rememberMultiplePermissionsState(
permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) listOf(
Manifest.permission.READ_MEDIA_IMAGES
) else listOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
)
)
val galleryLauncher =
rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
val data: Intent? = it.data
if (data != null) {
imageUri = Uri.parse(data.data.toString())
if (imageUri.toString().isNotEmpty()) {
Log.d("myImageUri", "$imageUri ")
}
}
}
}
Finally, we define the variables we’ll use to check the status of the permissions we need for both camera and gallery;
val hasCameraPermission = cameraPermissionState.status.isGranted
val hasMediaPermission = mediaPermissionState.allPermissionsGranted
Here’s a sample complete snippet that sets up a Column
layout containing two rows, each representing a different action: selecting an image from the gallery and taking a photo. It demonstrates conditional logic to handle actions based on whether specific permissions are granted or not. When the user interacts with the rows, the app either opens the gallery or camera or initiates the permission request flow, depending on the permissions’ status.
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(16.dp)
.clickable {
if(hasCameraPermission){
cameraLauncher.launch(uri)
} else {
cameraPermissionState.launchPermissionRequest()
}
},
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
ImageFromRes(imageRes = R.drawable.ic_gallery)
AppText(textValue = "Select from gallery")
}
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(16.dp)
.clickable {
if(hasMediaPermission){
galleryLauncher.launch(galleryIntent)
} else {
mediaPermissionState.launchMultiplePermissionRequest()
}
},
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
ImageFromRes(imageRes = R.drawable.ic_camera)
AppText(textValue = "Take a photo" )
}
}
In summary, to launch camera you check for camera permissions and launch camera as below if true or else launch the permission request flow, which will typically display a system dialog to ask the user for permission to access the camera.
if(hasCameraPermission){
cameraLauncher.launch(uri)
} else {
cameraPermissionState.launchPermissionRequest()
}
Similarly, to launch gallery, you check for media permissions if true or else launch the permission request flow, which will typically display a system dialog to ask the user for the required media permissions.
if(hasMediaPermission){
galleryLauncher.launch(galleryIntent)
} else {
mediaPermissionState.launchMultiplePermissionRequest()
}
Congratulations! 📸 You’ve made it this far, which means your camera and gallery features should be ready to rock ’n’ roll. Go ahead and give them a whirl — it’s the moment of truth! 🌟! Whether it’s capturing an image or selecting one from your gallery, you’re about to get your hands on that all-important URI.
Now, stand by for the next piece on how to display those images and maybe even send them on a magical journey to the cloud! 😉
May the pixels be ever in your favor! 🫶🏽