
Unifies media selection for images, videos, and files with system-camera capture, streaming reads, lightweight file references, seamless single-call API, and no custom camera UI or extra permissions.
A unified media-picker for Compose Multiplatform. One commonMain call site, every
target. No expect/actual leaks into your code, no custom camera UI, no CAMERA
permission to declare.
| Target | Pick image / video / file | System camera |
|---|---|---|
| Android | ✅ Photo Picker / SAF | ✅ ACTION_IMAGE_CAPTURE / ACTION_VIDEO_CAPTURE
|
| iOS | ✅ PHPicker / Document Picker | ✅ UIImagePickerController(.camera)
|
| Desktop (JVM) | ✅ FileDialog
|
Unsupported (no system camera) |
| Web (wasmJs) | ✅ <input type="file">
|
✅ on mobile-web via capture attribute |
// build.gradle.kts
dependencies {
implementation("io.github.aashutosh-rana:compose-media-picker:0.2.0")
}Repo: https://github.com/aashutosh-rana/compose-media-picker.
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val picker = rememberMediaPicker()
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
when (val r = picker.captureImage()) {
is MediaPickerResult.Success -> handle(r.data)
else -> Unit
}
}
}) { Text("Take photo") }
}
}
}That's it — no initialization call, no platform-specific imports beyond the call site.
rememberMediaPicker() auto-discovers the host: ComponentActivity on Android,
foreground UIViewController on iOS, the active Compose Window on Desktop, the browser
on Web. iOS / Desktop / Web call sites are identical to the snippet above.
The library declares its own FileProvider under ${applicationId}.mediakit.fileprovider
via manifest merging — you don't need to add anything to your AndroidManifest.xml.
You also don't need the CAMERA permission, because the system camera app owns it.
Delete any initializeMediaPicker(...) calls and their *PlatformContext imports.
That's the entire change — see CHANGELOG.md for the
full migration snippet.
Every call returns a MediaPickerResult<T>:
sealed interface MediaPickerResult<out T> {
data class Success<T>(val data: T) : MediaPickerResult<T>
data object Cancelled : MediaPickerResult<Nothing>
data class PermissionDenied(val permission: String, val permanentlyDenied: Boolean) : MediaPickerResult<Nothing>
data object Unsupported : MediaPickerResult<Nothing>
data class Error(val throwable: Throwable) : MediaPickerResult<Nothing>
}Unsupported lets you feature-detect cleanly:
when (val r = picker.captureImage()) {
is Success -> show(r.data)
Unsupported -> showMessage("No camera available")
Cancelled -> Unit
is Error -> showError(r.throwable)
is PermissionDenied -> showPermissionDialog()
}MediaFile is a lightweight reference — bytes are not loaded until you ask:
val file: MediaFile = (result as Success).data
file.source().use { source -> // streaming, off-main, cancellable
// …
}
scope.launch {
file.readProgress().collect { p -> updateBar(p) }
}A single Compose Multiplatform sample app — :samples:composeApp — runs on all four
targets from one shared commonMain Compose UI:
# Android (with emulator running)
./gradlew :samples:composeApp:installDebug
adb shell am start -n io.github.aashutosh.mediapicker.sample/.MainActivity
# Desktop
./gradlew :samples:composeApp:run
# Web (browser dev server on http://localhost:8080)
./gradlew :samples:composeApp:wasmJsBrowserDevelopmentRun
# iOS — see samples/iosApp/README.md for the xcodegen + Xcode workflowThe iOS host lives in samples/iosApp/ as a SwiftUI shell that embeds the
ComposeApp.framework produced by :samples:composeApp. The Compose UI itself is the
same Kotlin code that runs on Android, Desktop, and Web.
Apps that need any of the above should reach for CameraX / dedicated libraries directly.
A unified media-picker for Compose Multiplatform. One commonMain call site, every
target. No expect/actual leaks into your code, no custom camera UI, no CAMERA
permission to declare.
| Target | Pick image / video / file | System camera |
|---|---|---|
| Android | ✅ Photo Picker / SAF | ✅ ACTION_IMAGE_CAPTURE / ACTION_VIDEO_CAPTURE
|
| iOS | ✅ PHPicker / Document Picker | ✅ UIImagePickerController(.camera)
|
| Desktop (JVM) | ✅ FileDialog
|
Unsupported (no system camera) |
| Web (wasmJs) | ✅ <input type="file">
|
✅ on mobile-web via capture attribute |
// build.gradle.kts
dependencies {
implementation("io.github.aashutosh-rana:compose-media-picker:0.2.0")
}Repo: https://github.com/aashutosh-rana/compose-media-picker.
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val picker = rememberMediaPicker()
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
when (val r = picker.captureImage()) {
is MediaPickerResult.Success -> handle(r.data)
else -> Unit
}
}
}) { Text("Take photo") }
}
}
}That's it — no initialization call, no platform-specific imports beyond the call site.
rememberMediaPicker() auto-discovers the host: ComponentActivity on Android,
foreground UIViewController on iOS, the active Compose Window on Desktop, the browser
on Web. iOS / Desktop / Web call sites are identical to the snippet above.
The library declares its own FileProvider under ${applicationId}.mediakit.fileprovider
via manifest merging — you don't need to add anything to your AndroidManifest.xml.
You also don't need the CAMERA permission, because the system camera app owns it.
Delete any initializeMediaPicker(...) calls and their *PlatformContext imports.
That's the entire change — see CHANGELOG.md for the
full migration snippet.
Every call returns a MediaPickerResult<T>:
sealed interface MediaPickerResult<out T> {
data class Success<T>(val data: T) : MediaPickerResult<T>
data object Cancelled : MediaPickerResult<Nothing>
data class PermissionDenied(val permission: String, val permanentlyDenied: Boolean) : MediaPickerResult<Nothing>
data object Unsupported : MediaPickerResult<Nothing>
data class Error(val throwable: Throwable) : MediaPickerResult<Nothing>
}Unsupported lets you feature-detect cleanly:
when (val r = picker.captureImage()) {
is Success -> show(r.data)
Unsupported -> showMessage("No camera available")
Cancelled -> Unit
is Error -> showError(r.throwable)
is PermissionDenied -> showPermissionDialog()
}MediaFile is a lightweight reference — bytes are not loaded until you ask:
val file: MediaFile = (result as Success).data
file.source().use { source -> // streaming, off-main, cancellable
// …
}
scope.launch {
file.readProgress().collect { p -> updateBar(p) }
}A single Compose Multiplatform sample app — :samples:composeApp — runs on all four
targets from one shared commonMain Compose UI:
# Android (with emulator running)
./gradlew :samples:composeApp:installDebug
adb shell am start -n io.github.aashutosh.mediapicker.sample/.MainActivity
# Desktop
./gradlew :samples:composeApp:run
# Web (browser dev server on http://localhost:8080)
./gradlew :samples:composeApp:wasmJsBrowserDevelopmentRun
# iOS — see samples/iosApp/README.md for the xcodegen + Xcode workflowThe iOS host lives in samples/iosApp/ as a SwiftUI shell that embeds the
ComposeApp.framework produced by :samples:composeApp. The Compose UI itself is the
same Kotlin code that runs on Android, Desktop, and Web.
Apps that need any of the above should reach for CameraX / dedicated libraries directly.