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:
- All library features and configurations
| 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 viaImagePickerResult(Idle → Loading → Success/Dismissed/Error). NoRender(), no manual booleans.- Cross-platform — Android, iOS, Desktop, Web
- Camera & Gallery — Direct access with unified API
- Image Cropping — Built-in crop functionality
- Smart Compression — Configurable quality levels
- EXIF Metadata — GPS, camera info, timestamps (Android/iOS)
- PDF Support — Select PDF documents alongside images
- Extension Functions — Easy image processing (
loadPainter(),loadBytes(),loadBase64()) - Permission Handling — Automatic permission management
- Async Processing — Non-blocking UI with coroutines
- Format Support — JPEG, PNG, HEIC, HEIF, WebP, GIF, BMP, PDF
| 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:
- Mobile Demos - Android & iOS camera/gallery functionality
- Desktop Demo - File picker and image processing
- Web Demo - React integration with WebRTC camera
- Crop Demo - Interactive image cropping across platforms
| 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:
- WebRTC Camera Access - Mobile & desktop camera support
- TypeScript Support - Full type definitions included
- Drag & Drop - File picker with drag and drop
- React Components - Ready-to-use React components
- Cross-Framework - Works with React, Vue, Angular, Vanilla JS
- Images: Opens native Android gallery for photos
- PDFs: Opens file explorer for document access
- Mixed Types: Automatically chooses best picker for content type
- Automatic Detection: No configuration needed - works out of the box!
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:
- Select your app target → Build Phases → Link Binary With Libraries
- Click +, search for CoreLocation, and click Add
- Clean (⇧⌘K) and rebuild
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 💻 |
YaminMahdi 💻 |
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!


