
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.41")
}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
)The library exposes a single public API based on the standard Compose state-hook pattern:
@Composable
fun MyScreen() {
val picker = rememberImagePickerKMP()
val result = picker.result
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")
}
}
when (result) {
is ImagePickerResult.Loading -> CircularProgressIndicator()
is ImagePickerResult.Success -> {
val photos = result.photos
if (photos.size == 1) {
CameraResultCard(photo = photos.first())
} else {
MultiPhotoGrid(photos = photos)
}
}
is ImagePickerResult.Error -> Text("Error: ${result.exception.message}", color = Color.Red)
is ImagePickerResult.Dismissed -> Text("Selection cancelled", color = Color.Gray)
is ImagePickerResult.Idle -> Text("Press a button to get started", color = Color.Gray)
}
}Pass an ImagePickerKMPConfig to customize behavior globally:
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cropConfig = CropConfig(enabled = true, squareCrop = true),
galleryConfig = GalleryConfig(allowMultiple = true, selectionLimit = 10),
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH,
includeExif = true
)
)
)Override any parameter for a single invocation without changing the global config:
// Allow multiple selection only for this launch
picker.launchGallery(allowMultiple = true, selectionLimit = 5)
// Enable EXIF only for this camera launch
picker.launchCamera(cameraCaptureConfig = CameraCaptureConfig(includeExif = true))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 |
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH // LOW, MEDIUM, HIGH
),
permissionAndConfirmationConfig = PermissionAndConfirmationConfig(
skipConfirmation = true
)
)
)
picker.launchCamera()val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
includeExif = true // Android/iOS only
)
)
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
result.photos.first().exif?.let { exif ->
println(" Location: ${exif.latitude}, ${exif.longitude}")
println(" Camera: ${exif.cameraModel}")
println(" Taken: ${exif.dateTaken}")
}
}
else -> Unit
}val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG),
includeExif = true // Android/iOS only
)
)
)
picker.launchGallery()// Images only
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG)
),
cropConfig = CropConfig(enabled = true)
)
)
picker.launchGallery()
// Images and PDFs
picker.launchGallery(
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:
val picker = rememberImagePickerKMP()
when (val result = picker.result) {
is ImagePickerResult.Success -> {
val photo = result.photos.first()
val imageBytes = photo.loadBytes() // ByteArray for file operations
val imagePainter = photo.loadPainter() // Painter for Compose UI
val imageBitmap = photo.loadImageBitmap() // ImageBitmap for graphics
val imageBase64 = photo.loadBase64() // Base64 string for APIs
// File system operations (kotlinx-io)
val absolutePath = photo.absolutePath // String - absolute file path
val path = photo.asPath() // Path object for file operations
val exists = photo.exists() // Check if file exists
val rawSource = photo.asRawSource() // RawSource for low-level reading
val source = photo.asSource() // Buffered Source for efficient reading
// Copy photo to another location
val sink = SystemFileSystem.sink(Path("copy.jpg"))
photo.transferToSink(sink) // Transfer content to RawSink
}
else -> Unit
}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 💻 🐛 |
YaminMahdi 💻 |
jadlr 💻 |
daniil-pastuhov 💻 |
fanqieVip 💻 |
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.41")
}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
)The library exposes a single public API based on the standard Compose state-hook pattern:
@Composable
fun MyScreen() {
val picker = rememberImagePickerKMP()
val result = picker.result
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")
}
}
when (result) {
is ImagePickerResult.Loading -> CircularProgressIndicator()
is ImagePickerResult.Success -> {
val photos = result.photos
if (photos.size == 1) {
CameraResultCard(photo = photos.first())
} else {
MultiPhotoGrid(photos = photos)
}
}
is ImagePickerResult.Error -> Text("Error: ${result.exception.message}", color = Color.Red)
is ImagePickerResult.Dismissed -> Text("Selection cancelled", color = Color.Gray)
is ImagePickerResult.Idle -> Text("Press a button to get started", color = Color.Gray)
}
}Pass an ImagePickerKMPConfig to customize behavior globally:
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cropConfig = CropConfig(enabled = true, squareCrop = true),
galleryConfig = GalleryConfig(allowMultiple = true, selectionLimit = 10),
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH,
includeExif = true
)
)
)Override any parameter for a single invocation without changing the global config:
// Allow multiple selection only for this launch
picker.launchGallery(allowMultiple = true, selectionLimit = 5)
// Enable EXIF only for this camera launch
picker.launchCamera(cameraCaptureConfig = CameraCaptureConfig(includeExif = true))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 |
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
compressionLevel = CompressionLevel.HIGH // LOW, MEDIUM, HIGH
),
permissionAndConfirmationConfig = PermissionAndConfirmationConfig(
skipConfirmation = true
)
)
)
picker.launchCamera()val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
cameraCaptureConfig = CameraCaptureConfig(
includeExif = true // Android/iOS only
)
)
)
when (val result = picker.result) {
is ImagePickerResult.Success -> {
result.photos.first().exif?.let { exif ->
println(" Location: ${exif.latitude}, ${exif.longitude}")
println(" Camera: ${exif.cameraModel}")
println(" Taken: ${exif.dateTaken}")
}
}
else -> Unit
}val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG),
includeExif = true // Android/iOS only
)
)
)
picker.launchGallery()// Images only
val picker = rememberImagePickerKMP(
config = ImagePickerKMPConfig(
galleryConfig = GalleryConfig(
allowMultiple = true,
mimeTypes = listOf(MimeType.IMAGE_JPEG, MimeType.IMAGE_PNG)
),
cropConfig = CropConfig(enabled = true)
)
)
picker.launchGallery()
// Images and PDFs
picker.launchGallery(
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:
val picker = rememberImagePickerKMP()
when (val result = picker.result) {
is ImagePickerResult.Success -> {
val photo = result.photos.first()
val imageBytes = photo.loadBytes() // ByteArray for file operations
val imagePainter = photo.loadPainter() // Painter for Compose UI
val imageBitmap = photo.loadImageBitmap() // ImageBitmap for graphics
val imageBase64 = photo.loadBase64() // Base64 string for APIs
// File system operations (kotlinx-io)
val absolutePath = photo.absolutePath // String - absolute file path
val path = photo.asPath() // Path object for file operations
val exists = photo.exists() // Check if file exists
val rawSource = photo.asRawSource() // RawSource for low-level reading
val source = photo.asSource() // Buffered Source for efficient reading
// Copy photo to another location
val sink = SystemFileSystem.sink(Path("copy.jpg"))
photo.transferToSink(sink) // Transfer content to RawSink
}
else -> Unit
}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 💻 🐛 |
YaminMahdi 💻 |
jadlr 💻 |
daniil-pastuhov 💻 |
fanqieVip 💻 |
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!