A modern, Jetpack Compose-ready image picker library for Android.
- Works seamlessly with Jetpack Compose, XML + Kotlin, or both.
- Supports Camera and Gallery.
- Clean handling of runtime permissions – even on Android 13+
- Supports multiple image selection and compression
- Provides structured result callbacks for success and error handling
- Just works – no hidden setup, no ActivityResultContracts, and no more permission nightmares!
- Add to your project:
If using as a module:
implementation(project(":JetImagePicker"))
OR
implementation("com.github.nerojust:JetImagePicker:v1")
- Add to your settings.gradle:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- for Android 13+ -->
<application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application><?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="images" path="." />
</paths>@Composable
fun ImagePickerScreen() {
val context = LocalContext.current
var message by remember { mutableStateOf<String?>(null) }
val pickerState = rememberJetImagePickerState(
context = context,
config = JetImagePickerConfig(
enableCompression = true,
compressionQuality = 70,
allowMultiple = true,
targetWidth = 1024,
targetHeight = 1024
)
) { result ->
when (result) {
is ImagePickerResult.Success -> message = null
is ImagePickerResult.PermissionDenied -> {
message = "Permission denied: ${result.permission}"
//go ahead, all good
}
is ImagePickerResult.PermissionPermanentlyDenied -> {
message = "Permanently denied: ${result.permission}"
//do something
}
is ImagePickerResult.ShowRationale -> {
message = "Please allow ${result.permission} to proceed."
//do some business logic here
}
}
}
Column(Modifier.padding(16.dp)) {
Button(onClick = pickerState.pickFromGallery) {
Text("Pick from Gallery")
}
Spacer(Modifier.height(8.dp))
Button(onClick = pickerState.captureWithCamera) {
Text("Capture with Camera")
}
Spacer(Modifier.height(16.dp))
when (pickerState.selectedImageUris.size) {
1 -> ImagePreview(uri = pickerState.selectedImageUris.first())
in 2..Int.MAX_VALUE -> MultiImagePreview(imageUris = pickerState.selectedImageUris)
}
message?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall
)
}
}
}JetImagePickerConfig(
enableCompression = true,
compressionQuality = 70, // 0–100
allowMultiple = true,
targetWidth = 1024,
targetHeight = 1024
)Use the ImagePickerResult sealed class:
sealed class ImagePickerResult {
data class Success(val uris: List<Uri>) : ImagePickerResult()
data class PermissionDenied(val permission: String) : ImagePickerResult()
data class PermissionPermanentlyDenied(val permission: String) : ImagePickerResult()
data class ShowRationale(val permission: String) : ImagePickerResult()
}Contributions are welcome! Open issues, submit PRs, or suggest ideas.
Made with 💙 by Nerojust
MIT License. See LICENSE for details.
