
Cross-platform image picker and camera library enables seamless camera access, gallery selection, custom UI, and smart permission handling, ensuring a smooth, customizable user experience.
Cross-platform Image Picker & Camera Library for Kotlin Multiplatform
Easily capture or select images on Android, iOS, Desktop, and Web — all with a single API.
Built with Compose Multiplatform, designed for simplicity, performance, and flexibility.
ImagePickerKMP saves you 2 weeks of native Android/iOS/Web integration work.
It's free and open source. If your app or company benefits from it, consider sponsoring to keep it maintained and updated with every new KMP/Compose release.
→ Become a sponsor
Full-featured sample application showcasing:
| Requirement | Minimum version |
|---|---|
| Kotlin | 2.3.20 (breaking change — see CHANGELOG) |
| Compose Multiplatform | 1.10.3 |
| Ktor | 3.4.1 |
Android minSdk
|
24 |
Android compileSdk
|
36 |
Note: This library is compiled with Kotlin 2.3.20. Projects using Kotlin < 2.3.x will get an ABI incompatibility error at compile time. If you need Kotlin 2.1.x support, use a previous version of this library.
Kotlin Multiplatform:
dependencies {
implementation("io.github.ismoy:imagepickerkmp:1.0.35-alpha1")
}React/JavaScript:
npm install imagepickerkmpThe modern, idiomatic Compose API. A single state holder — no manual booleans, no Render() call needed.
@Composable
fun basicUsageScreen() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
selectionLimit = 20
)
)
)
val result = picker.result
Scaffold(
modifier = Modifier
.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Basic Usage") },
navigationIcon = {
IconButton(onClick = {}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Go back"
)
}
}
)
},
bottomBar = {
BottomAppBar {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { picker.launchCamera() },
modifier = Modifier.weight(1f)
) {
Text("Camera")
}
Button(
onClick = { picker.launchGallery() },
modifier = Modifier.weight(1f)
) {
Text("Gallery")
}
}
}
}
){scaffoldPadding->
Column(
modifier = Modifier
.padding(scaffoldPadding)
.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
when (result) {
is ImagePickerResult.Loading -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
CircularProgressIndicator()
Text(
text = "Loading...",
color = Color.Gray,
modifier = Modifier.padding(top = 12.dp)
)
}
}
is ImagePickerResult.Success -> {
// Result here
}
is ImagePickerResult.Error -> {
Text(
text = "Error: ${result.exception.message}",
color = Color.Red,
modifier = Modifier.padding(16.dp)
)
}
is ImagePickerResult.Dismissed -> {
Text("Selection cancelled", color = Color.Gray)
}
is ImagePickerResult.Idle -> {
Text("Press a button to get started", color = Color.Gray)
}
}
}
}
}
}Per-launch overrides:
// Override gallery options for a single launch
picker.launchGallery(
allowMultiple = true,
selectionLimit = 5,
mimeTypes = listOf(MimeType.IMAGE_JPEG),
includeExif = true
)
// Override camera options for a single launch
picker.launchCamera(
cameraCaptureConfig = CameraCaptureConfig(compressionLevel = CompressionLevel.HIGH),
enableCrop = false
)TL;DR: Use
rememberImagePickerKMPfor all new code. The legacyImagePickerLauncher/GalleryPickerLauncherare deprecated and will be removed in a future major release.
| Legacy API (v1) — Deprecated | New API (v2) — Recommended | |
|---|---|---|
| Camera | ImagePickerLauncher(config = ...) |
picker.launchCamera() |
| Gallery | GalleryPickerLauncher(...) |
picker.launchGallery() |
| Result handling | Callbacks (onPhotoCaptured, onDismiss, onError) |
Reactive when (picker.result)
|
| State management | Manual showCamera, showGallery booleans |
Automatic via ImagePickerKMPState
|
| Per-launch config | Not supported | Override any param on each launch*() call |
| Reset | Call onDismiss callback |
picker.reset() |
| Configuration |
ImagePickerConfig + GalleryPickerConfig
|
ImagePickerKMPConfig (unified) |
| Legacy pattern | New API equivalent |
|---|---|
showCamera = true |
picker.launchCamera() |
showGallery = true |
picker.launchGallery() |
onPhotoCaptured = { result -> ... } |
is ImagePickerResult.Success -> result.photos |
onDismiss = { showCamera = false } |
is ImagePickerResult.Dismissed -> ... |
onError = { e -> ... } |
is ImagePickerResult.Error -> result.exception |
ImagePickerConfig(cameraCaptureConfig = ...) |
ImagePickerKMPConfig(cameraCaptureConfig = ...) |
GalleryPickerConfig(includeExif = true) |
ImagePickerKMPConfig(galleryConfig = GalleryConfig(includeExif = true)) |
allowMultiple = true in GalleryPickerLauncher
|
picker.launchGallery(allowMultiple = true) |
The legacy API still works and will not break existing apps. You will see a compiler warning recommending migration to rememberImagePickerKMP.
Camera Capture (legacy):
var showCamera by remember { mutableStateOf(false) }
var capturedPhoto by remember { mutableStateOf<PhotoResult?>(null) }
if (showCamera) {
ImagePickerLauncher( // Deprecated — migrate to rememberImagePickerKMP
config = ImagePickerConfig(
onPhotoCaptured = { result ->
capturedPhoto = result
showCamera = false
},
onError = { showCamera = false },
onDismiss = { showCamera = false }
)
)
}
Button(onClick = { showCamera = true }) {
Text("Take Photo")
}Gallery Selection (legacy):
var showGallery by remember { mutableStateOf(false) }
var selectedImages by remember { mutableStateOf<List<PhotoResult>>(emptyList()) }
if (showGallery) {
GalleryPickerLauncher( // Deprecated — migrate to rememberImagePickerKMP
config = GalleryPickerConfig(includeExif = true),
onPhotosSelected = { photos ->
selectedImages = photos
showGallery = false
},
onError = { showGallery = false },
onDismiss = { showGallery = false },
allowMultiple = true
)
}
Button(onClick = { showGallery = true }) {
Text("Choose from Gallery")
}Camera Preview Not Showing? Some developers have reported that the camera usage indicator appears, but the preview doesn't show up. This happens when ImagePickerLauncher is not placed inside a visible container composable.
✅ Correct usage:
Box(modifier = Modifier.fillMaxSize()) {
if (showCamera) {
ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { /* handle image */ },
onDismiss = { showCamera = false }
)
)
}
}❌ Incorrect usage:
if (showCamera) {
ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { /* handle image */ },
onDismiss = { showCamera = false }
)
)
}💡 Always wrap the camera launcher inside a composable container (Box, Column, Row) and control its visibility with state.
Thanks to @rnstewart and other contributors for pointing this out! 🙏
rememberImagePickerKMP — New idiomatic API: single state holder, launchCamera() / launchGallery() with per-launch overrides, reactive result via ImagePickerResult (Idle → Loading → Success/Dismissed/Error). No Render(), no manual booleans.loadPainter(), loadBytes(), loadBase64())| Platform | Minimum Version | Camera | Gallery | Crop | EXIF | Status |
|---|---|---|---|---|---|---|
| Android | API 21+ | ✅ | ✅ | ✅ | ✅ | ✅ |
| iOS | iOS 12.0+ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Desktop | JDK 11+ | ❌ | ✅ | ✅ | ❌ | ✅ |
| JS/Web | Modern Browsers | ❌ | ✅ | ✅ | ❌ | ✅ |
| Wasm/Web | Modern Browsers | ✅ |
Experience ImagePickerKMP in action:
| Resource | Description |
|---|---|
| Integration Guide | Complete setup and configuration |
| Customization Guide | UI customization and theming |
| React Guide | Web development setup |
| Permissions Guide | Platform permissions |
| API Reference | Complete API documentation |
ImagePickerLauncher(
config = ImagePickerConfig(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH, // LOW, MEDIUM, HIGH
skipConfirmation = true
)
)
)ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { result ->
result.exif?.let { exif ->
println(" Location: ${exif.latitude}, ${exif.longitude}")
println(" Camera: ${exif.cameraModel}")
println(" Taken: ${exif.dateTaken}")
}
},
cameraCaptureConfig = CameraCaptureConfig(
includeExif = true // Android/iOS only
)
)
)
GalleryPickerLauncher(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG),
includeExif = true // Android/iOS only
)// Images only
GalleryPickerLauncher(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG),
enableCrop = true
)
// Images and PDFs
GalleryPickerLauncher(
allowMultiple = true,
mimeTypes = listOf(
MimeType.IMAGE_JPEG,
MimeType.IMAGE_PNG,
MimeType.APPLICATION_PDF // PDF support
)
)Add to your Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access needed to take photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access needed to select images</string>Process images easily with built-in extension functions:
ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { result ->
val imageBytes = result.loadBytes() // ByteArray for file operations
val imagePainter = result.loadPainter() // Painter for Compose UI
val imageBitmap = result.loadImageBitmap() // ImageBitmap for graphics
val imageBase64 = result.loadBase64() // Base64 string for APIs
}
)
)ImagePickerKMP is available as an NPM package for web development:
npm install imagepickerkmpFeatures:
Complete React Integration Guide →
If your iOS build fails with:
ld: Undefined symbols: _OBJC_CLASS_$_CLLocation
linker command failed with exit code 1
Android and JVM Desktop work fine, but iOS fails during the linking phase.
Fix: Add CoreLocation.framework manually in Xcode:
No code changes needed. See FAQ and Integration Guide for full details.
ImagePickerKMP is free and open source. Maintaining it across Android, iOS, Desktop, Web and WASM with every Kotlin/Compose Multiplatform release takes real time and effort.
If this library saves you time or money in production, please consider supporting it:
| Tier | Amount | Benefit |
|---|---|---|
| ☕ Coffee | $5/mo | Name in the backers list |
| 🥈 Silver | $25/mo | Logo in README + priority issue response |
![]() james-codersHT |
Sponsors get their name/logo displayed here. → Become a sponsor
Thanks to these wonderful people (emoji key):
|
ismoy 💻 📖 🚧 🎨 🤔 |
medAndro 💻 🐛 |
daniil-pastuhov 💻 |
This project follows the all-contributors specification. Contributions of any kind welcome!
Made with ❤️ for the Kotlin Multiplatform community
Star this repo if it helped you!
Cross-platform Image Picker & Camera Library for Kotlin Multiplatform
Easily capture or select images on Android, iOS, Desktop, and Web — all with a single API.
Built with Compose Multiplatform, designed for simplicity, performance, and flexibility.
ImagePickerKMP saves you 2 weeks of native Android/iOS/Web integration work.
It's free and open source. If your app or company benefits from it, consider sponsoring to keep it maintained and updated with every new KMP/Compose release.
→ Become a sponsor
Full-featured sample application showcasing:
| Requirement | Minimum version |
|---|---|
| Kotlin | 2.3.20 (breaking change — see CHANGELOG) |
| Compose Multiplatform | 1.10.3 |
| Ktor | 3.4.1 |
Android minSdk
|
24 |
Android compileSdk
|
36 |
Note: This library is compiled with Kotlin 2.3.20. Projects using Kotlin < 2.3.x will get an ABI incompatibility error at compile time. If you need Kotlin 2.1.x support, use a previous version of this library.
Kotlin Multiplatform:
dependencies {
implementation("io.github.ismoy:imagepickerkmp:1.0.35-alpha1")
}React/JavaScript:
npm install imagepickerkmpThe modern, idiomatic Compose API. A single state holder — no manual booleans, no Render() call needed.
@Composable
fun basicUsageScreen() {
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
selectionLimit = 20
)
)
)
val result = picker.result
Scaffold(
modifier = Modifier
.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Basic Usage") },
navigationIcon = {
IconButton(onClick = {}) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Go back"
)
}
}
)
},
bottomBar = {
BottomAppBar {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { picker.launchCamera() },
modifier = Modifier.weight(1f)
) {
Text("Camera")
}
Button(
onClick = { picker.launchGallery() },
modifier = Modifier.weight(1f)
) {
Text("Gallery")
}
}
}
}
){scaffoldPadding->
Column(
modifier = Modifier
.padding(scaffoldPadding)
.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
when (result) {
is ImagePickerResult.Loading -> {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
CircularProgressIndicator()
Text(
text = "Loading...",
color = Color.Gray,
modifier = Modifier.padding(top = 12.dp)
)
}
}
is ImagePickerResult.Success -> {
// Result here
}
is ImagePickerResult.Error -> {
Text(
text = "Error: ${result.exception.message}",
color = Color.Red,
modifier = Modifier.padding(16.dp)
)
}
is ImagePickerResult.Dismissed -> {
Text("Selection cancelled", color = Color.Gray)
}
is ImagePickerResult.Idle -> {
Text("Press a button to get started", color = Color.Gray)
}
}
}
}
}
}Per-launch overrides:
// Override gallery options for a single launch
picker.launchGallery(
allowMultiple = true,
selectionLimit = 5,
mimeTypes = listOf(MimeType.IMAGE_JPEG),
includeExif = true
)
// Override camera options for a single launch
picker.launchCamera(
cameraCaptureConfig = CameraCaptureConfig(compressionLevel = CompressionLevel.HIGH),
enableCrop = false
)TL;DR: Use
rememberImagePickerKMPfor all new code. The legacyImagePickerLauncher/GalleryPickerLauncherare deprecated and will be removed in a future major release.
| Legacy API (v1) — Deprecated | New API (v2) — Recommended | |
|---|---|---|
| Camera | ImagePickerLauncher(config = ...) |
picker.launchCamera() |
| Gallery | GalleryPickerLauncher(...) |
picker.launchGallery() |
| Result handling | Callbacks (onPhotoCaptured, onDismiss, onError) |
Reactive when (picker.result)
|
| State management | Manual showCamera, showGallery booleans |
Automatic via ImagePickerKMPState
|
| Per-launch config | Not supported | Override any param on each launch*() call |
| Reset | Call onDismiss callback |
picker.reset() |
| Configuration |
ImagePickerConfig + GalleryPickerConfig
|
ImagePickerKMPConfig (unified) |
| Legacy pattern | New API equivalent |
|---|---|
showCamera = true |
picker.launchCamera() |
showGallery = true |
picker.launchGallery() |
onPhotoCaptured = { result -> ... } |
is ImagePickerResult.Success -> result.photos |
onDismiss = { showCamera = false } |
is ImagePickerResult.Dismissed -> ... |
onError = { e -> ... } |
is ImagePickerResult.Error -> result.exception |
ImagePickerConfig(cameraCaptureConfig = ...) |
ImagePickerKMPConfig(cameraCaptureConfig = ...) |
GalleryPickerConfig(includeExif = true) |
ImagePickerKMPConfig(galleryConfig = GalleryConfig(includeExif = true)) |
allowMultiple = true in GalleryPickerLauncher
|
picker.launchGallery(allowMultiple = true) |
The legacy API still works and will not break existing apps. You will see a compiler warning recommending migration to rememberImagePickerKMP.
Camera Capture (legacy):
var showCamera by remember { mutableStateOf(false) }
var capturedPhoto by remember { mutableStateOf<PhotoResult?>(null) }
if (showCamera) {
ImagePickerLauncher( // Deprecated — migrate to rememberImagePickerKMP
config = ImagePickerConfig(
onPhotoCaptured = { result ->
capturedPhoto = result
showCamera = false
},
onError = { showCamera = false },
onDismiss = { showCamera = false }
)
)
}
Button(onClick = { showCamera = true }) {
Text("Take Photo")
}Gallery Selection (legacy):
var showGallery by remember { mutableStateOf(false) }
var selectedImages by remember { mutableStateOf<List<PhotoResult>>(emptyList()) }
if (showGallery) {
GalleryPickerLauncher( // Deprecated — migrate to rememberImagePickerKMP
config = GalleryPickerConfig(includeExif = true),
onPhotosSelected = { photos ->
selectedImages = photos
showGallery = false
},
onError = { showGallery = false },
onDismiss = { showGallery = false },
allowMultiple = true
)
}
Button(onClick = { showGallery = true }) {
Text("Choose from Gallery")
}Camera Preview Not Showing? Some developers have reported that the camera usage indicator appears, but the preview doesn't show up. This happens when ImagePickerLauncher is not placed inside a visible container composable.
✅ Correct usage:
Box(modifier = Modifier.fillMaxSize()) {
if (showCamera) {
ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { /* handle image */ },
onDismiss = { showCamera = false }
)
)
}
}❌ Incorrect usage:
if (showCamera) {
ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { /* handle image */ },
onDismiss = { showCamera = false }
)
)
}💡 Always wrap the camera launcher inside a composable container (Box, Column, Row) and control its visibility with state.
Thanks to @rnstewart and other contributors for pointing this out! 🙏
rememberImagePickerKMP — New idiomatic API: single state holder, launchCamera() / launchGallery() with per-launch overrides, reactive result via ImagePickerResult (Idle → Loading → Success/Dismissed/Error). No Render(), no manual booleans.loadPainter(), loadBytes(), loadBase64())| Platform | Minimum Version | Camera | Gallery | Crop | EXIF | Status |
|---|---|---|---|---|---|---|
| Android | API 21+ | ✅ | ✅ | ✅ | ✅ | ✅ |
| iOS | iOS 12.0+ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Desktop | JDK 11+ | ❌ | ✅ | ✅ | ❌ | ✅ |
| JS/Web | Modern Browsers | ❌ | ✅ | ✅ | ❌ | ✅ |
| Wasm/Web | Modern Browsers | ✅ |
Experience ImagePickerKMP in action:
| Resource | Description |
|---|---|
| Integration Guide | Complete setup and configuration |
| Customization Guide | UI customization and theming |
| React Guide | Web development setup |
| Permissions Guide | Platform permissions |
| API Reference | Complete API documentation |
ImagePickerLauncher(
config = ImagePickerConfig(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH, // LOW, MEDIUM, HIGH
skipConfirmation = true
)
)
)ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { result ->
result.exif?.let { exif ->
println(" Location: ${exif.latitude}, ${exif.longitude}")
println(" Camera: ${exif.cameraModel}")
println(" Taken: ${exif.dateTaken}")
}
},
cameraCaptureConfig = CameraCaptureConfig(
includeExif = true // Android/iOS only
)
)
)
GalleryPickerLauncher(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG),
includeExif = true // Android/iOS only
)// Images only
GalleryPickerLauncher(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG),
enableCrop = true
)
// Images and PDFs
GalleryPickerLauncher(
allowMultiple = true,
mimeTypes = listOf(
MimeType.IMAGE_JPEG,
MimeType.IMAGE_PNG,
MimeType.APPLICATION_PDF // PDF support
)
)Add to your Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access needed to take photos</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access needed to select images</string>Process images easily with built-in extension functions:
ImagePickerLauncher(
config = ImagePickerConfig(
onPhotoCaptured = { result ->
val imageBytes = result.loadBytes() // ByteArray for file operations
val imagePainter = result.loadPainter() // Painter for Compose UI
val imageBitmap = result.loadImageBitmap() // ImageBitmap for graphics
val imageBase64 = result.loadBase64() // Base64 string for APIs
}
)
)ImagePickerKMP is available as an NPM package for web development:
npm install imagepickerkmpFeatures:
Complete React Integration Guide →
If your iOS build fails with:
ld: Undefined symbols: _OBJC_CLASS_$_CLLocation
linker command failed with exit code 1
Android and JVM Desktop work fine, but iOS fails during the linking phase.
Fix: Add CoreLocation.framework manually in Xcode:
No code changes needed. See FAQ and Integration Guide for full details.
ImagePickerKMP is free and open source. Maintaining it across Android, iOS, Desktop, Web and WASM with every Kotlin/Compose Multiplatform release takes real time and effort.
If this library saves you time or money in production, please consider supporting it:
| Tier | Amount | Benefit |
|---|---|---|
| ☕ Coffee | $5/mo | Name in the backers list |
| 🥈 Silver | $25/mo | Logo in README + priority issue response |
![]() james-codersHT |
Sponsors get their name/logo displayed here. → Become a sponsor
Thanks to these wonderful people (emoji key):
|
ismoy 💻 📖 🚧 🎨 🤔 |
medAndro 💻 🐛 |
daniil-pastuhov 💻 |
This project follows the all-contributors specification. Contributions of any kind welcome!
Made with ❤️ for the Kotlin Multiplatform community
Star this repo if it helped you!