File Picker¶
Installation¶
Quick Start¶
| Android | iOS |
|---|---|
![]() | ![]() |
| Desktop | Web |
|---|---|
![]() | ![]() |
val scope = rememberCoroutineScope()
val pickerLauncher = rememberFilePickerLauncher(
type = FilePickerFileType.Image,
selectionMode = FilePickerSelectionMode.Single,
onResult = { files ->
scope.launch {
files.firstOrNull()?.let { file ->
// Do something with the selected file
// You can get the ByteArray of the file
file.readByteArray()
}
}
}
)
Button(
onClick = {
pickerLauncher.launch()
},
modifier = Modifier.padding(16.dp)
) {
Text("Open File Picker")
}
Settings¶
Use FilePickerSettings to customize the dialog behavior:
val pickerLauncher = rememberFilePickerLauncher(
type = FilePickerFileType.Image,
settings = FilePickerSettings(
title = "Select an image",
initialDirectory = "/home/user/Pictures",
),
onResult = { files -> /* ... */ }
)
Or use rememberFilePickerSettings inside a @Composable to automatically recompose when settings change:
val settings = rememberFilePickerSettings(
title = dialogTitle,
initialDirectory = selectedDirectory,
)
val pickerLauncher = rememberFilePickerLauncher(
type = FilePickerFileType.Image,
settings = settings,
onResult = { files -> /* ... */ }
)
Common properties available on all platforms:
title- Dialog window titleinitialDirectory- Directory to open the dialog inimageRepresentationMode- Controls how iOS returns image assets (see iOS Image Representation)
Desktop adds an additional property:
parentWindow- TheComposeWindowto attach the dialog to (see Desktop Setup)
iOS Image Representation¶
By default, the iOS photo picker transcodes images to a compatible format (e.g. HEIC → JPEG) so they can be displayed with Compose/Skia. If you need the original format (HEIC, RAW, etc.), set imageRepresentationMode to Current:
ImageRepresentationMode.Compatible(default) - Transcodes to JPEG. Recommended for displaying images with Compose.ImageRepresentationMode.Current- Returns original format. Use when you handle decoding yourself or need original quality.
This setting only affects iOS. It is ignored on other platforms.
File Types¶
FilePickerFileType controls which files the user can select:
FilePickerFileType.Image- Images onlyFilePickerFileType.Video- Videos onlyFilePickerFileType.ImageVideo- Images and videosFilePickerFileType.Audio- Audio files onlyFilePickerFileType.Document- Documents onlyFilePickerFileType.Text- Text files onlyFilePickerFileType.Pdf- PDF files onlyFilePickerFileType.All- All file typesFilePickerFileType.Folder- Folders only
Filter by MIME type:
Filter by extension:
Selection Modes¶
FilePickerSelectionMode.Single- Pick a single fileFilePickerSelectionMode.Multiple- Pick multiple files (unlimited)FilePickerSelectionMode.Multiple(maxItems = 5)- Pick up to 5 files
Limiting Selection Count¶
You can limit the number of files the user can select by passing maxItems to Multiple:
val pickerLauncher = rememberFilePickerLauncher(
type = FilePickerFileType.Image,
selectionMode = FilePickerSelectionMode.Multiple(maxItems = 5),
onResult = { files ->
// files.size will be at most 5
}
)
On Android (visual media picker) and iOS (PHPicker), the limit is enforced natively by the picker UI — the user cannot select more than maxItems files. On all other platforms and picker types, the result list is truncated to maxItems after selection.
Passing null (the default) allows unlimited selection. FilePickerSelectionMode.Multiple without parentheses is equivalent to Multiple(maxItems = null).
Desktop Setup¶
macOS Dark Theme¶
The file dialog follows the application's theme. To enable dark mode support on macOS, add this JVM argument to your Gradle configuration:
compose.desktop {
application {
nativeDistributions {
macOS {
jvmArgs(
"-Dapple.awt.application.appearance=system",
)
}
}
}
}
Make sure to use the Gradle
runtask. Other run configurations (e.g.hotRun,jvmRun) may ignore these settings.
Parent Window¶
To attach the file dialog to the current window, you have three options:
Option 1: ProvideFilePickerParentWindow (recommended)¶
Wrap your content with ProvideFilePickerParentWindow to automatically provide the parent window to all file pickers in the tree:
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
ProvideFilePickerParentWindow {
App()
}
}
}
Option 2: FrameWindowScope extension¶
Use the FrameWindowScope.rememberFilePickerLauncher extension, which auto-captures the window:
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
val pickerLauncher = rememberFilePickerLauncher(
type = FilePickerFileType.Image,
onResult = { files -> /* ... */ }
)
}
}
Option 3: Explicit settings¶
Pass the window directly through FilePickerSettings:
val window = LocalFilePickerParentWindow.current
val pickerLauncher = rememberFilePickerLauncher(
type = FilePickerFileType.Image,
settings = FilePickerSettings(
parentWindow = window,
),
onResult = { files -> /* ... */ }
)
Extensions¶
All KmpFile extension functions work without passing a context parameter. On Android, the application context is captured automatically when using rememberFilePickerLauncher.
- Read the
ByteArrayof the file using thereadByteArrayextension function:
The
readByteArrayextension function is a suspending function, so you need to call it from a coroutine scope.It's not recommended to use
readByteArrayextension function on large files, as it reads the entire file into memory. For large files, it's recommended to use the platform-specific APIs to read the file. You can read more about accessing the platform-specific APIs below.
- Check if the file exists using the
existsextension function:
- Get the file name using the
getNameextension function:
- Get the file path using the
getPathextension function:
- Check if the file is a directory using the
isDirectoryextension function:
Overloads that accept a
PlatformContextparameter are still available for backward compatibility.
File Saver¶
rememberFileSaverLauncher lets the user save a file to a location of their choice. Available on all platforms.
@OptIn(ExperimentalCalfApi::class)
@Composable
fun SaveFileExample() {
val saverLauncher = rememberFileSaverLauncher(
onResult = { file ->
// file is the saved KmpFile, or null if cancelled
// On web, onResult is not called (downloads are fire-and-forget)
}
)
Button(onClick = {
saverLauncher.launch(
bytes = myByteArray,
baseName = "document",
extension = "pdf",
)
}) {
Text("Save File")
}
}
Saving from a KmpFile¶
If you already have a KmpFile (e.g., from a file picker), you can pass it directly instead of loading bytes into memory:
This avoids loading the entire file content into memory. On each platform, the file is copied or streamed directly to the destination chosen by the user.
Platform behavior:
| Platform | Mechanism |
|---|---|
| Android | System document creation dialog (CreateDocument) |
| iOS | Export dialog (UIDocumentPickerViewController) |
| Desktop | Native save dialog via rfd |
| Web | Browser download — onResult is not called since downloads are fire-and-forget |
rememberFileSaverLauncheris annotated with@ExperimentalCalfApi.
Platform-specific APIs¶
KmpFile is a wrapper around platform-specific APIs, you can access the native APIs for each platform using the following properties:
Android¶
iOS¶
// Persistent copy — always accessible, use for reading content
val nsUrl: NSURL = kmpFile.url
// Original picker URL — may expire due to iOS security scoping, use for metadata only
val originalNsUrl: NSURL = kmpFile.originalUrl
Note: On iOS, file picker URLs are security-scoped and may become inaccessible shortly after the picker callback returns.
urlpoints to a persistent copy in the app's temp directory that remains readable.originalUrlholds the original picker URL — use it only for metadata (e.g., the original file name or path viagetName()/getPath()).Migration from 0.10.x:
KmpFile.urlon iOS previously referred to the original picker URL. It now points to the persistent copy. If you were usingurlfor the original file path, switch tooriginalUrl.
Desktop¶
Web¶
Coil Extensions¶
In case you're using Coil in your project, Calf has a dedicated package that includes utilities to ease the integration between both libraries.
You can use it by adding the following dependency to your module build.gradle.kts file:
Currently, this package contains a KmpFileFetcher that you can use to let Coil know how to load a KmpFile by adding it to Coil's ImageLoader:
For more info regarding how to extend the Image Pipeline in Coil, you can read here.



