# Use FileKit with AI assistants Source: https://filekit.mintlify.app/ai-assistants Connect Claude, ChatGPT, Cursor, VS Code, and other AI tools to the FileKit documentation AI coding assistants often learn older APIs from their training data. Point your assistant at the current FileKit docs so its code matches the version you use. ## MCP server The FileKit docs run a hosted [Model Context Protocol](https://modelcontextprotocol.io) server. Once connected, your assistant can search and read the docs while it writes code. ```text theme={null} https://filekit.mintlify.app/mcp ``` **Claude Code** ```bash theme={null} claude mcp add --transport http filekit https://filekit.mintlify.app/mcp ``` **Cursor** Add to `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json`: ```json theme={null} { "mcpServers": { "filekit": { "url": "https://filekit.mintlify.app/mcp" } } } ``` **VS Code** Add to `.vscode/mcp.json`: ```json theme={null} { "servers": { "filekit": { "type": "http", "url": "https://filekit.mintlify.app/mcp" } } } ``` **Other clients** Any MCP client that supports remote servers over HTTP can use `https://filekit.mintlify.app/mcp`. You can also use the page menu at the top of any docs page to copy the MCP URL or install the server in Cursor or VS Code. ## llms.txt The docs are published in the [llms.txt](https://llmstxt.org) format, which AI tools can read directly: | File | Content | | - | - | | [`/llms.txt`](https://filekit.mintlify.app/llms.txt) | An index of every page with a one-line summary | | [`/llms-full.txt`](https://filekit.mintlify.app/llms-full.txt) | The full documentation in a single Markdown file | Paste `llms-full.txt` into a chat for full context, or give your agent the `llms.txt` URL so it fetches only the pages it needs. ## Markdown pages Every page is also available as Markdown. Add `.md` to the URL, for example [`/quickstart.md`](https://filekit.mintlify.app/quickstart.md). Use the menu at the top of each page to: * Copy the page as Markdown. * View the page as Markdown. * Open the page in ChatGPT, Claude, or Perplexity with the page as context. ## Project instructions If your project uses an `AGENTS.md`, `CLAUDE.md`, `.cursor/rules`, or Copilot instructions file, add a short FileKit section. It prevents the most common mistakes: ```markdown AGENTS.md theme={null} ## FileKit (Kotlin Multiplatform file APIs) - Docs: https://filekit.mintlify.app (MCP server: https://filekit.mintlify.app/mcp, full text: https://filekit.mintlify.app/llms-full.txt). - Modules: `filekit-core` (PlatformFile, read/write, file system), `filekit-dialogs` (suspend pickers/saver), `filekit-dialogs-compose` (`remember…Launcher` for Compose), `filekit-coil` (Coil 3 support). Group `io.github.vinceglb`. - Most APIs are extension functions: import them from `io.github.vinceglb.filekit` (core), `io.github.vinceglb.filekit.dialogs`, `io.github.vinceglb.filekit.dialogs.compose`, or `io.github.vinceglb.filekit.coil`. - Use `PlatformFile`, not `java.io.File` or `NSURL`, in shared code. Reading and writing (`readBytes()`, `readString()`, `write()`, `writeString()`) are `suspend` functions. - Pickers return `null` when the user cancels, except `SingleWithState`/`MultipleWithState`, which emit `FileKitPickerState.Cancelled`. Prefer Compose launcher overloads that take `onError`. - Platform setup: `FileKit.init(appId = ...)` on JVM desktop, Linux native, and Windows native before using app directories. On Android, `FileKit.init(activity)` before calling suspend dialog APIs (Compose launchers do it for you). - Some APIs are platform-specific (for example, the file saver is not available on JS/Wasm; use `FileKit.download`). Check https://filekit.mintlify.app/platform-support before using an API in `commonMain`. ``` # DataStore Source: https://filekit.mintlify.app/cookbook/datastore Create a Preferences DataStore from common code with FileKit.databasesDir [See the platform support matrix](/platform-support#filekit-core) The [official DataStore setup](https://developer.android.com/kotlin/multiplatform/datastore) needs an `expect`/`actual` function to find a file path on each platform. `FileKit.databasesDir` gives you that path from shared code, so the whole setup fits in one file. ## Dependencies ```kotlin build.gradle.kts theme={null} commonMain.dependencies { implementation("io.github.vinceglb:filekit-core:") implementation("androidx.datastore:datastore-preferences:") } ``` `datastore-preferences` 1.3.0 publishes no Windows native or web targets, and `databasesDir` doesn't exist on the web. If your app has those targets, add the dependency and the code below to a source set shared only by the platforms above instead of `commonMain`. ## Create the DataStore ```kotlin theme={null} import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.databasesDir import io.github.vinceglb.filekit.path import io.github.vinceglb.filekit.resolve import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import okio.Path.Companion.toPath object AppPreferences { private val dataStore: DataStore = PreferenceDataStoreFactory.createWithPath( produceFile = { FileKit.databasesDir.resolve("app.preferences_pb").path.toPath() }, ) private val darkModeKey = booleanPreferencesKey("dark_mode") val darkMode: Flow = dataStore.data.map { it[darkModeKey] ?: false } suspend fun setDarkMode(enabled: Boolean) { dataStore.edit { it[darkModeKey] = enabled } } } ``` DataStore expects an okio `Path`, so the last step converts FileKit's path string with okio's `toPath()`. DataStore creates the parent folder when it first writes, so `databasesDir` doesn't need to exist yet on desktop. Create only one DataStore per file. A singleton `object`, or a single instance from your dependency injection, keeps that guarantee. On JVM and Linux native, call `FileKit.init(appId)` before the first read. See [Core setup](/core/setup). ## Where the file goes | Platform | `databasesDir` | | - | - | | Android | The app's `databases/` directory | | iOS, watchOS | `Documents/databases/` | | macOS | `~/Library/Application Support//databases/` | | JVM, Linux native | `/databases/`, for example `~/.local/share//databases/` on Linux | See [App directories](/core/file-utils#directory-locations) for every platform. # Test resources Source: https://filekit.mintlify.app/cookbook/test-resources Read test fixture files from shared Kotlin Multiplatform tests with FileKit.projectDir [See the platform support matrix](/platform-support#filekit-core) Kotlin Multiplatform has no common API to read files from a test resources folder. `FileKit.projectDir` points to your module's directory while tests run, so you can build paths to fixtures from shared test code. ## Usage Put the fixtures in a folder of your module, for example `src/commonTest/resources/hello.txt`, then read them: ```kotlin jvmTest theme={null} import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.div import io.github.vinceglb.filekit.exists import io.github.vinceglb.filekit.projectDir import io.github.vinceglb.filekit.readString import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class ResourcesTest { private val resources = FileKit.projectDir / "src/commonTest/resources" @Test fun readHello() = runTest { val hello = resources / "hello.txt" assertTrue(hello.exists()) assertEquals("Hello, World!", hello.readString()) } } ``` `projectDir` is declared in `nonWebMain`, so it doesn't compile in `commonTest` when your module has JS or Wasm targets. Put the test in a test source set without web targets, such as `jvmTest`, `iosTest`, or a shared `nonWebTest` you declare. The fixtures folder can stay anywhere in the module. ## How each platform finds the module | Platform | `projectDir` | Works in | | - | - | - | | JVM, macOS, Linux native, Windows native | `PlatformFile(".")`, the working directory | Gradle test tasks, which run from the module directory | | Android | `PlatformFile(".")` | Local unit tests that run on your computer. On a device or emulator, `.` is not your project. | | iOS, watchOS | Four folders above the test bundle | Simulator tests, where that is the module directory. Not on a real device. | `projectDir` is for tests and tooling only. Don't use it in production code. ## Related * [App directories](/core/file-utils) for runtime locations. * [Reading files](/core/read-file) for the full read API. # Zip archives Source: https://filekit.mintlify.app/cookbook/zip-archives Create and extract zip archives with FileKit and kmp-zip [See the platform support matrix](/platform-support#filekit-core) FileKit doesn't create or extract archives itself. [kmp-zip](https://github.com/henrik242/kmp-zip) is a Kotlin Multiplatform zip library that works well with FileKit, because both use [kotlinx-io](https://github.com/Kotlin/kotlinx-io) streams. This recipe works with any `PlatformFile`, including files and folders returned by FileKit dialogs, such as Android `content://` documents. ## Dependencies Add kmp-zip and its kotlinx-io adapters alongside FileKit Core: ```kotlin build.gradle.kts theme={null} commonMain.dependencies { // FileKit Core implementation("io.github.vinceglb:filekit-core:") // kmp-zip implementation("no.synth:kmp-zip:") implementation("no.synth:kmp-zip-kotlinx:") } ``` kmp-zip 1.0.0 publishes no watchOS targets. On the web, `source()` and `sink()` don't exist, so build the archive in memory instead: see [Zip on the web](#zip-on-the-web). ## Create an archive The `zip` function below writes files and folders into an archive. Each folder keeps its name as the top-level entry, so extracting the archive recreates the folder: ```kotlin nonWebMain theme={null} import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.isDirectory import io.github.vinceglb.filekit.list import io.github.vinceglb.filekit.name import io.github.vinceglb.filekit.sink import io.github.vinceglb.filekit.source import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.withContext import kotlinx.io.buffered import no.synth.kmpzip.kotlinx.asOutputStream import no.synth.kmpzip.kotlinx.asSink import no.synth.kmpzip.zip.ZipEntry import no.synth.kmpzip.zip.ZipOutputStream suspend fun zip( sources: List, archive: PlatformFile, ): Unit = withContext(Dispatchers.IO) { val names = sources.map { it.name } require(names.size == names.toSet().size) { "Sources must have distinct names: $names" } ZipOutputStream(archive.sink().buffered().asOutputStream()).use { zip -> sources.forEach { source -> zip.putFile(source, entryName = source.name) } } } private fun ZipOutputStream.putFile(file: PlatformFile, entryName: String) { if (file.isDirectory()) { putNextEntry(ZipEntry("$entryName/")) closeEntry() file.list().forEach { child -> putFile(child, entryName = "$entryName/${child.name}") } } else { putNextEntry(ZipEntry(entryName)) file.source().buffered().use { source -> source.transferTo(asSink()) } closeEntry() } } ``` Files are streamed into the archive, so large files are never fully loaded into memory. Sources must have distinct names, otherwise their entries would overwrite each other on extraction. > [!NOTE] > > A few things to keep in mind: > > * Save the archive outside the files and folders you are zipping. Otherwise, `zip` can read the archive while writing it, or overwrite a source file before reading it. > * Symbolic links are followed. A link to a parent folder makes `zip` loop through the same folders until it fails. > * kmp-zip doesn't support ZIP64. Each file and the archive must stay under 4 GB, with at most 65,535 entries. ## Extract an archive The `unzip` function extracts every entry into a destination folder: ```kotlin nonWebMain theme={null} import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.createDirectories import io.github.vinceglb.filekit.div import io.github.vinceglb.filekit.sink import io.github.vinceglb.filekit.source import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.withContext import kotlinx.io.buffered import no.synth.kmpzip.kotlinx.asInputStream import no.synth.kmpzip.kotlinx.asSource import no.synth.kmpzip.zip.ZipInputStream suspend fun unzip( archive: PlatformFile, destination: PlatformFile, ): Unit = withContext(Dispatchers.IO) { ZipInputStream(archive.source().buffered().asInputStream()).use { zip -> while (true) { val entry = zip.nextEntry ?: break val segments = entry.name.safePathSegments() val parent = segments.dropLast(1).fold(destination) { dir, segment -> dir / segment } val target = parent / segments.last() if (entry.isDirectory) { target.createDirectories() } else { parent.createDirectories() target.sink().buffered().use { sink -> sink.transferFrom(zip.asSource()) } } } } } private fun String.safePathSegments(): List { val path = replace('\\', '/') val segments = path.split('/').filter { it.isNotEmpty() && it != "." } require(!path.startsWith('/') && segments.isNotEmpty() && segments.none { it == ".." || ':' in it }) { "Unsafe zip entry: $this" } return segments } ``` > [!WARNING] > > Always validate entry names before extracting an archive you didn't create. A malicious archive can contain names such as `../../file.txt` to write files outside the destination folder ([Zip Slip](https://security.snyk.io/research/zip-slip-vulnerability)). This is what `safePathSegments()` prevents. Extract these archives into a new, empty folder: if the destination already contains a symbolic link, an entry could still be written through it. > > Untrusted archives can also be decompression bombs: a few kilobytes that extract to gigabytes. kmp-zip doesn't limit the extracted size, so count the bytes you write and stop above a maximum. See the [kmp-zip threat model](https://github.com/henrik242/kmp-zip#threat-model) for an example. ## Zip files picked by the user Combined with [FileKit Dialogs](/dialogs/setup), you can let the user pick files and choose where to save the archive: ```kotlin nonWebMain theme={null} val files = FileKit.openFilePicker(mode = FileKitMode.Multiple()) ?: return val archive = FileKit.openFileSaver(suggestedName = "archive", defaultExtension = "zip") ?: return zip(files, archive) ``` To zip a whole folder, use `FileKit.openDirectoryPicker()` and pass the folder to `zip`. ## Zip on the web [See the platform support matrix](/platform-support#filekit-core) Build the archive in memory, then save it with [`FileKit.download`](/core/web#download-files): ```kotlin webMain theme={null} import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.download import io.github.vinceglb.filekit.name import io.github.vinceglb.filekit.readBytes import no.synth.kmpzip.io.ByteArrayOutputStream import no.synth.kmpzip.zip.ZipEntry import no.synth.kmpzip.zip.ZipOutputStream suspend fun zipToBytes(files: List): ByteArray { val bytes = ByteArrayOutputStream() ZipOutputStream(bytes).use { zip -> files.forEach { file -> zip.putNextEntry(ZipEntry(file.name)) zip.write(file.readBytes()) zip.closeEntry() } } return bytes.toByteArray() } FileKit.download(zipToBytes(files), "archive.zip") ``` The whole archive is kept in memory, so this approach is best suited for small files. See the [kmp-zip documentation](https://github.com/henrik242/kmp-zip#js-and-wasmjs-notes) for more details about web support. ## Go further kmp-zip also supports password-protected archives (AES and legacy ZipCrypto), GZIP, and reading a single entry without extracting the whole archive. See the [kmp-zip documentation](https://github.com/henrik242/kmp-zip). # Bookmark data Source: https://filekit.mintlify.app/core/bookmark-data Keep access to user-picked files and folders across app restarts [See the platform support matrix](/platform-support#filekit-core) A file the user picked can become unreachable after a restart: Android revokes `content://` access and sandboxed Apple apps lose their security scope. `bookmarkData()` turns a `PlatformFile` into bytes you can store, and `PlatformFile.fromBookmarkData(bytes)` gives the file back, with its access, on the next launch. ## Usage ```kotlin nonWebMain theme={null} // After the user picks a file val bytes: ByteArray = file.bookmarkData().bytes // Store the bytes: a file, DataStore, a database... // On the next launch val restored: PlatformFile = PlatformFile.fromBookmarkData(bytes) ``` Both functions are declared in `nonWebMain`. `bookmarkData()` is a suspend function. Bookmark bytes are platform-specific: don't share them between operating systems or apps. ## Restore a bookmark safely A bookmark breaks when the user deletes the file, revokes your app's access, or (on path-based platforms) moves it. Handle failure and ask the user to pick the file again. This helper stores one bookmark in your app's files directory: ```kotlin nonWebMain theme={null} // A getter, so FileKit.filesDir is only read after FileKit.init on desktop private val bookmarkFile get() = FileKit.filesDir / "last-file.bookmark" suspend fun saveLastFile(file: PlatformFile) { bookmarkFile.write(file.bookmarkData().bytes) } suspend fun loadLastFile(): PlatformFile? { if (!bookmarkFile.exists()) return null val bytes = bookmarkFile.readBytes() val resolution = runCatching { PlatformFile.resolveBookmarkData(bytes) }.getOrNull() if (resolution == null || !resolution.file.exists()) { bookmarkFile.delete() // gone or no longer accessible: ask the user again return null } if (resolution.shouldRefresh) { // Best effort: keep the old bookmark if the refresh fails try { saveLastFile(resolution.file) } catch (e: CancellationException) { throw e } catch (e: Exception) { // Ignore and retry on the next launch } } return resolution.file } ``` `resolveBookmarkData` returns a `BookmarkResolution` with the `file` and two flags: | Property | Meaning | | - | - | | `isStale` | The operating system resolved the bookmark but asks you to recreate it. | | `shouldRefresh` | FileKit recommends replacing the stored bytes. `true` when `isStale` is, and when the bytes come from an older FileKit format. | Refreshing is best effort: the old bytes may still work, and `bookmarkData()` can fail if the file disappeared or macOS already revoked access. Use `fromBookmarkData` when you only need the file. ## Platform notes | Platform | Stored as | Security-scoped | Survives a move | | - | - | - | - | | Android, regular path | The path | Not needed | No | | Android, `content://` | The URI, plus a persisted URI permission | Not needed (the permission persists) | Depends on the document provider | | iOS | Foundation bookmark | Yes | Yes | | watchOS | Foundation bookmark | No | Yes | | macOS, JVM on macOS | Native bookmark in a versioned FileKit wrapper | Yes when the app is sandboxed | Yes | | JVM on Linux and Windows, Linux native, Windows native | The path | No | No | On Android, `releaseBookmark()` releases the persisted URI permission. Android limits how many URI permissions an app can persist, so release the ones you no longer need. ## Set up the macOS App Sandbox FileKit reads your app's App Sandbox entitlement at runtime. When it is present, FileKit creates security-scoped bookmarks and reports a failure instead of falling back to a bookmark that can't restore access. **1. Enable the sandbox and user-selected file access** Add these entitlements to the signed app: ```xml entitlements.plist theme={null} com.apple.security.app-sandbox com.apple.security.files.user-selected.read-write com.apple.security.files.bookmarks.app-scope ``` **2. Test the packaged app** Entitlements only apply to a signed build. Pick a file, quit, relaunch, and restore the bookmark. ## Keep security-scoped access On iOS and in sandboxed macOS apps, a file outside your container is readable only while its security-scoped access is active. FileKit starts and stops access around its own operations, and keeps it active until returned sources and sinks are closed. Derive files from the restored `PlatformFile` with `/`, `resolve()`, or `list()` so they share its access: ```kotlin nonWebMain theme={null} val folder = PlatformFile.fromBookmarkData(savedFolderBookmark) source.atomicMove(folder / "backup.db") ``` A `PlatformFile` rebuilt from a path string or `toKotlinxIoPath()` loses that access, and so does one restored by `PlatformFileSerializer`, which stores only the path. Persist bookmark data instead. When you hand a file to another library, wrap the call in `withScopedAccess`. It calls `startAccessingSecurityScopedResource()` and `stopAccessingSecurityScopedResource()` for you, and is a no-op on platforms without security scopes: ```kotlin theme={null} file.withScopedAccess { scoped -> thirdPartyLibrary.open(scoped.path) } ``` Call `releaseBookmark()` once you are done with a restored bookmark and every file derived from it. Already-open sources and sinks still close cleanly, but new scoped operations then throw a `FileKitException`. > **Upgrading from older versions** > > * **macOS bookmark format:** new bookmarks on macOS (native and JVM) are wrapped in a versioned FileKit format. Unwrapped Kotlin/Native bookmarks and JVM path bytes written by earlier versions remain readable, and `resolveBookmarkData` reports `shouldRefresh = true` for them. > * **iOS security scope:** restored bookmarks now keep the security-scoped URL for the bookmarked item and its descendants, including after a device restart. Stored bookmarks get this behavior without a migration. > * **iOS `releaseBookmark()`:** it was a no-op in FileKit 0.15.0 and 0.16.0 and now releases access. Calling `startAccessingSecurityScopedResource()` on a released bookmark throws instead of returning `false`. > * **`fromBookmarkData`** is not deprecated. `resolveBookmarkData` was added for refresh metadata. ## Related * [Directory picker](/dialogs/directory-picker) to let users pick a folder worth bookmarking. * [App directories](/core/file-utils) for files your app owns, which need no bookmark. # File operations Source: https://filekit.mintlify.app/core/file-operations List, walk, copy, move, and delete files and directories with PlatformFile [See the platform support matrix](/platform-support#filekit-core) These functions manage files and directory trees on disk. On the web, `list()`, `walk()`, and `sizeRecursively()` work on folders from the directory picker, and the experimental [origin private file system](/core/web#store-files-in-the-origin-private-file-system) adds `listEntries()` and `delete()`. Call the other functions from `nonWebMain` or a platform source set. ## Usage ```kotlin nonWebMain theme={null} val reports = FileKit.filesDir / "reports" reports.createDirectories() val files: List = reports.list() pickedFile.copyTo(reports) (reports / "old.txt").delete(mustExist = false) ``` ## Options **maxDepth** `Int` (default: Int.MAX\_VALUE) `1` yields only the direct children. Must be positive. **mustCreate** `Boolean` (default: false) `true` throws if the directory already exists. **overwrite** `Boolean` (default: false) `true` replaces existing files. See [Copy a directory tree](#copy-a-directory-tree). **mustExist** `Boolean` (default: true) `true` throws if the file doesn't exist. **recursively** `Boolean` (default: false) `true` empties a directory before removing it. ## List a directory ```kotlin theme={null} val children: List = directory.list() ``` `list()` returns the direct children and throws on a regular file. It does blocking I/O: on every platform except the web, call it on `Dispatchers.IO`. ## Walk a directory tree `walk()` lazily yields every file and subdirectory below a directory, depth-first, without the directory itself: ```kotlin theme={null} val images = directory.walk() .filter { it.isRegularFile() && it.extension == "png" } .toList() val children = directory.walk(maxDepth = 1).toList() // like list() ``` On a regular file, `walk()` yields only that file. It is synchronous, like `list()`: iterate it on `Dispatchers.IO`. ## Get the size of a tree ```kotlin theme={null} val totalBytes: Long = directory.sizeRecursively() ``` `sizeRecursively()` adds up the size of every regular file in the tree, or returns the file's own size for a regular file. Files whose size is unknown count as `0`. It is a suspend function that switches to `Dispatchers.IO` on every platform except the web. Symbolic links are never followed by `walk()` or `sizeRecursively()`, so a link can't cause an infinite loop: `walk()` yields the link itself, and `sizeRecursively()` counts it as `0`. This also applies when you call them on a link. ## Create directories ```kotlin nonWebMain theme={null} val reports = FileKit.filesDir / "reports" / "2026" reports.createDirectories() (reports / "summary.txt").writeString("Q4") ``` `createDirectories()` creates the directory and any missing parent. It does nothing if the directory already exists, unless you pass `mustCreate = true`. ## Copy a file ```kotlin nonWebMain theme={null} source.copyTo(FileKit.filesDir / "backup.pdf") // to this file source.copyTo(FileKit.filesDir) // into this folder, as source.name ``` `copyTo()` streams the content, so it works when the source size is unknown. An existing destination file is replaced. It throws if the source and destination are the same file, or if the destination folder contains a directory with the source's name. ## Copy a directory tree `copyRecursively()` copies a file or a whole directory tree. Unlike `copyTo()`, the target becomes the new root, not a folder to copy into (as with Kotlin's `File.copyRecursively()`): ```kotlin nonWebMain theme={null} source.copyRecursively(target) // source/a.txt -> target/a.txt source.copyRecursively(target, overwrite = true) // merge, replacing existing files ``` | Target entry | `overwrite = false` | `overwrite = true` | | - | - | - | | Missing, or missing parent directories | Created | Created | | Existing directory | Merged | Merged | | Existing file | Throws | Replaced | | A file where the source has a directory, or the other way around | Throws | Throws | * The copy stops at the first error and leaves the target partially copied: there is no rollback. * Symbolic links inside the tree are skipped. Calling it on a link, or copying a directory into itself, throws. * It runs on `Dispatchers.IO`. > [!WARNING] > > With `overwrite = true`, FileKit can't tell that an existing target file is the source itself when it is reached through a hard link or a Windows junction. Copying then truncates the source file. ## Move a file ```kotlin nonWebMain theme={null} source.atomicMove(FileKit.filesDir / "archive" / "report.pdf") ``` `atomicMove()` renames the file in a single step and replaces an existing destination file. When the destination is an existing directory, the file moves into it under its own name. Moving across volumes can throw `UnsupportedOperationException`: use `copyTo()` then `delete()` instead. ## Delete a file or directory ```kotlin nonWebMain theme={null} file.delete() // throws if the file doesn't exist file.delete(mustExist = false) directory.delete(recursively = true) // also deletes the contents ``` Without `recursively = true`, deleting a non-empty directory throws. Symbolic links (including dangling links) and Windows directory junctions are removed as links: their targets are left untouched. ## Platform notes > **Android: content:// files and document trees** > > * `atomicMove()` falls back to copy then delete when the source or the destination is a `content://` file, and doesn't support directories. > * `delete()` lets the document provider remove the document, with its contents, whatever `recursively` says. > * `list()`, `walk()`, and `sizeRecursively()` cost provider queries for each subdirectory, so large trees are slow. Files whose provider doesn't report a size count as `0`. > * `copyRecursively()` accepts document trees as a source. As a target, it can create subdirectories only with providers whose document IDs are paths, such as device storage: with other providers, it throws when it needs a new subdirectory. An entry named `.` or `..`, containing `/`, or sharing its name with a sibling makes the copy throw before anything is written. Copying a tree into itself is detected only for providers whose document IDs are paths. > **iOS and macOS: security-scoped folders** > > Keep security-scoped access to a picked folder active while you iterate `walk()`, for example inside `folder.withScopedAccess { }`. `copyRecursively()` holds access to the source and the target for the whole copy. > **Windows: reparse points** > > Every reparse point counts as a link, including directory junctions and OneDrive Files On-Demand folders. `walk()` lists such a folder without its contents, `sizeRecursively()` doesn't count it, and `copyRecursively()` skips it. A `copyRecursively()` target that reaches the source through a junction isn't detected as being inside it: the copy doesn't throw, and it still ends without copying its own output. > **Web: picked folders only** > > `list()`, `walk()`, and `sizeRecursively()` work on folders returned by the directory picker, whose tree is already in memory, and run on the caller's dispatcher. They throw on origin private file system directories: use `listEntries()` instead. `copyTo()`, `copyRecursively()`, `createDirectories()`, and `atomicMove()` are not available. `delete()` exists only for origin private file system entries. See [Files on the web](/core/web). ## Related * [PlatformFile](/core/platform-file): paths, properties, and the `/` operator * [Writing files](/core/write-file) * [Directory picker](/dialogs/directory-picker): let the user pick a folder # App directories Source: https://filekit.mintlify.app/core/file-utils Find the app files, cache, and databases directories, plus Downloads and other user folders [See the platform support matrix](/platform-support#filekit-core) `FileKit.filesDir`, `FileKit.cacheDir`, and `FileKit.databasesDir` return the app's private directories as `PlatformFile`s, mapped to each platform's conventions. They are declared in `nonWebMain`, so call them from any source set without JS or Wasm. ## Usage ```kotlin nonWebMain theme={null} val config = FileKit.filesDir / "config.json" config.writeString("""{"version": 1}""") val thumbnail = FileKit.cacheDir / "thumb.jpg" val database = FileKit.databasesDir / "app.db" ``` On JVM, Linux native, and Windows native, call `FileKit.init(appId)` before reading any of these directories. `FileKit.init(appId, filesDir, cacheDir)` overrides the locations, and `FileKit.init(filesDir, cacheDir)` sets both without an app ID. See [Core setup](/core/setup). ## Directory locations | Platform | `filesDir` | `cacheDir` | `databasesDir` | | - | - | - | - | | Android | `Context.filesDir` | `Context.cacheDir` | `databases/` in the app data directory | | iOS, watchOS | `Documents/` (included in backups) | `Library/Caches/` (not backed up, may be purged) | `Documents/databases/` | | macOS | `~/Library/Application Support//` | `~/Library/Caches/` (in the container when sandboxed) | `/databases/` | | JVM on macOS | `~/Library/Application Support//` | `~/Library/Caches//` | `/databases/` | | JVM on Linux, Linux native | `$XDG_DATA_HOME//`, default `~/.local/share//` | `$XDG_CACHE_HOME//`, default `~/.cache//` | `/databases/` | | JVM on Windows, Windows native | `%APPDATA%\\` | `%LOCALAPPDATA%\\Cache\` | `\databases\` | `filesDir` and `cacheDir` always exist when you read them: FileKit creates them on desktop targets if needed. Android and Apple platforms also create `databasesDir` for you. > [!NOTE] > > On JVM, Linux native, and Windows native, `databasesDir` is not created automatically. Call `FileKit.databasesDir.createDirectories()` before writing into it. On macOS native, `cacheDir` is the shared user Caches folder, not a per-app subfolder. Add your own subfolder, such as `FileKit.cacheDir / "com.example.myapp"`, when your app isn't sandboxed. ## Use the temporary directory on Apple platforms `FileKit.tempDir` returns `NSFileManager.temporaryDirectory`. It is declared for iOS, macOS, and watchOS only, so call it from an Apple source set such as `appleMain` or `iosMain`: ```kotlin iosMain theme={null} val scratch = FileKit.tempDir / "upload.tmp" ``` The system can delete this directory's contents when your app is not running. ## Get the project directory in tests `FileKit.projectDir` exists on every non-web target, but it only makes sense during development and tests: | Platform | Returns | | - | - | | Android, JVM, macOS, Linux native, Windows native | `PlatformFile(".")`: the process working directory. Gradle runs JVM and native host tests from the module directory. On an Android device, `.` is not your project. | | iOS, watchOS | The folder four levels above the app bundle. For a test binary on the simulator, that is the module directory. On a real device, it doesn't point to your project. | See [Test resources](/cookbook/test-resources) to read fixture files with it. Don't use `projectDir` in production code. ## Open user folders on desktop On JVM, macOS, Linux native, and Windows native, FileKit resolves the user's Downloads, Pictures, Videos, Music, and Documents folders: ```kotlin jvmMain theme={null} val downloads: PlatformFile = FileKit.downloadsDir val pictures = FileKit.userDirectory(FileKitUserDirectory.Pictures) // Returns null instead of throwing when the folder can't be resolved val music: PlatformFile? = FileKit.userDirectoryOrNull(FileKitUserDirectory.Music) ``` The shortcuts are `downloadsDir`, `picturesDir`, `videosDir`, `musicDir`, and `documentsDir`. They throw a `FileKitException` when the folder can't be resolved. These APIs are not declared on Android, iOS, watchOS, or the web. | Platform | Resolution | | - | - | | macOS native | Foundation search paths (`Videos` is `~/Movies`). | | JVM on macOS | `$HOME/`, with `Videos` mapped to `Movies`. | | JVM on Linux, Linux native | `XDG__DIR` environment variables, then `~/.config/user-dirs.dirs`, then `$HOME/`. | | JVM on Windows, Windows native | Windows known folders (`SHGetKnownFolderPath`), then `%USERPROFILE%\`. | On JVM, Linux native, and Windows native, a missing folder is created when you read it. ## Download a file on the web Browsers have no app directories. To save bytes to the user's Downloads folder, use `FileKit.download`. See [Files on the web](/core/web#download-files). ## Related * [Core setup](/core/setup) to set the application ID or custom directories. * [Writing files](/core/write-file) into these directories. * [Bookmark data](/core/bookmark-data) to keep access to files outside them. # Image and video utilities Source: https://filekit.mintlify.app/core/image-and-video-utils Compress and resize images, save images and videos to the gallery, and convert files to ImageBitmap [See the platform support matrix](/platform-support#filekit-core) `FileKit.compressImage` re-encodes an image as JPEG or PNG, optionally downscaled, and returns the new bytes. It is declared in `nonWebMain` and throws a `FileKitException` on Linux native, Windows native, and watchOS. ## Usage ```kotlin nonWebMain theme={null} val compressed: ByteArray = FileKit.compressImage( file = photo, quality = 80, maxWidth = 1024, maxHeight = 1024, ) (FileKit.cacheDir / "photo-small.jpg").write(compressed) ``` Pass `bytes = imageBytes` instead of `file` when the image is already in memory. ## Options **file / bytes** `PlatformFile | ByteArray` The source image. **imageFormat** `ImageFormat` (default: JPEG) Output format: `ImageFormat.JPEG` or `ImageFormat.PNG`. **quality** `Int` (default: 80) `0` to `100`. Applies to JPEG only. **maxWidth / maxHeight** `Int?` (default: null) Downscales to fit, keeping the aspect ratio. Smaller images are never upscaled. On Android, the EXIF orientation is applied before resizing, so the output is upright. ## Save to the gallery [See the platform support matrix](/platform-support#filekit-core) `saveImageToGallery` and `saveVideoToGallery` return a `Result` instead of throwing: ```kotlin nonWebMain theme={null} FileKit.saveImageToGallery(file = photo) .onFailure { error -> showError(error.message) } FileKit.saveVideoToGallery(file = video, filename = "holiday.mp4") ``` `filename` defaults to the file's name. `saveImageToGallery(bytes, filename)` saves an image from memory. | Platform | Destination | | - | - | | Android | MediaStore, in `DCIM/` on Android 10 and later. | | iOS | The Photos library. `filename` is ignored. | | macOS, JVM, Linux native, Windows native | The user's Pictures folder for images and Videos (Movies on macOS) folder for videos. | | watchOS | Returns a failed `Result`. | | Web | Not declared. | > [!WARNING] > > On iOS, add `NSPhotoLibraryAddUsageDescription` to your `Info.plist` before saving to the gallery. Without it, the system terminates your app on the first save. > [!NOTE] > > On Android 9 and lower, writing to MediaStore requires the `WRITE_EXTERNAL_STORAGE` permission. FileKit doesn't declare it, so add it to your manifest with `android:maxSdkVersion="28"` if you support those versions. ## Convert to and from ImageBitmap in Compose [See the platform support matrix](/platform-support#compose-and-coil) `PlatformFile.toImageBitmap()` and `ImageBitmap.encodeToByteArray()` come from `filekit-dialogs-compose`, not `filekit-core`. Add it to `commonMain` (see [Installation](/installation) for the current version): ```kotlin build.gradle.kts theme={null} implementation("io.github.vinceglb:filekit-dialogs-compose:") ``` ```kotlin theme={null} import io.github.vinceglb.filekit.dialogs.compose.util.encodeToByteArray import io.github.vinceglb.filekit.dialogs.compose.util.toImageBitmap val bitmap: ImageBitmap = file.toImageBitmap() val png: ByteArray = bitmap.encodeToByteArray(format = ImageFormat.PNG) ``` Both are suspend functions. `encodeToByteArray` defaults to `ImageFormat.JPEG` with `quality = 100`. To display a file in an `Image`, the [Coil](/integrations/coil) loads it for you, with caching. ## Related * [Gallery picker](/dialogs/gallery-picker) to let users pick photos and videos. * [Camera picker](/dialogs/camera-picker) to take a photo. * [Coil](/integrations/coil) to display a `PlatformFile` image. # PlatformFile Source: https://filekit.mintlify.app/core/platform-file Create, inspect, and serialize files and directories with one type on every platform [See the platform support matrix](/platform-support#filekit-core) `PlatformFile` is the file or directory type that every FileKit API takes and returns. It wraps the native representation of each platform (`java.io.File`, Android `Uri`, `NSURL`, a browser `File`) behind one set of properties and functions. ## Create a PlatformFile On every platform except the web, create a file from a path, or from a parent directory and a child path: ```kotlin nonWebMain theme={null} val file = PlatformFile("/path/to/document.pdf") val fromPath = PlatformFile(Path("/path/to/document.pdf")) // kotlinx.io.files.Path val notes = PlatformFile(FileKit.filesDir, "notes.txt") val today = FileKit.filesDir / "notes" / "today.txt" val same = FileKit.filesDir.resolve("notes/today.txt") ``` The file doesn't have to exist yet. Each platform also accepts its native type: ```kotlin androidMain theme={null} val fromFile = PlatformFile(File("/path/to/document.pdf")) val fromUri = PlatformFile(uri) // content:// or file:// Uri ``` ```kotlin jvmMain theme={null} val file = PlatformFile(File("/path/to/document.pdf")) val javaFile: File = file.file ``` ```kotlin appleMain theme={null} val file = PlatformFile(NSURL.fileURLWithPath("/path/to/document.pdf")) val url: NSURL = file.nsUrl ``` ```kotlin webMain theme={null} // Browsers have no paths: files come from a picker or from the origin private file system val picked: PlatformFile? = FileKit.openFilePicker() ``` On Android, `PlatformFile(path)` also parses `content://` and `file://` strings into a `Uri`. ## Properties | Property | Type | Notes | | - | - | - | | `name` | `String` | File name with extension, such as `document.pdf`. | | `extension` | `String` | `pdf`. Empty when the name has none. | | `nameWithoutExtension` | `String` | `document`. | | `path` | `String` | The path you created the file with. The `Uri` string for Android `content://` files, the relative path on the web. | | `size()` | `Long` | Size in bytes. `-1` when unknown: the file doesn't exist, or an Android provider doesn't report a size. `0` for web directories. | | `lastModified()` | `Instant` | Epoch `0` for web directories. | | `createdAt()` | `Instant?` | Not on web. Always `null` on Linux native, for Android `content://` files, and before Android 8.0. | | `mimeType()` | `MimeType?` | `null` for directories and unknown types. See [Check the MIME type](#check-the-mime-type). | | `parent()` | `PlatformFile?` | `null` at the root. See Android notes below for `content://` files. | | `isRegularFile()`, `isDirectory()` | `Boolean` | `false` for both when the file doesn't exist. | | `exists()`, `isAbsolute()` | `Boolean` | Not on web. | | `absolutePath()`, `absoluteFile()` | `String`, `PlatformFile` | Not on web. Resolves a relative path against the working directory. | | `toKotlinxIoPath()` | `Path` | Not on web. Throws `FileKitUriPathNotSupportedException` for Android `content://` files. | `Instant` is `kotlin.time.Instant`. To read the content, see [Reading files](/core/read-file). To list, copy, move, or delete, see [File operations](/core/file-operations). ## Check the MIME type ```kotlin theme={null} val type: MimeType? = file.mimeType() val isImage = type?.primaryType == "image" ``` | Platform | Source of the MIME type | | - | - | | Android | The `ContentResolver` for `content://` files, then `MimeTypeMap` from the extension. | | iOS, macOS, watchOS | Provider metadata (Files app, iCloud Drive), then the extension. Extension-less documents can still resolve. | | JVM | `Files.probeContentType`, which depends on the operating system's registry. | | Linux native | The shared MIME database (`/usr/share/mime/globs2`), then `/etc/mime.types`. | | Windows native | The `Content Type` value of the extension in the Windows registry. | | Web | The type the browser reports for the file. | ## Serialization `PlatformFile` is `@Serializable` through `PlatformFileSerializer`, so you can store it in your own serializable classes with the kotlinx.serialization plugin: ```kotlin nonWebMain theme={null} @Serializable data class Document(val title: String, val file: PlatformFile) val json = Json.encodeToString(Document("Report", FileKit.filesDir / "report.pdf")) val restored = Json.decodeFromString(json) ``` The file is encoded as its `path` string and decoded with `PlatformFile(path)`. > [!NOTE] > > Serialization keeps the path, not the access rights. A file picked outside your app (an Android `content://` file, a security-scoped file on Apple platforms) may not be readable after a restart: use [bookmark data](/core/bookmark-data) for persistent access. On the web, `PlatformFileSerializer` throws at runtime. ## Platform notes > **Android: content:// files** > > Files from pickers are usually `content://` Uris, not filesystem paths. `name` comes from the provider's display name. For Photo Picker Uris that only expose a synthetic name (such as `18.jpg`), FileKit looks up the original name in `MediaStore`, and falls back to a stable name derived from the provider and the Uri. > > `parent()` works inside external-storage document trees and keeps access to the selected tree. It returns `null` at the root of the selected directory and for providers whose parent can't be derived from the Uri. > **iOS and macOS: security-scoped files** > > Files picked outside your app's sandbox need security-scoped access. FileKit starts and stops it around each of its own calls on that file. Wrap longer work in `file.withScopedAccess { }`: iterating `walk()` over a picked folder, or passing `nsUrl` to Apple APIs yourself. > **Web: browser handles** > > A picked file is a read-only snapshot of a browser `File`: you can read it, not write it, and there is no path to reopen it later. A directory picker result is a virtual tree built from the selected files, so empty folders are missing, directories report a size of `0`, and their `lastModified()` is epoch `0`. See [Files on the web](/core/web). ## Related * [Reading files](/core/read-file) and [Writing files](/core/write-file) * [File operations](/core/file-operations): list, walk, copy, move, and delete * [App directories](/core/file-utils): app directories and user folders # Reading files Source: https://filekit.mintlify.app/core/read-file Read a PlatformFile as bytes, as text, or as a kotlinx-io stream [See the platform support matrix](/platform-support#filekit-core) `readBytes()` and `readString()` load the whole file into memory and return a `ByteArray` or a UTF-8 `String`. For large files, stream them with `source()` instead. ## Usage ```kotlin theme={null} val bytes: ByteArray = file.readBytes() val text: String = file.readString() ``` Both are suspend functions. On every platform except the web, they switch to `Dispatchers.IO` themselves. ## Stream a large file `source()` returns a [kotlinx-io](https://github.com/Kotlin/kotlinx-io) `RawSource`, so you can process the file in chunks without loading it at once. It is not available on the web. ```kotlin nonWebMain theme={null} suspend fun countLines(file: PlatformFile): Int = withContext(Dispatchers.IO) { file.source().buffered().use { source -> var count = 0 while (source.readLine() != null) count++ count } } ``` `source()` does blocking I/O: call it on `Dispatchers.IO`, and close it with `use`. ## Handle errors ```kotlin theme={null} val text: String? = try { file.readString() } catch (e: FileNotFoundException) { // kotlinx.io.files null } ``` A missing file throws `FileNotFoundException`, other I/O failures throw `kotlinx.io.IOException`. FileKit's own errors, such as reading a directory or a `content://` Uri that can't be opened, throw `FileKitException`. ## Platform notes | Platform | Behavior | | - | - | | Android | `content://` files are read through the `ContentResolver`. | | iOS, macOS | Security-scoped access is started and stopped for you, also for `source()` until you close it. | | Web | Picked files are read with a `FileReader`. Files in the origin private file system are read at their current content. See [Files on the web](/core/web). | ## Related * [Writing files](/core/write-file) * [PlatformFile](/core/platform-file): name, size, and other properties # Core setup Source: https://filekit.mintlify.app/core/setup Initialize FileKit Core on each platform, choose app directories, and add ProGuard rules [See the platform support matrix](/platform-support#filekit-core) `filekit-core` works without setup on most targets. JVM desktop, Linux native, and Windows native need an application ID so FileKit can resolve `FileKit.filesDir`, `FileKit.cacheDir`, and `FileKit.databasesDir`. Add the dependency first: see [Installation](/installation). ## Initialize FileKit **Android** Nothing to do. FileKit Core initializes itself with [App Startup](https://developer.android.com/topic/libraries/app-startup). | Directory | Location | | - | - | | `filesDir` | `Context.filesDir` (app-private internal storage) | | `cacheDir` | `Context.cacheDir` | | `databasesDir` | The parent of `Context.getDatabasePath(...)` (`databases/`) | These directories can't be changed. If you disable App Startup in your app, initialize FileKit Core yourself before using it: ```kotlin MainActivity.kt theme={null} class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FileKit.manualFileKitCoreInitialization(this) } } ``` Dialogs need their own registration when you use `filekit-dialogs` without Compose. See [Dialogs setup](/dialogs/setup). **JVM desktop** Call `FileKit.init` once at startup, before you read any app directory: ```kotlin main.kt theme={null} fun main() { FileKit.init(appId = "com.example.myapp") application { Window(onCloseRequest = ::exitApplication) { App() } } } ``` | Directory | Windows | macOS | Linux | | - | - | - | - | | `filesDir` | `%APPDATA%\` | `~/Library/Application Support/` | `$XDG_DATA_HOME/`, or `~/.local/share/` | | `cacheDir` | `%LOCALAPPDATA%\\Cache` | `~/Library/Caches/` | `$XDG_CACHE_HOME/`, or `~/.cache/` | | `databasesDir` | `filesDir/databases` | `filesDir/databases` | `filesDir/databases` | To use your own locations (for a portable app, for example), pass `java.io.File` values. A directory you leave out keeps its default: ```kotlin main.kt theme={null} val appDir = File(System.getProperty("user.home"), ".myapp") FileKit.init( appId = "com.example.myapp", filesDir = File(appDir, "data"), cacheDir = File(appDir, "cache"), ) ``` `FileKit.init(filesDir = ..., cacheDir = ...)` without an `appId` also works when you set both directories. **Linux native** For `linuxX64` and `linuxArm64`, call `FileKit.init` once at startup: ```kotlin main.kt theme={null} fun main() { FileKit.init(appId = "com.example.myapp") } ``` | Directory | Location | | - | - | | `filesDir` | `$XDG_DATA_HOME/`, or `~/.local/share/` | | `cacheDir` | `$XDG_CACHE_HOME/`, or `~/.cache/` | | `databasesDir` | `filesDir/databases` | A relative `XDG_*` value is ignored, as the XDG spec requires. When `HOME` is unset, FileKit reads the home directory from the passwd database. To use your own locations, pass `PlatformFile` values: ```kotlin main.kt theme={null} FileKit.init( appId = "com.example.myapp", filesDir = PlatformFile("/opt/myapp/data"), cacheDir = PlatformFile("/opt/myapp/cache"), ) ``` **Windows native** For `mingwX64`, call `FileKit.init` once at startup: ```kotlin main.kt theme={null} fun main() { FileKit.init(appId = "com.example.myapp") } ``` | Directory | Location | | - | - | | `filesDir` | `%APPDATA%\` | | `cacheDir` | `%LOCALAPPDATA%\\Cache` | | `databasesDir` | `filesDir\databases` | To use your own locations, pass `PlatformFile` values: ```kotlin main.kt theme={null} FileKit.init( appId = "com.example.myapp", filesDir = PlatformFile("C:\\MyApp\\data"), cacheDir = PlatformFile("C:\\MyApp\\cache"), ) ``` **iOS, macOS, watchOS, web** Nothing to do. Directories follow the platform conventions and can't be changed. | Platform | `filesDir` | `cacheDir` | | - | - | - | | iOS, watchOS | The app's `Documents` directory | The app's `Caches` directory | | macOS | `~/Library/Application Support/` | `~/Library/Caches` (the container's `Caches` when sandboxed) | `databasesDir` is `filesDir/databases` on Apple platforms. On macOS, `filesDir` uses the app bundle identifier and throws if the executable has none. Browsers have no app directories. To store files on the web, use the [origin private file system](/core/web#store-files-in-the-origin-private-file-system). > [!NOTE] > > On JVM, Linux native, and Windows native, reading `FileKit.filesDir`, `cacheDir`, or `databasesDir` before `FileKit.init` throws. FileKit creates `filesDir` and `cacheDir` the first time you read them. ## ProGuard If you shrink or obfuscate a JVM desktop app, add these rules to your `proguard-rules.pro`: ```proguard proguard-rules.pro theme={null} # Required on JVM for JNA-based integrations. -keep class com.sun.jna.** { *; } -keep class * implements com.sun.jna.** { *; } # Required when using FileKit Dialogs on Linux (XDG Desktop Portal / DBus). -keep class org.freedesktop.dbus.** { *; } -keep class io.github.vinceglb.filekit.dialogs.platform.xdg.** { *; } -keepattributes Signature,InnerClasses,RuntimeVisibleAnnotations ``` ## Related * [App directories](/core/file-utils): app directories and user folders such as Downloads. * [Dialogs setup](/dialogs/setup): Android activity registration and Linux packaging. # Files on the web Source: https://filekit.mintlify.app/core/web Read picked files, download files, and store files in the browser with FileKit on JS and Wasm [See the platform support matrix](/platform-support#filekit-core) On JS and Wasm, a `PlatformFile` wraps a browser handle, not a path. Files the user picks are read-only. To give the user a file, download it. To keep files between sessions, store them in the browser's origin private file system. ## What a web PlatformFile is | Source | Wraps | Read | Write, delete | List | | - | - | - | - | - | | File picker | A snapshot of a browser `File` | Yes | No | No | | Directory picker | A virtual tree of the selected files | Files only | No | `list()`, `walk()`, `listEntries()` | | `FileKit.originPrivateDir` (experimental) | A live origin private file system entry | Yes | Yes | `listEntries()` | The `path` of a picked file is its name, or for a file inside a picked folder, its relative path starting with the folder's name. Path constructors such as `PlatformFile("/path")`, `FileKit.filesDir`, and `exists()` don't exist on the web: keep that code in `nonWebMain`. ## Read a file ```kotlin webMain theme={null} val file = FileKit.openFilePicker() ?: return val text: String = file.readString() val bytes: ByteArray = file.readBytes() ``` Reads load the whole file into memory: `source()` is not available on the web. `name`, `extension`, `size()`, `lastModified()`, and `mimeType()` come from the browser's file metadata. ## Download files Browsers don't let a page choose where a file is saved, so there is no file saver on the web. `FileKit.download` hands the file to the browser, which saves it to the user's downloads folder: ```kotlin webMain theme={null} scope.launch { FileKit.download("Hello, FileKit!".encodeToByteArray(), fileName = "hello.txt") FileKit.download(file) // fileName defaults to file.name } ``` Both overloads are suspend functions. The `PlatformFile` overload reads the file first, so it works with picked files and origin private file system files. ## Store files in the origin private file system The [origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) (OPFS) is a private storage space for your site, hidden from the user and from other sites. FileKit can read, write, list, and delete files in it. > [!NOTE] > > This API is experimental: opt in with `@OptIn(ExperimentalFileKitApi::class)`, and expect signature changes. OPFS only works in secure contexts: HTTPS pages and local addresses such as `localhost`. ### Get a file or directory ```kotlin webMain theme={null} @OptIn(ExperimentalFileKitApi::class) suspend fun todayNote(): PlatformFile { val notes = FileKit.originPrivateDir.resolveDirectory("notes", create = true) return notes.resolveFile("today.txt", create = true) } ``` `resolveFile(relative, create = false)` and `resolveDirectory(relative, create = false)` take a path relative to an OPFS directory. With `create = true`, they create the entry and its missing parent directories. Without it, a missing entry throws. Empty segments are ignored, and `.` or `..` segments throw. ### Read and write ```kotlin webMain theme={null} file.writeString("Hello, FileKit!") file.write("Hello again".encodeToByteArray()) val text = file.readString() ``` `write` and `writeString` replace the file's content. They throw on any file that doesn't come from `FileKit.originPrivateDir`. ### List and delete ```kotlin webMain theme={null} val notes = FileKit.originPrivateDir.resolveDirectory("notes") val entries: List = notes.listEntries() notes.delete(recursively = true) ``` `list()` and `walk()` can't read OPFS directories: use the suspend `listEntries()`, which also works on picked folders. `delete(mustExist = true, recursively = false)` behaves as on other platforms. The root directory can't be deleted. ### Good to know * `size()` and `lastModified()` are up to date after each FileKit write. If another tab or worker changes a file, resolve it again to see the change. * Browsers may clear OPFS when the device runs low on storage. To prevent this, request persistent storage with [`navigator.storage.persist()`](https://developer.mozilla.org/en-US/docs/Web/API/StorageManager/persist): the data is protected only if it returns `true`, as the browser can refuse. ## Related * [File picker](/dialogs/file-picker) and [Directory picker](/dialogs/directory-picker) * [PlatformFile](/core/platform-file) # Writing files Source: https://filekit.mintlify.app/core/write-file Write bytes, text, or a kotlinx-io stream to a PlatformFile [See the platform support matrix](/platform-support#filekit-core) `write()` and `writeString()` replace the content of a file, creating it if needed. For large or incremental writes, stream with `sink()`. ## Usage ```kotlin nonWebMain theme={null} val file = FileKit.filesDir / "notes.txt" file.writeString("Hello, FileKit!") file.write("Hello again".encodeToByteArray()) ``` Both are suspend functions that switch to `Dispatchers.IO`. The parent directory must exist: create it first with [`createDirectories()`](/core/file-operations#create-directories). Failures throw `kotlinx.io.IOException` or `FileKitException`. > [!NOTE] > > These functions are not declared in `commonMain`. Call them from `nonWebMain` or a platform source set. The web has separate, experimental versions for the origin private file system: see [Write on the web](#write-on-the-web). ## Copy another file's content `write` also takes a `PlatformFile` and streams its content into the destination, without loading it into memory: ```kotlin nonWebMain theme={null} val destination = FileKit.filesDir / "copy.pdf" destination write pickedFile ``` This works when the source size is unknown, as with some Android `content://` files. It throws if the source is a directory or the same file as the destination. To copy into a folder or copy a whole tree, see [File operations](/core/file-operations#copy-a-file). ## Stream with a sink `sink()` returns a kotlinx-io `RawSink`. Pass `append = true` to add to the end of the file instead of replacing it: ```kotlin nonWebMain theme={null} suspend fun appendLog(file: PlatformFile, line: String) = withContext(Dispatchers.IO) { file.sink(append = true).buffered().use { sink -> sink.writeString("$line\n") } } ``` `sink()` does blocking I/O: call it on `Dispatchers.IO`, and close it with `use` so the data is flushed. ## Write on the web Browsers don't let a page write to the user's files. On JS and Wasm you have two options: * Give the user a file with `FileKit.download(bytes, fileName)`: see [Download files](/core/web#download-files). * Store files in the browser's private storage with the experimental origin private file system API, which provides its own `write`, `writeString`, and `delete`: see [Files on the web](/core/web#store-files-in-the-origin-private-file-system). ## Platform notes | Platform | Behavior | | - | - | | Android | Writing to a `content://` file truncates it first (`append = false`), including on providers such as Google Drive. A file you create with `/` inside a picked folder is created as a new document. | | iOS, macOS | Security-scoped access is started and stopped for you, also for `sink()` until you close it. | | Web | Only origin private file system files can be written. Writing to a picked file throws. | ## Related * [Reading files](/core/read-file) * [File operations](/core/file-operations): copy, move, delete, and create directories * [File saver](/dialogs/file-saver): let the user choose where to save # Camera picker Source: https://filekit.mintlify.app/dialogs/camera-picker Take a photo with the device camera and get it back as a PlatformFile on Android and iOS [See the platform support matrix](/platform-support#filekit-dialogs) Open the system camera to take a photo. FileKit writes the photo to a file and returns it as a `PlatformFile`. Call it from a source set that only includes Android and iOS, such as `mobileMain` (see [shared source sets](/platform-support#use-platform-specific-apis-from-shared-code)). ## Usage ```kotlin Compose theme={null} val launcher = rememberCameraPickerLauncher( onError = { failure -> showError(failure.message) }, ) { photo -> // photo is a PlatformFile, or null if the user cancelled } Button(onClick = { launcher.launch() }) { Text("Take a photo") } ``` ```kotlin Suspend API theme={null} val photo: PlatformFile? = FileKit.openCameraPicker() ``` The result is `null` when the user cancels. Failures go to `onError` in Compose, or are thrown as a `FileKitDialogException` by the suspend API. See [Results and errors](/dialogs/error-handling). ## Options **type** `FileKitCameraType` (default: Photo) What to capture. Only `Photo` is available: video capture is not supported yet. **cameraFacing** `FileKitCameraFacing` (default: System) `System` lets the camera app choose, or force `Front` or `Back`. Best effort on Android: see [Platform notes](#platform-notes). **destinationFile** `PlatformFile` (default: FileKit.cacheDir / "\.jpg") Where the photo is written. Files in `filesDir` and `cacheDir` need no Android setup. **openCameraSettings** `FileKitOpenCameraSettings` (default: createDefault()) Platform-specific settings. In the suspend API, pass it to `openCameraPicker()`. **properties** **authority** `String?` (default: null) FileProvider authority, only needed for a `destinationFile` outside `filesDir` and `cacheDir`. When `null`, FileKit uses its bundled `${applicationId}.FileKitFileProvider`. See [Android setup](#android-setup). **presenter** `UIViewController?` (default: null) View controller that presents the camera. When `null`, the suspend API presents from its own window above your app, and the Compose launcher uses the current `LocalUIViewController`. ## Keep the photo The default destination is in the cache directory, which the system can clear. To keep the photo, write it to `FileKit.filesDir` directly: ```kotlin theme={null} val photo = FileKit.openCameraPicker( destinationFile = FileKit.filesDir / "profile.jpg", ) ``` Files in `FileKit.filesDir` and `FileKit.cacheDir` need no Android setup. For any other directory, see [Android setup](#android-setup). ## Platform notes | Platform | Behavior | | - | - | | Android | Launches the device camera app with `ACTION_IMAGE_CAPTURE`. If your manifest declares `android.permission.CAMERA`, FileKit requests it before opening the camera, and a denial returns `null`. If you don't declare it, no permission is needed. | | Android | `Front` and `Back` are best effort. Android has no standard extra to choose the camera, so FileKit sends the extras that common camera apps read. Some devices ignore them. | | iOS | Add `NSCameraUsageDescription` to your `Info.plist`: iOS terminates apps that open the camera without it. | | iOS | The photo is saved as a full-quality JPEG. A missing camera (such as the Simulator) or an unavailable `Front` or `Back` camera fails with a `FileKitDialogException`. | ## Android setup Only needed for files outside `FileKit.filesDir` and `FileKit.cacheDir`. `filekit-dialogs` already ships a FileProvider (`${applicationId}.FileKitFileProvider`) that covers those two directories, and `content://` files returned by the pickers need no provider at all. For any other location, such as `getExternalFilesDir()`, `FileProvider` throws an `IllegalArgumentException` until you declare your own provider. **1. Declare a provider in your manifest** ```xml AndroidManifest.xml theme={null} ``` **2. List the directories it may expose** Add one entry per location you use, for example app-specific external storage: ```xml res/xml/file_paths.xml theme={null} ``` See [FileProvider](https://developer.android.com/reference/androidx/core/content/FileProvider) for every path type. **3. Pass the authority to FileKit** The `authority` parameter exists only on Android, so build these settings in `androidMain`: ```kotlin androidMain theme={null} val authority = "${FileKit.context.packageName}.fileprovider" FileKitOpenCameraSettings(authority = authority) // camera picker FileKitShareSettings(authority = authority) // share file FileKitOpenFileSettings(authority = authority) // open with default app ``` For example, to save to app-specific external storage: ```kotlin androidMain theme={null} val externalDir = PlatformFile(FileKit.context.getExternalFilesDir(null)!!) val photo = FileKit.openCameraPicker( destinationFile = externalDir / "photo.jpg", openCameraSettings = FileKitOpenCameraSettings( authority = "${FileKit.context.packageName}.fileprovider", ), ) ``` With Compose, pass the same `openCameraSettings` to `rememberCameraPickerLauncher` and the file to `launch(destinationFile = ...)`. ## Related * [Gallery picker](/dialogs/gallery-picker) to pick existing photos. * [Share file](/dialogs/share-file) to send the photo to another app. * [Results and errors](/dialogs/error-handling) for cancellation and failure handling. # Dialog settings Source: https://filekit.mintlify.app/dialogs/dialog-settings Customize picker and saver dialogs per platform with FileKitDialogSettings [See the platform support matrix](/platform-support#filekit-dialogs) `FileKitDialogSettings` customizes the file picker, directory picker, gallery picker, and file saver. Each platform has its own constructor, so build custom settings in platform code. `FileKitDialogSettings.createDefault()` works everywhere. ## Settings **title** `String?` (default: null) Dialog title. macOS shows it as the panel message. Some save dialogs ignore it: see [File saver](/dialogs/file-saver#platform-notes). **parent** `FileKitDialogParent?` (default: null) Window that owns the dialog, for modality and stacking. See [Parent window (JVM)](#parent-window-jvm). **macOS** `FileKitMacOSSettings` (default: FileKitMacOSSettings()) Native panel options when a JVM app runs on macOS. **properties** **canCreateDirectories** `Boolean` (default: true) Shows the New Folder button. **resolvesAliases** `Boolean?` (default: null) Alias resolution in open panels. `null` keeps the system behavior. **canCreateDirectories** `Boolean` (default: true) Shows the New Folder button. **assetRepresentationMode** `FileKitAssetRepresentationMode` (default: Automatic) Photos picker format: `Automatic`, `Current` (avoid transcoding), or `Compatible` (transcode to a widely supported format, for example when a library can't decode HEIC). **presenter** `UIViewController?` (default: null) View controller that presents the dialog. When `null`, the suspend API uses the top-most view controller and Compose launchers use the current `LocalUIViewController`. **title, canCreateDirectories** Accepted for parity, currently ignored. Android and the web have no settings: use `createDefault()`. ## Share settings across platforms Declare a function in `commonMain` and implement it per platform: ```kotlin commonMain theme={null} expect fun createDialogSettings(): FileKitDialogSettings ``` ```kotlin jvmMain theme={null} actual fun createDialogSettings() = FileKitDialogSettings( title = "Select a file", macOS = FileKitMacOSSettings(canCreateDirectories = false), ) ``` ```kotlin iosMain theme={null} actual fun createDialogSettings() = FileKitDialogSettings( assetRepresentationMode = FileKitAssetRepresentationMode.Compatible, ) ``` ```kotlin Other targets theme={null} // androidMain, webMain (or jsMain and wasmJsMain), macosMain, linuxMain, mingwX64Main actual fun createDialogSettings() = FileKitDialogSettings.createDefault() ``` Shared code then passes the result to any dialog: ```kotlin theme={null} suspend fun pickDocument(): PlatformFile? = FileKit.openFilePicker(dialogSettings = createDialogSettings()) ``` ## Parent window (JVM) Inside a Compose Desktop `Window`, the `WindowScope` versions of `rememberFilePickerLauncher`, `rememberDirectoryPickerLauncher`, and `rememberFileSaverLauncher` parent the dialog to that window, replacing any `parent` in your settings. Elsewhere, pass the parent yourself: ```kotlin jvmMain theme={null} fun dialogSettingsFor(window: java.awt.Window) = FileKitDialogSettings( parent = FileKitDialogParent.awt(window), ) ``` `FileKitDialogParent` also has `windows(hwnd)`, `x11(xid)`, and `wayland(exportedHandle)` factories for non-AWT windows. FileKit borrows the parent: keep the window alive until the dialog call returns. Invalid factory arguments and a parent the active dialog can't use both throw `IllegalArgumentException`. > **Supported parents per JVM dialog** > > | Active JVM dialog | Accepted parents | Notes | > | - | - | - | > | Windows | AWT, Windows HWND, or none | An AWT window is resolved to an HWND when the dialog opens. | > | Linux XDG portal | AWT, X11 XID, Wayland export, or none | X11 identifiers are sent as lowercase hexadecimal. | > | Linux AWT fallback | AWT `Frame` or `Dialog`, or none | An X11 or Wayland parent fails when the portal is unavailable; it is not dropped silently. | > | Linux Swing directory fallback | Any AWT `Window`, or none | X11 and Wayland parents are not compatible. | > | macOS | AWT, or none | The dialog is application-modal, not a window-modal sheet. | > **Wayland handles** > > `wayland()` takes the unprefixed opaque handle exported through `xdg_foreign`. FileKit adds the `wayland:` portal prefix without normalizing the value. A raw `wl_surface*`, a Tao handle, or a string built from a pointer is not an exported Wayland handle. Empty handles and handles containing NUL are rejected. > **Nucleus with the Tao backend** > > Nucleus 2.3.2 exposes the native identities needed to parent dialogs on Windows and Linux/X11. Convert the window yourself and pass the settings to a plain (non-`WindowScope`) launcher: > > ```kotlin jvmMain theme={null} > private fun NucleusWindow.fileKitDialogParent(): FileKitDialogParent? { > unsafe.awtWindow?.let { return FileKitDialogParent.awt(it) } > > return when (Platform.Current) { > Platform.Windows -> unsafe.taoWindow > ?.nativeHandle > ?.takeIf { it != 0L } > ?.let(FileKitDialogParent::windows) > Platform.Linux -> unsafe.taoWindow > ?.x11WindowId > ?.let(FileKitDialogParent::x11) > else -> null > } > } > > val settings = FileKitDialogSettings(parent = nucleusWindow.fileKitDialogParent()) > ``` > > Don't pass `nucleusWindow.unsafe.taoHandle`: it identifies the Tao event loop, not an operating-system window. On Wayland, `x11WindowId` is `null`, so this adapter opens an unparented dialog. The dialog is also unparented until the Nucleus window exposes a supported identity. ## Other settings classes | Class | Used by | Properties | | - | - | - | | `FileKitOpenCameraSettings` | [Camera picker](/dialogs/camera-picker) | Android `authority`, iOS `presenter` | | `FileKitShareSettings` | [Share file](/dialogs/share-file) | Android `authority`, `addOptionChooseIntent`; iOS `presenter`, `addOptionUIActivityViewController`, `metaTitle` | | `FileKitOpenFileSettings` | [Open with default app](/dialogs/open-file) | Android `authority` | > **Upgrading from older versions** > > FileKit 0.15 replaced the JVM `parentWindow` property. Replace `FileKitDialogSettings(parentWindow = window)` with `FileKitDialogSettings(parent = FileKitDialogParent.awt(window))`. There is no compatibility property or constructor. ## Related * [File picker](/dialogs/file-picker) * [Directory picker](/dialogs/directory-picker) * [File saver](/dialogs/file-saver) # Directory picker Source: https://filekit.mintlify.app/dialogs/directory-picker Let users pick a folder with the native directory dialog [See the platform support matrix](/platform-support#filekit-dialogs) `openDirectoryPicker` shows the platform's native folder dialog and returns the selected folder as a `PlatformFile`. ## Usage ```kotlin Compose theme={null} val launcher = rememberDirectoryPickerLauncher( onError = { failure -> showError(failure.message) }, ) { directory -> // directory is a PlatformFile, or null when the user cancelled } Button(onClick = { launcher.launch() }) { Text("Pick a folder") } ``` ```kotlin Suspend API theme={null} val directory: PlatformFile? = FileKit.openDirectoryPicker() ``` The result is `null` when the user cancels. Failures go to `onError` in Compose, or are thrown as a `FileKitDialogException` by the suspend API. See [Results and errors](/dialogs/error-handling). ## Try it This is the FileKit sample running in your browser. Pick a folder to browse its contents as a tree, then open a file to inspect its metadata. [Open in a new tab ↗](https://vinceglb.github.io/FileKit/?demo=directory-picker) ## Options In Compose, pass these options to `rememberDirectoryPickerLauncher`. `launch()` takes no arguments. **directory** `PlatformFile?` (default: null) Folder the dialog opens in. Ignored on the web. **dialogSettings** `FileKitDialogSettings` (default: createDefault()) Title, parent window, and other [per-platform settings](/dialogs/dialog-settings). ## Read the folder's contents Call `list()` for the direct children or `walk()` for the whole tree. See [File operations](/core/file-operations#list-a-directory). ## Keep access across launches A picked folder is only accessible for the current session on Android and Apple platforms. Save its bookmark to reopen it after a restart: ```kotlin nonWebMain theme={null} val directory = FileKit.openDirectoryPicker() ?: return val bookmark = directory.bookmarkData() saveBytes("project_folder", bookmark.bytes) // After a restart val restored = PlatformFile.fromBookmarkData(loadBytes("project_folder")) ``` `saveBytes` and `loadBytes` stand for your own storage. See [Bookmark data](/core/bookmark-data) for invalid bookmarks and security-scoped access on Apple platforms. ## Platform notes | Platform | Behavior | | - | - | | Android | Storage Access Framework tree picker. The result is a `content://` tree URI. | | iOS | `UIDocumentPickerViewController` in folder mode. | | macOS | `NSOpenPanel` configured for folders. | | JVM | Native dialog on macOS and Windows, and the XDG Desktop Portal on Linux with a Swing fallback when no portal runs. | | Linux native | XDG Desktop Portal only, with no fallback dialog. See [Linux setup](/dialogs/setup#linux-setup). | | Windows native | `IFileDialog` in folder mode. | | Web | Browsers expose the folder as a flat file list, so FileKit rebuilds a read-only tree from relative paths. Empty subfolders are missing, folders report a `lastModified()` of epoch `0`, and an empty folder returns `null`, like a cancellation. | ## Related * [Bookmark data](/core/bookmark-data) to keep access to the folder. * [Results and errors](/dialogs/error-handling) for cancellation and failure handling. # Results and errors Source: https://filekit.mintlify.app/dialogs/error-handling How FileKit dialogs report results, cancellation, and failures in Compose and the suspend API [See the platform support matrix](/platform-support#filekit-dialogs) Every FileKit dialog separates three outcomes: a value, a user cancellation (`null`), and an operational failure (a `FileKitDialogException`). Invalid arguments and unexpected defects are never converted into failures: they propagate as the original exception. ## Compose launchers Use the launcher overloads that take `onError`. The trailing lambda is `onResult`: ```kotlin theme={null} val launcher = rememberFilePickerLauncher( onError = { failure -> showError(failure.message) }, ) { file -> // file is null when the user cancelled } ``` | Outcome | `onResult` | `onError` | Propagates | | - | - | - | - | | A value-returning picker, directory picker, saver, or camera succeeds | Called once with the value | Not called | No | | The user dismisses a value-returning dialog | Called once with `null` | Not called | No | | Android camera permission is denied | Called once with `null` | Not called | No | | A valid FileKit operation cannot start or complete | Not called | Called once with a `FileKitDialogException` subtype | No | | The launcher coroutine is cancelled | Not called | Not called | Cancellation | | The invocation is invalid or an unexpected defect occurs | Not called | Not called | The original exception | | Your `onResult` or `onError` callback throws | No compensating callback is delivered | No compensating callback is delivered | The callback exception | `rememberFilePickerLauncher` passes a `FileKitPickerException` (the picker subtype of `FileKitDialogException`) to `onError`. The directory picker, file saver, camera, and share launchers pass a `FileKitDialogException`. If a dialog never appears on iOS, check where the launcher is remembered: see [Compose launchers](/dialogs/setup#compose-launchers). ## Suspend API The `FileKit.open…` functions return `null` when the user cancels and throw on operational failures: | Function | On cancel | Throws | | - | - | - | | `openFilePicker` (`Single`, `Multiple`) | `null` | `FileKitPickerException` | | `openDirectoryPicker`, `openFileSaver`, `openCameraPicker` | `null` | `FileKitDialogException` | | `shareFile` | Returns normally | `FileKitDialogException` | ```kotlin theme={null} val file = try { FileKit.openFilePicker() } catch (e: FileKitPickerException) { showError(e.message) null } ``` Coroutine cancellation and invalid arguments (for example `maxItems` outside `1..50`) propagate as usual. Catch `FileKitDialogException` to handle every dialog failure in one place. ## State-tracking picker modes `FileKitMode.SingleWithState` and `MultipleWithState` deliver progress and their terminal outcome as `FileKitPickerState` values, through `onResult` in Compose or the returned `Flow` in the suspend API: * `Started` and `Progress` are non-terminal values. * Exactly one of `Completed`, `Cancelled`, or `Failed` is the terminal value. * `FileKitPickerState.Failed` is data: it is not duplicated through `onError`. * A picker failure thrown outside the state stream reaches `onError` (or is thrown while collecting the flow). * Coroutine cancellation stops delivery and produces no later terminal value. ## Sharing Sharing has no success callback. A successful share launch, including user dismissal after the system share sheet appears, stays callback-less. A FileKit-owned operational failure reaches `onError`. Coroutine cancellation, invalid invocations, unexpected defects, and callback exceptions propagate. ## Compatibility overloads Launcher overloads without `onError` remain available for source and binary compatibility. They keep their historical callback shape and ignore operational failures without logging. They still propagate coroutine cancellation, invalid invocations, unexpected defects, and exceptions thrown by your callbacks. Prefer the `onError` overloads in new code so failures are visible. ## Related * [File picker](/dialogs/file-picker) for picking modes, including state tracking. * [Dialogs setup](/dialogs/setup#compose-launchers) for where to remember launchers. # File picker Source: https://filekit.mintlify.app/dialogs/file-picker Let users pick one or several files with the native file dialog [See the platform support matrix](/platform-support#filekit-dialogs) `openFilePicker` shows the platform's native file dialog and returns the selected file as a `PlatformFile`, or a list of files in multiple mode. ## Usage ```kotlin Compose theme={null} val launcher = rememberFilePickerLauncher( onError = { failure -> showError(failure.message) }, ) { file -> // file is a PlatformFile, or null when the user cancelled } Button(onClick = { launcher.launch() }) { Text("Pick a file") } ``` ```kotlin Suspend API theme={null} val file: PlatformFile? = FileKit.openFilePicker() ``` The result is `null` when the user cancels. Failures go to `onError` in Compose, or are thrown as a `FileKitDialogException` by the suspend API. See [Results and errors](/dialogs/error-handling). ## Try it This is the FileKit sample running in your browser. Pick files, switch the selection mode or restrict extensions, then open a file to inspect its metadata. [Open in a new tab ↗](https://vinceglb.github.io/FileKit/?demo=file-picker) ## Options In Compose, pass these options to `rememberFilePickerLauncher`. `launch()` takes no arguments. **type** `FileKitType` (default: File()) Which files the user can pick. `File()` allows any file. **values** **File(extensions)** `FileKitType` Files with the given extensions, without the dot: `FileKitType.File("pdf", "docx")`. See [Filter by extension](#filter-by-extension). **Image, Video, ImageAndVideo** `FileKitType` Media files. Opens the native [gallery picker](/dialogs/gallery-picker) on Android and iOS, and a filtered file dialog elsewhere. **mode** `FileKitMode` (default: Single) How many files the user can pick. The result type follows the mode. **values** **Single** `PlatformFile?` One file. **Multiple(maxItems)** `List\?` Several files. `maxItems` is `1` to `50`, or `null` for no limit. See [Pick multiple files](#pick-multiple-files). **SingleWithState, MultipleWithState(maxItems)** `FileKitPickerState` Reports progress while picked files are prepared. The suspend API returns a `Flow`. See [Track progress](#track-progress). **directory** `PlatformFile?` (default: null) Folder the dialog opens in. Ignored on Android and the web. **dialogSettings** `FileKitDialogSettings` (default: createDefault()) Title, parent window, and other [per-platform settings](/dialogs/dialog-settings). ## Filter by extension Pass the extensions without the dot: ```kotlin Compose theme={null} val launcher = rememberFilePickerLauncher( type = FileKitType.File("pdf", "docx"), onError = { failure -> showError(failure.message) }, ) { file -> // a pdf or docx file, or null } ``` ```kotlin Suspend API theme={null} val file = FileKit.openFilePicker(type = FileKitType.File("pdf", "docx")) ``` `FileKitType.File` also accepts a `List` or a `Set`. ## Pick multiple files `FileKitMode.Multiple()` returns a `List`, or `null` when the user cancels: ```kotlin Compose theme={null} val launcher = rememberFilePickerLauncher( mode = FileKitMode.Multiple(maxItems = 10), onError = { failure -> showError(failure.message) }, ) { files -> // List? with at most 10 files } ``` ```kotlin Suspend API theme={null} val files = FileKit.openFilePicker(mode = FileKitMode.Multiple(maxItems = 10)) ``` The iOS gallery picker and the Android Photo Picker enforce `maxItems` in their UI. Other dialogs, including Android's document picker fallback, have no native limit, so FileKit keeps the first `maxItems` files in the order the platform returns them. ## Track progress `SingleWithState` and `MultipleWithState` report each step as a `FileKitPickerState`, which lets you show a loading indicator while large files are prepared. In Compose, each state reaches `onResult`. The suspend API returns a `Flow`: ```kotlin theme={null} FileKit.openFilePicker(mode = FileKitMode.MultipleWithState()).collect { state -> when (state) { is FileKitPickerState.Started -> showLoading(total = state.total) is FileKitPickerState.Progress -> updateLoading(state.processed.size, state.total) is FileKitPickerState.Completed -> showFiles(state.result) is FileKitPickerState.Failed -> showError(state.cause.message) FileKitPickerState.Cancelled -> hideLoading() } } ``` Progress matters most for iOS gallery picks, where FileKit copies each item before returning it. Other dialogs deliver every state at once when they close. See [Results and errors](/dialogs/error-handling#state-tracking-picker-modes) for the full state contract. ## Platform notes | Platform | Behavior | | - | - | | Android | Storage Access Framework document picker. Extensions are converted to MIME types: unknown extensions are dropped, and if none is known every file is allowed. | | iOS | `UIDocumentPickerViewController`. Media types open `PHPickerViewController`. | | macOS | `NSOpenPanel`. | | JVM | `NSOpenPanel` on macOS, `IFileDialog` on Windows, and the XDG Desktop Portal on Linux, with an AWT/Swing fallback when no portal runs. | | Linux native | XDG Desktop Portal only, with no fallback dialog. Dialog settings support the title only. See [Linux setup](/dialogs/setup#linux-setup). | | Windows native | `IFileDialog`. Dialog settings support the title only. | | Web | ``. The type filter becomes the `accept` attribute. `directory` and dialog settings are ignored. | ## Related * [Gallery picker](/dialogs/gallery-picker) for photos and videos. * [Reading files](/core/read-file) once the user has picked it. * [Results and errors](/dialogs/error-handling) for cancellation and failure handling. # File saver Source: https://filekit.mintlify.app/dialogs/file-saver Let the user choose where to save a file, then write your data to it [See the platform support matrix](/platform-support#filekit-dialogs) Open the native save dialog. FileKit returns the location the user picked as a `PlatformFile`, and you write your data to it. The file saver is not available on the web, so call it from a non-web source set such as `nonWebMain` (see [shared source sets](/platform-support#use-platform-specific-apis-from-shared-code)). ## Usage ```kotlin Compose theme={null} val scope = rememberCoroutineScope() val launcher = rememberFileSaverLauncher( dialogSettings = FileKitDialogSettings.createDefault(), onError = { failure -> showError(failure.message) }, ) { file -> // onError covers the dialog only: handle write errors (IOException) here scope.launch { file?.writeString("Hello FileKit!") } } Button(onClick = { launcher.launch(suggestedName = "notes", defaultExtension = "txt") }) { Text("Save") } ``` ```kotlin Suspend API theme={null} val file = FileKit.openFileSaver(suggestedName = "notes", defaultExtension = "txt") file?.writeString("Hello FileKit!") ``` The result is `null` when the user cancels. Failures go to `onError` in Compose, or are thrown as a `FileKitDialogException` by the suspend API. See [Results and errors](/dialogs/error-handling). ## Options **suggestedName** `String` File name without the extension. **defaultExtension** `String?` (default: null) Extension without the dot, appended to `suggestedName`. **allowedExtensions** `Set\?` (default: null) Save types offered by the dialog, without dots. A hint, not a validation. **directory** `PlatformFile?` (default: null) Initial folder. Ignored on Android. **dialogSettings** `FileKitDialogSettings` (default: createDefault()) Required by the Compose launcher. In a Compose Desktop `Window`, `WindowScope.rememberFileSaverLauncher` makes it optional and parents the dialog to the window. See [Dialog settings](/dialogs/dialog-settings). ## Offer several file types Pass `allowedExtensions` to let the user pick a format. `defaultExtension` stays the preselected one: ```kotlin theme={null} val file = FileKit.openFileSaver( suggestedName = "report", defaultExtension = "pdf", allowedExtensions = setOf("pdf", "txt"), ) ``` Not every platform enforces the filter, so check `file.extension` if your app needs a specific format. ## Download file on the web Browsers don't let you choose a save location: use `FileKit.download` to send bytes to the user's downloads folder instead. See [Download files](/core/web#download-files). ## Platform notes | Platform | Behavior | | - | - | | Android | Opens the system "Create document" screen, which creates an empty file at the chosen location. With several `allowedExtensions`, the MIME type falls back to `*/*`. | | iOS | Exports a temporary placeholder file through the Files picker, then removes it so you can write to the returned location. `allowedExtensions` is ignored. | | Desktop | The save dialog shows `dialogSettings.title` on Linux native, Windows native, and JVM on macOS. The macOS native saver and the JVM savers on Windows and Linux ignore it. | | Linux native | Needs a running XDG Desktop Portal. See [Dialogs setup](/dialogs/setup#linux-setup). | | Web | Not available. Use `FileKit.download`. | > **Upgrading from older versions** > > The `extension` parameter of `openFileSaver` and `launch()` is deprecated. Rename it to `defaultExtension`; the behavior is the same. ## Related * [Writing files](/core/write-file) for streaming large content. * [Dialog settings](/dialogs/dialog-settings) for titles and parent windows. # Gallery picker Source: https://filekit.mintlify.app/dialogs/gallery-picker Let users pick photos and videos with the native gallery on Android and iOS [See the platform support matrix](/platform-support#filekit-dialogs) Pass `FileKitType.Image`, `Video`, or `ImageAndVideo` to the file picker. On Android and iOS it opens the system photo picker; on other targets it opens a file dialog filtered to media files. ## Usage ```kotlin Compose theme={null} val launcher = rememberFilePickerLauncher( type = FileKitType.Image, onError = { failure -> showError(failure.message) }, ) { image -> // image is a PlatformFile, or null when the user cancelled } Button(onClick = { launcher.launch() }) { Text("Pick a photo") } ``` ```kotlin Suspend API theme={null} val image: PlatformFile? = FileKit.openFilePicker(type = FileKitType.Image) ``` The result is `null` when the user cancels. Failures go to `onError` in Compose, or are thrown as a `FileKitDialogException` by the suspend API. See [Results and errors](/dialogs/error-handling). Every [file picker option](/dialogs/file-picker#options) applies, including the state-tracking modes. ## Try it This is the FileKit sample running in your browser. Choose images, videos, or both, switch the selection mode, then open a file to inspect its metadata. In a browser the picker is a file input filtered to media files. [Open in a new tab ↗](https://vinceglb.github.io/FileKit/?demo=gallery-picker) ## What users see | Platform | Dialog | | - | - | | Android | The system [Photo Picker](https://developer.android.com/training/data-storage/shared/photopicker). If the device has none, FileKit opens the document picker filtered by `image/*` or `video/*`. | | iOS | [`PHPickerViewController`](https://developer.apple.com/documentation/photokit/selecting-photos-and-videos-in-ios). FileKit copies each selected item to a temporary directory before returning it. | | macOS, JVM, Linux native, Windows native | The regular file dialog, filtered to `png`, `jpg`, `jpeg`, `gif`, `bmp` for images and `mp4`, `mov`, `avi`, `mkv`, `webm` for videos. | | Web | `` with `accept` set to `image/*`, `video/*`, or both. | ![The Android Photo Picker](https://developer.android.com/static/images/training/data-storage/kotlin-picker.gif) ## Limit the number of items Use `FileKitMode.Multiple(maxItems)` with a value from `1` to `50`. The same `mode` works on `rememberFilePickerLauncher`: ```kotlin theme={null} val media: List? = FileKit.openFilePicker( type = FileKitType.ImageAndVideo, mode = FileKitMode.Multiple(maxItems = 5), ) ``` The iOS picker and the Android Photo Picker enforce the limit in their UI. When Android falls back to the document picker, and on other targets, the dialog can't limit the selection, so FileKit keeps the first `maxItems` files. > [!TIP] > > iOS can return photos as HEIC, which some image decoders and servers can't read. In `iosMain`, pass `FileKitDialogSettings(assetRepresentationMode = FileKitAssetRepresentationMode.Compatible)` as `dialogSettings` so iOS transcodes them to a more compatible format. ## Related * [Camera picker](/dialogs/camera-picker) to take a new photo. * [Coil](/integrations/coil) to display the picked image. * [Image and video utilities](/core/image-and-video-utils) to compress or resize it. # Open with default app Source: https://filekit.mintlify.app/dialogs/open-file Open a file in the app the system associates with its type, such as a PDF viewer [See the platform support matrix](/platform-support#filekit-dialogs) Hand a file to the app the system associates with its type: a PDF viewer for `.pdf`, an image viewer for `.jpg`. Your app doesn't read the content. The function is not available on the web, so call it from a non-web source set such as `nonWebMain` (see [shared source sets](/platform-support#use-platform-specific-apis-from-shared-code)). ## Usage `openFileWithDefaultApplication` is a regular function, not a suspend function, so you can call it directly from a click handler. There is no Compose launcher. ```kotlin theme={null} val file = FileKit.filesDir / "report.pdf" FileKit.openFileWithDefaultApplication(file) ``` ## Options **file** `PlatformFile` The file to open. It must exist. **openFileSettings** `FileKitOpenFileSettings` (default: createDefault()) Platform-specific settings. **properties** **authority** `String?` (default: null) FileProvider authority, only needed for files outside `filesDir` and `cacheDir`. When `null`, FileKit uses its bundled `${applicationId}.FileKitFileProvider`. See [Android setup](#android-setup). ## Platform notes Failures are reported differently on each platform. Only the JVM wraps some of them in `FileKitDialogException`: | Platform | Behavior | | - | - | | Android | Sends an `ACTION_VIEW` intent with the MIME type of the file extension. Throws `ActivityNotFoundException` when no app handles that type, and `IllegalArgumentException` for a file outside the directories its FileProvider covers. | | iOS | Calls `openURL`. If that fails, shows the "Open in" menu (`UIDocumentInteractionController`) so the user can pick an app. Reports no error. | | macOS | Uses `NSWorkspace`. Reports no error. | | JVM | Uses `java.awt.Desktop.open` when the desktop supports opening files. Otherwise (headless setups, Linux without GNOME libraries), on Linux it falls back to `xdg-open` and waits up to half a second for it to report an error. Throws `FileKitDialogException` when neither works: `Desktop` cannot open files and `xdg-open` is missing, cannot start, or exits with an error code. Errors from `Desktop.open` itself are not wrapped, such as `IOException` when no app is associated. | | Linux native | Starts `xdg-open` without waiting for it. Throws `FileKitException` only if the process cannot start. | | Windows native | Calls `ShellExecuteW` with the `open` verb. Reports no error. | | Web | Not available. | ## Android setup Only needed for files outside `FileKit.filesDir` and `FileKit.cacheDir`. `filekit-dialogs` already ships a FileProvider (`${applicationId}.FileKitFileProvider`) that covers those two directories, and `content://` files returned by the pickers need no provider at all. For any other location, such as `getExternalFilesDir()`, `FileProvider` throws an `IllegalArgumentException` until you declare your own provider. **1. Declare a provider in your manifest** ```xml AndroidManifest.xml theme={null} ``` **2. List the directories it may expose** Add one entry per location you use, for example app-specific external storage: ```xml res/xml/file_paths.xml theme={null} ``` See [FileProvider](https://developer.android.com/reference/androidx/core/content/FileProvider) for every path type. **3. Pass the authority to FileKit** The `authority` parameter exists only on Android, so build these settings in `androidMain`: ```kotlin androidMain theme={null} val authority = "${FileKit.context.packageName}.fileprovider" FileKitOpenCameraSettings(authority = authority) // camera picker FileKitShareSettings(authority = authority) // share file FileKitOpenFileSettings(authority = authority) // open with default app ``` ## Related * [Share file](/dialogs/share-file) to send a file to another app. * [File picker](/dialogs/file-picker) to let the user choose one. # Set up FileKit Dialogs Source: https://filekit.mintlify.app/dialogs/setup Add FileKit Dialogs, register your Android activity, and prepare Linux and desktop builds [See the platform support matrix](/platform-support#filekit-dialogs) FileKit Dialogs opens native file, folder, gallery, and save dialogs and returns the result as a `PlatformFile`. Most targets need no setup beyond the dependency. ## Installation FileKit can helps you to display dialogs to the user like file, directory and photo pickers, save dialogs, camera and more. FileKit dialogs are available in two flavors: ```kotlin build.gradle.kts theme={null} // Enables FileKit dialogs without Compose dependencies implementation("io.github.vinceglb:filekit-dialogs:0.17.0") // Enables FileKit dialogs with Composable utilities implementation("io.github.vinceglb:filekit-dialogs-compose:0.17.0") ``` ```toml libs.versions.toml theme={null} [versions] filekit = "0.17.0" [libraries] filekit-dialogs = { module = "io.github.vinceglb:filekit-dialogs", version.ref = "filekit" } filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" } ``` Both artifacts include `filekit-core`. Check the [platform support matrix](/platform-support#filekit-dialogs) for the dialogs available on each target. ## Android setup Only Android needs an init step for dialogs, and only when you call the suspend APIs (`FileKit.openFilePicker()`…). Compose launchers register the activity for you, but a suspend call made before any launcher is composed throws `FileKitNotInitializedException`. Register your activity so FileKit can reach its `ActivityResultRegistry`: ```kotlin MainActivity.kt theme={null} class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FileKit.init(this) } } ``` With `filekit-dialogs-compose`, the launchers find the registry themselves. On JVM, Linux native, and Windows native, `FileKit.init(appId)` is not needed for dialogs: it only configures app directories such as `filesDir` and `cacheDir`. See [Core setup](/core/setup). ## Linux setup **JVM on Linux** Packaged Compose Desktop apps need the `jdk.security.auth` module, otherwise dialogs can fail with a `NoClassDefFoundError` ([issue #107](https://github.com/vinceglb/FileKit/issues/107)): ```kotlin build.gradle.kts theme={null} compose.desktop { application { nativeDistributions { linux { modules("jdk.security.auth") } } } } ``` Dialogs use the XDG Desktop Portal when it is running and fall back to AWT/Swing dialogs otherwise. **Linux native** On `linuxX64` and `linuxArm64`, dialogs go through the XDG Desktop Portal (`org.freedesktop.portal.Desktop`), which runs by default on GNOME, KDE, and most modern desktops. There is no fallback dialog and no parent window. Your binary links against `libdbus-1`: * **To build**, install the D-Bus development package for the target architecture (`libdbus-1-dev` on Debian/Ubuntu). It provides the unversioned `libdbus-1.so` linker file. * **To run**, the runtime library is enough. If the library directory is outside Kotlin/Native's linker search paths, add it to your target: ```kotlin build.gradle.kts theme={null} kotlin { linuxX64 { binaries.all { linkerOpts("-L/usr/lib/x86_64-linux-gnu") } } } ``` On a Linux build machine, `pkg-config --variable=libdir dbus-1` prints the directory to use. Cross-compiling requires the libraries of the target architecture. ## Compose launchers `remember…Launcher` functions capture platform context when they are remembered, not when you call `launch()`. A few platforms care about where that happens. | Platform | What to do | | - | - | | iOS | Remember launchers in a stable scope, such as your screen's root. A launcher remembered inside a `ModalBottomSheet`, dialog, popup, or dropdown keeps that transient view controller as its presenter ([issue #547](https://github.com/vinceglb/FileKit/issues/547)). | | JVM | Called with a `WindowScope` receiver (directly inside `Window { }` content), the launchers parent the dialog to that window. Elsewhere, pass `FileKitDialogSettings(parent = FileKitDialogParent.awt(window))`. See [Dialog settings](/dialogs/dialog-settings). | | JVM on macOS | To make dialogs follow the system light or dark theme, set `apple.awt.application.appearance` before creating any window (snippet below). | ```kotlin main.kt theme={null} fun main() { System.setProperty("apple.awt.application.appearance", "system") application { Window(onCloseRequest = ::exitApplication) { App() } } } ``` ## ProGuard If you shrink or obfuscate your JVM app, add the JNA and D-Bus keep rules from [Core setup](/core/setup#proguard). ## Related * [File picker](/dialogs/file-picker) to open your first dialog. * [Results and errors](/dialogs/error-handling) for cancellation and failure handling. # Share file Source: https://filekit.mintlify.app/dialogs/share-file Open the native share sheet for one or more files on Android and iOS [See the platform support matrix](/platform-support#filekit-dialogs) Open the system share sheet so the user can send files to another app. Call it from a source set that only includes Android and iOS, such as `mobileMain` (see [shared source sets](/platform-support#use-platform-specific-apis-from-shared-code)). ![iOS share sheet for a JPEG image, with Reminders, Copy, Print, and Assign to Contact actions](https://mintcdn.com/filekit/CkpVFgz9dKnI5cGG/images/share-file-ios.png?fit=max\&auto=format\&n=CkpVFgz9dKnI5cGG\&q=85\&s=021252783dbb079afaba614388dd5384) ![Android share sheet previewing an image, with Quick Share, Photos, Print, Drive, and Maps targets](https://mintcdn.com/filekit/CkpVFgz9dKnI5cGG/images/share-file-android.png?fit=max\&auto=format\&n=CkpVFgz9dKnI5cGG\&q=85\&s=1e6a3d0a4c33a6676781bac89f6947c8) ## Usage ```kotlin Compose theme={null} val launcher = rememberShareFileLauncher( onError = { failure -> showError(failure.message) }, ) Button(onClick = { launcher.launch(FileKit.filesDir / "report.pdf") }) { Text("Share") } ``` ```kotlin Suspend API theme={null} FileKit.shareFile(FileKit.filesDir / "report.pdf") // Several files at once FileKit.shareFile(listOf(FileKit.filesDir / "a.jpg", FileKit.filesDir / "b.jpg")) ``` Sharing has no result: a completed share and a dismissed sheet look the same. Failures go to `onError` in Compose, or are thrown as a `FileKitDialogException` by the suspend API. See [Results and errors](/dialogs/error-handling#sharing). ## Options **file / files** `PlatformFile | List\` What to share. An empty list does nothing. **shareSettings** `FileKitShareSettings` (default: createDefault()) Platform-specific settings. In the suspend API, pass it to `shareFile()`. **properties** **authority** `String?` (default: null) FileProvider authority, only needed for files outside `filesDir` and `cacheDir`. When `null`, FileKit uses its bundled `${applicationId}.FileKitFileProvider`. **addOptionChooseIntent** `(Intent) -\> Unit` (default: no-op) Customizes the chooser `Intent` before it launches. **addOptionUIActivityViewController** `(UIActivityViewController) -\> Unit` (default: no-op) Customizes the share sheet before it appears, for example `excludedActivityTypes`. **presenter** `UIViewController?` (default: null) View controller that presents the sheet. When `null`, the suspend API uses the top-most view controller and the Compose launcher uses the current `LocalUIViewController`. **metaTitle** `String` (default: "Share File") Accepted but currently not applied to the share sheet. ## Platform notes | Platform | Behavior | | - | - | | Android | Files in `FileKit.filesDir`, `FileKit.cacheDir`, or returned by a picker (`content://`) share without setup. The MIME type comes from the file extensions, or `*/*` when they differ. The suspend call returns once the chooser opens. | | iOS | The suspend call returns when the sheet closes. An error reported by the chosen activity fails with a `FileKitDialogException`. On iPad, the sheet opens as a popover centered on the presenter's view. | ## Android setup Only needed for files outside `FileKit.filesDir` and `FileKit.cacheDir`. `filekit-dialogs` already ships a FileProvider (`${applicationId}.FileKitFileProvider`) that covers those two directories, and `content://` files returned by the pickers need no provider at all. For any other location, such as `getExternalFilesDir()`, `FileProvider` throws an `IllegalArgumentException` until you declare your own provider. **1. Declare a provider in your manifest** ```xml AndroidManifest.xml theme={null} ``` **2. List the directories it may expose** Add one entry per location you use, for example app-specific external storage: ```xml res/xml/file_paths.xml theme={null} ``` See [FileProvider](https://developer.android.com/reference/androidx/core/content/FileProvider) for every path type. **3. Pass the authority to FileKit** The `authority` parameter exists only on Android, so build these settings in `androidMain`: ```kotlin androidMain theme={null} val authority = "${FileKit.context.packageName}.fileprovider" FileKitOpenCameraSettings(authority = authority) // camera picker FileKitShareSettings(authority = authority) // share file FileKitOpenFileSettings(authority = authority) // open with default app ``` > [!NOTE] > > A custom `authority` applies to every file in the share. If you share files from several directories at once, for example `filesDir` and `getExternalFilesDir()`, your provider's `file_paths.xml` must cover all of them. Otherwise FileKit throws an `IllegalArgumentException` before the share sheet opens. ## Related * [Open with default app](/dialogs/open-file) to view a file instead of sending it. * [Camera picker](/dialogs/camera-picker) to capture a photo to share. # Installation Source: https://filekit.mintlify.app/installation Add FileKit to your Kotlin Multiplatform project and set up each platform FileKit is published on Maven Central under the `io.github.vinceglb` group. Add the modules you need to your shared module's `commonMain` dependencies. If some of your targets aren't supported by a module (for example `filekit-dialogs` on watchOS, or Compose modules on Linux and Windows native), add it to an [intermediate source set](/platform-support#use-platform-specific-apis-from-shared-code) instead. ## Choose your modules | You want to… | Add | | - | - | | Show native pickers and save dialogs from Compose | `filekit-dialogs-compose` | | Show native dialogs from ViewModels, shared logic, or non-Compose UI | `filekit-dialogs` | | Read, write, and manage files without dialogs | `filekit-core` | | Display a `PlatformFile` image with Coil 3 | `filekit-coil` | `filekit-dialogs-compose` includes `filekit-dialogs`, which includes `filekit-core`. ## Add the dependencies **Dialogs** FileKit can helps you to display dialogs to the user like file, directory and photo pickers, save dialogs, camera and more. FileKit dialogs are available in two flavors: ```kotlin build.gradle.kts theme={null} // Enables FileKit dialogs without Compose dependencies implementation("io.github.vinceglb:filekit-dialogs:0.17.0") // Enables FileKit dialogs with Composable utilities implementation("io.github.vinceglb:filekit-dialogs-compose:0.17.0") ``` ```toml libs.versions.toml theme={null} [versions] filekit = "0.17.0" [libraries] filekit-dialogs = { module = "io.github.vinceglb:filekit-dialogs", version.ref = "filekit" } filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" } ``` **Core only** FileKit provides a platform-agnostic file abstraction called `PlatformFile`. It allows you to work with files in a platform-agnostic way. You can create, read, write, and delete files using the `PlatformFile` API. You can use `PlatformFile` in your project by adding the following dependency: ```kotlin build.gradle.kts theme={null} implementation("io.github.vinceglb:filekit-core:0.17.0") ``` ```toml libs.versions.toml theme={null} [versions] filekit = "0.17.0" [libraries] filekit-core = { module = "io.github.vinceglb:filekit-core", version.ref = "filekit" } ``` **Coil** ```kotlin build.gradle.kts theme={null} implementation("io.github.vinceglb:filekit-coil:0.17.0") ``` ```toml libs.versions.toml theme={null} [versions] filekit = "0.17.0" [libraries] filekit-coil = { module = "io.github.vinceglb:filekit-coil", version.ref = "filekit" } ``` See [Coil](/integrations/coil) to register `PlatformFile` support in your `ImageLoader`. ## Supported targets | Platform | Kotlin targets | | - | - | | Android | Android, minSdk 24 | | iOS | `iosArm64`, `iosSimulatorArm64` | | macOS | `macosArm64` | | JVM desktop | `jvm` (JVM 17) on Windows, macOS, and Linux | | Linux native | `linuxX64`, `linuxArm64` (core and dialogs) | | Windows native | `mingwX64` (core and dialogs) | | watchOS | `watchosArm64`, `watchosDeviceArm64`, `watchosSimulatorArm64` (core only) | | Web | `js`, `wasmJs` (browser) | Not every feature exists on every platform. Check the [platform support matrix](/platform-support) before calling an API from `commonMain`. ## Platform setup Most targets need no setup. Do these steps once per app: | Platform | `filekit-core` | `filekit-dialogs` | `filekit-dialogs-compose` | | - | - | - | - | | Android | Automatic (App Startup) | `FileKit.init(activity)` in your activity | Automatic for launchers. `FileKit.init(activity)` if you also call suspend dialog APIs | | JVM desktop | `FileKit.init(appId)` for app directories | None, plus `jdk.security.auth` on Linux | Same as dialogs | | Linux native | `FileKit.init(appId)` for app directories | None, plus `libdbus-1` | Not available | | Windows native | `FileKit.init(appId)` for app directories | None | Not available | | iOS, macOS, JS, Wasm | None | None | None | | watchOS | None | Not available | Not available | The setup pages explain each step: > **[Core setup](/core/setup)** > > Application ID, custom directories, ProGuard rules, and manual Android initialization. > **[Dialogs setup](/dialogs/setup)** > > Android activity registration, Linux packaging, and D-Bus on Linux native. > [!TIP] > > Some features need extra platform configuration, such as a `FileProvider` to share or open files on Android. Each feature page lists what it needs. ## Next steps * Follow the [Quickstart](/quickstart) to pick and read your first file. * Learn the [`PlatformFile`](/core/platform-file) API. * Set up your [AI assistant](/ai-assistants) with the FileKit docs. # Coil 3 Source: https://filekit.mintlify.app/integrations/coil Display a PlatformFile image with Coil 3 AsyncImage [See the platform support matrix](/platform-support#compose-and-coil) `filekit-coil` lets [Coil 3](https://coil-kt.github.io/coil/) load a `PlatformFile` directly, so you can pass a picked file to `AsyncImage` as its `model`. ## Usage ```kotlin build.gradle.kts theme={null} implementation("io.github.vinceglb:filekit-coil:0.17.0") ``` ```toml libs.versions.toml theme={null} [versions] filekit = "0.17.0" [libraries] filekit-coil = { module = "io.github.vinceglb:filekit-coil", version.ref = "filekit" } ``` Register `PlatformFile` support once, in your root composable: ```kotlin theme={null} import io.github.vinceglb.filekit.coil.addPlatformFileSupport @Composable fun App() { setSingletonImageLoaderFactory { context -> ImageLoader.Builder(context) .components { addPlatformFileSupport() } .build() } MaterialTheme { /* your content */ } } ``` Then use a `PlatformFile` as the model: ```kotlin theme={null} @Composable fun Gallery(files: List, modifier: Modifier = Modifier) { LazyColumn(modifier) { items(files) { file -> AsyncImage( model = file, contentDescription = file.name, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxWidth().height(200.dp), ) } } } ``` If you build your own `ImageLoader` instead of the singleton, call `addPlatformFileSupport()` in its `components` block. ## Load files that need security-scoped access Coil reads the file itself, without the security-scoped access FileKit starts around its own operations. Call `securelyAccessFile` from `onState` when the file comes from: * the file or directory picker on iOS, which return files outside your app's sandbox; * a [bookmark](/core/bookmark-data) restored on iOS, or on macOS (native or JVM) when the app is sandboxed. ```kotlin theme={null} import io.github.vinceglb.filekit.coil.securelyAccessFile AsyncImage( model = file, contentDescription = file.name, onState = { state -> state.securelyAccessFile(file) }, ) ``` It starts access when loading begins and stops it on success or error. You don't need it for gallery and camera results (on iOS, FileKit writes them into your app's temporary or cache folder), for files in your [app directories](/core/file-utils), or on Android and the web. Elsewhere it is a no-op, so calling it from common code is safe. ## Related * [Image and video utilities](/core/image-and-video-utils) to compress images or get an `ImageBitmap`. * [Gallery picker](/dialogs/gallery-picker) to let users pick photos. * [Coil documentation](https://coil-kt.github.io/coil/getting_started/) for transformations, placeholders, and caching. # Compose Media Player Source: https://filekit.mintlify.app/integrations/compose-media-player Play videos picked with FileKit using Compose Media Player [Compose Media Player](https://github.com/kdroidFilter/ComposeMediaPlayer) is a third-party video player for Compose Multiplatform. Its player state opens a FileKit `PlatformFile` directly, with no extra module. ## Usage ```kotlin build.gradle.kts theme={null} implementation("io.github.kdroidfilter:composemediaplayer:") ``` Pick a video with FileKit, then open it in the player: ```kotlin theme={null} scope.launch { val file = FileKit.openFilePicker(type = FileKitType.Video) ?: return@launch playerState.openFile(file) } ``` `playerState` is the library's video player state. See the [Compose Media Player README](https://github.com/kdroidFilter/ComposeMediaPlayer) for the current version, the player composable, and supported platforms. ## Related * [File picker](/dialogs/file-picker) for type filters and multiple selection. * [Gallery picker](/dialogs/gallery-picker) to pick videos from the photo library on Android and iOS. # Krop Source: https://filekit.mintlify.app/integrations/krop Crop images picked with FileKit using the Krop library [Krop](https://github.com/tamimattafi/krop) is a third-party image cropping library for Compose Multiplatform. Its FileKit extension turns a `PlatformFile` into a Krop image source. ![Krop cropping an image](https://raw.githubusercontent.com/tamimattafi/krop/refs/heads/main/art/preview.gif) ## Usage Add the Krop cropper UI and its FileKit extension: ```kotlin build.gradle.kts theme={null} implementation("com.attafitamim.krop:ui:") implementation("com.attafitamim.krop:extensions-filekit:") ``` Pick an image with FileKit, then hand it to Krop with `toImageSrc()`: ```kotlin theme={null} scope.launch { val file = FileKit.openFilePicker(type = FileKitType.Image) ?: return@launch val imageSrc = file.toImageSrc() // Pass imageSrc to Krop's image cropper } ``` See the [Krop README](https://github.com/tamimattafi/krop) for the cropper setup, supported platforms, and the current version. Once cropped, save the result with the [file saver](/dialogs/file-saver) or [compress it](/core/image-and-video-utils) first. # FileKit Source: https://filekit.mintlify.app/introduction Pick, save, read, and write files with one Kotlin Multiplatform API, using each platform's native dialogs. ![FileKit: file operations, pickers, and dialogs for Kotlin and Compose Multiplatform](https://mintcdn.com/filekit/O3A078ijpazSdC5J/brand/hero.webp?fit=max\&auto=format\&n=O3A078ijpazSdC5J\&q=85\&s=d598e56274ef4f253d09b5decb23f0c7) **FileKit is the file layer for Kotlin Multiplatform.** Open native file, photo, directory, and camera pickers, save and share files, and read or write them through a single `PlatformFile` type. The same code runs on Android, iOS, macOS, desktop JVM (Windows, macOS, Linux), Linux and Windows native, JS, and Wasm, and FileKit Core also runs on watchOS. ## FileKit in 30 seconds Let the user pick a photo, then show it or read its bytes. ```kotlin Compose theme={null} @Composable fun PhotoPicker() { val scope = rememberCoroutineScope() var photo by remember { mutableStateOf(null) } val launcher = rememberFilePickerLauncher( type = FileKitType.Image, onError = { failure -> println("Picker failed: ${failure.message}") }, ) { file -> // null when the user cancels if (file != null) scope.launch { photo = file.toImageBitmap() } } Button(onClick = { launcher.launch() }) { Text("Pick a photo") } photo?.let { Image(bitmap = it, contentDescription = null) } } ``` ```kotlin Suspend API theme={null} suspend fun importPhoto() { // Native picker on every platform; null when the user cancels val photo = FileKit.openFilePicker(type = FileKitType.Image) ?: return println("${photo.name}: ${photo.size()} bytes") // Read it, then upload it or store it val bytes: ByteArray = photo.readBytes() } ``` > **[Try it in your browser](/live-demo)** > > Run the FileKit sample app live, with no install. > **[Run it in two minutes](/quickstart)** > > Add one dependency, do the per-platform setup, and pick your first file. ## What you can build > **[Photo and file uploads](/dialogs/file-picker)** > > Open the native file picker or the Android and iOS photo picker, then read the bytes to upload them. > **[Export and save documents](/dialogs/file-saver)** > > Ask the user where to save a file, then write text, bytes, or a stream to it. > **[Work with project folders](/dialogs/directory-picker)** > > Let users pick a folder, then list, walk, and copy its contents. Keep access across launches with bookmarks. > **[Camera capture and photo processing](/dialogs/camera-picker)** > > Take a photo, compress or resize it, and save it to the user's gallery. > **[Share and open files](/dialogs/share-file)** > > Send a file to the system share sheet or open it in the default app. > **[Local app storage](/core/file-utils)** > > Store files in the app's files, cache, and databases directories. The same paths work with DataStore or Room. ## Platforms | Android | iOS | macOS | JVM desktop | Linux native | Windows native | JS | Wasm | watchOS | | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | Core only | Every target with `filekit-dialogs` uses the platform's native dialogs (watchOS has FileKit Core only). Some features are platform-specific: the camera and share sheet are mobile-only, the file saver is not available in browsers, and Compose and Coil support follows Compose Multiplatform's targets. See the [platform support matrix](/platform-support) for a feature-by-feature breakdown. ## Modules Add only the modules you need. Each module pulls in the ones it builds on. | Artifact | What it gives you | Add it when | | - | - | - | | `filekit-core` | `PlatformFile`, reading and writing, file system operations, app directories, image utilities, bookmarks | You work with files and don't need dialogs | | `filekit-dialogs` | Native file, gallery, directory, and camera pickers, plus the file saver, share, and open-file dialogs, as `suspend` functions | You open dialogs from shared code, ViewModels, or non-Compose UI | | `filekit-dialogs-compose` | `remember…Launcher` APIs and `PlatformFile.toImageBitmap()` | Your UI uses Compose Multiplatform | | `filekit-coil` | `PlatformFile` support in Coil 3 `AsyncImage` | You display images with Coil | ```mermaid theme={null} graph LR A[filekit-core] --> B[filekit-dialogs] B --> C[filekit-dialogs-compose] A --> D[filekit-coil] ``` ## Next steps > **[Quickstart](/quickstart)** > > Pick and read your first file. > **[Installation](/installation)** > > Dependencies and per-platform setup. > **[Platform support](/platform-support)** > > See which features each platform supports. > **[Use FileKit with AI assistants](/ai-assistants)** > > Give your coding assistant the FileKit docs through MCP, `llms.txt`, or Markdown. ## Who uses FileKit > The API just makes sense and I can just focus on my app instead of fighting with platform APIs ![](https://github.com/alexstyl.png?size=96) Alex Styl Composables [@alexstyl on X, Jun 2, 2025](https://x.com/alexstyl/status/1929504630490239214) > You literally saved me hours and hours of time 👍 ![](https://github.com/SimonSchubert.png?size=96) Simon Schubert Kai [@SimonSchubert on GitHub, Mar 15, 2026](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-16142366) > \[...] your documentation is outstanding. Finally a human friendly documentation 😅 ![](https://github.com/PavlosTze.png?size=96) Pavlos Tzegiannakis Tracker for Bluesky [@PavlosTze on GitHub, Mar 18, 2025](https://github.com/vinceglb/FileKit/issues/215#issuecomment-2734341542) See how each project uses FileKit in the [showcase](/showcase), and read more on the [wall of love](/wall-of-love). Browse the [sample apps](https://github.com/vinceglb/FileKit/tree/main/sample) to see FileKit running on each platform. # Try FileKit in your browser Source: https://filekit.mintlify.app/live-demo Run the FileKit sample app in your browser: file picker, directory picker, file saver and more The FileKit sample app is built with Compose Multiplatform and compiled to WebAssembly. It runs the same code as the Android, iOS and desktop samples, with FileKit handling every dialog through the browser APIs. [Open in a new tab ↗](https://vinceglb.github.io/FileKit/) > [!NOTE] > > The browser limits what a web page can do with files. File and directory pickers work in modern browsers. The camera picker and share sheet are Android and iOS only, and the file saver dialog is not available on the web, so the sample marks them as unsupported here. On the web, use [`FileKit.download()`](/core/web#download-files) to save files. Each page lists the supported platforms. ## Run it locally The source lives in [`sample/webApp`](https://github.com/vinceglb/FileKit/tree/main/sample/webApp) and [`sample/shared`](https://github.com/vinceglb/FileKit/tree/main/sample/shared). ```bash theme={null} ./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun ``` Add `?demo=file-picker` to the URL to open a screen directly. The available screens are `file-picker`, `gallery-picker`, `directory-picker`, `camera-picker`, `file-saver` and `share-file`. # Platform support Source: https://filekit.mintlify.app/platform-support Which FileKit features are available on Android, iOS, macOS, JVM desktop, Linux, Windows, watchOS, and the web FileKit gives you one API, but platforms differ: browsers can't write arbitrary files, desktops have no camera picker, and phones have no "Downloads" folder. This page lists what each target supports, based on the library's platform implementations. **Legend:** ✅ supported · ⚠️ supported with limitations (see the numbered notes) · ❌ not available > [!TIP] > > An API marked ❌ is usually not declared at all for that target, so calling it from `commonMain` won't compile. Call it from a source set that only includes supported targets. See [Use platform-specific APIs from shared code](#use-platform-specific-apis-from-shared-code). ## Targets per module | Module | Android | iOS | macOS | JVM desktop | Linux native | Windows native | watchOS | JS | Wasm | | - | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | | `filekit-core` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `filekit-dialogs` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | | `filekit-dialogs-compose` | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | | `filekit-coil` | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | | Platform | Kotlin targets | | - | - | | Android | Android library, minSdk 24 | | iOS | `iosArm64`, `iosSimulatorArm64` | | macOS | `macosArm64` | | JVM desktop | `jvm` (JVM 17) on Windows, macOS, and Linux | | Linux native | `linuxX64`, `linuxArm64` | | Windows native | `mingwX64` | | watchOS | `watchosArm64`, `watchosDeviceArm64`, `watchosSimulatorArm64` | | Web | `js` (browser), `wasmJs` (browser) | Intel-only Apple targets (`iosX64`, `macosX64`) are not published. ## FileKit Core Columns: **And** Android · **iOS** · **mac** macOS · **JVM** JVM desktop · **Lin** Linux native · **Win** Windows native · **watch** watchOS · **Web** JS and Wasm. | Feature | And | iOS | mac | JVM | Lin | Win | watch | Web | | - | :-: | :-: | :-: | :-: | :-: | :-: | :-: | :-: | | [`PlatformFile`](/core/platform-file) properties: name, extension, size, `isDirectory`, `lastModified` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ 1 | | Create from a path, `exists`, `absolutePath`, `/` operator | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | `mimeType()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [`readBytes`, `readString`](/core/read-file) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Streaming read: `source()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | [`write`, `writeString`](/core/write-file), `delete` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ 2 | | Streaming write: `sink()` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | [`copyTo`, `copyRecursively`, `createDirectories`](/core/file-operations) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | `atomicMove` | ⚠️ 3 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | `list`, `walk`, `sizeRecursively` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ 1 | | `createdAt()` | ✅ | ✅ | ✅ | ✅ | ⚠️ 4 | ✅ | ✅ | ❌ | | [`filesDir`, `cacheDir`, `databasesDir`](/core/file-utils) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | `downloadsDir`, `picturesDir`, `videosDir`, `musicDir`, `documentsDir` | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | | `tempDir` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | | [`compressImage`](/core/image-and-video-utils) | ✅ | ✅ | ⚠️ 5 | ✅ | ❌ 6 | ❌ 6 | ❌ 6 | ❌ | | `saveImageToGallery`, `saveVideoToGallery` | ✅ | ✅ | ⚠️ 7 | ⚠️ 7 | ⚠️ 7 | ⚠️ 7 | ❌ 6 | ❌ | | [Bookmark data](/core/bookmark-data) (persistent access) | ✅ | ✅ | ✅ | ⚠️ 8 | ⚠️ 8 | ⚠️ 8 | ✅ | ❌ | | `PlatformFile` serialization | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ 16 | | [`FileKit.download`](/core/web#download-files) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | | [Origin private file system (OPFS)](/core/web#store-files-in-the-origin-private-file-system) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ⚠️ 2 | ## FileKit Dialogs `filekit-dialogs` is not available on watchOS. Web means JS and Wasm. | Feature | Android | iOS | macOS | JVM | Linux native | Windows native | Web | | - | :-: | :-: | :-: | :-: | :-: | :-: | :-: | | [File picker](/dialogs/file-picker): single, multiple, extension filter | ✅ | ✅ | ✅ | ✅ 9 | ⚠️ 10 | ✅ | ⚠️ 11 | | [Gallery picker](/dialogs/gallery-picker): `Image`, `Video`, `ImageAndVideo` | ✅ Photo Picker | ✅ PHPicker | ⚠️ 12 | ⚠️ 12 | ⚠️ 12 | ⚠️ 12 | ⚠️ 12 | | [Directory picker](/dialogs/directory-picker) | ✅ | ✅ | ✅ | ✅ 9 | ⚠️ 10 | ✅ | ⚠️ 13 | | [File saver](/dialogs/file-saver) | ⚠️ 14 | ⚠️ 14 | ✅ | ✅ 9 | ⚠️ 10 | ✅ | ❌ use `FileKit.download` | | [Camera picker](/dialogs/camera-picker) (photo) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | [Share file](/dialogs/share-file) | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | [Open with default app](/dialogs/open-file) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | | [Dialog settings](/dialogs/dialog-settings) | None | ⚠️ 15 | ✅ | ✅ | Title only | Title only | None | ## Compose and Coil | Feature | Android | iOS | macOS | JVM | Web | | - | :-: | :-: | :-: | :-: | :-: | | `rememberFilePickerLauncher`, `rememberDirectoryPickerLauncher` | ✅ | ✅ | ✅ | ✅ | ✅ 13 | | `rememberFileSaverLauncher` | ✅ | ✅ | ✅ | ✅ | ❌ | | `rememberCameraPickerLauncher`, `rememberShareFileLauncher` | ✅ | ✅ | ❌ | ❌ | ❌ | | `PlatformFile.toImageBitmap()`, `ImageBitmap.encodeToByteArray()` | ✅ | ✅ | ✅ | ✅ | ✅ | | [Coil](/integrations/coil) `AsyncImage(model = platformFile)` | ✅ | ✅ | ✅ | ✅ | ✅ | Compose Multiplatform has no Linux native, Windows native, or watchOS targets, so `filekit-dialogs-compose` and `filekit-coil` don't either. On macOS native, Compose itself is experimental. ## Use platform-specific APIs from shared code Kotlin's default hierarchy gives you `appleMain`, `iosMain`, `nativeMain`, and `webMain`, but no source set for "everything except the web" or "Android and iOS". Create them in your shared module, following [Kotlin's guide to additional source sets](https://kotlinlang.org/docs/multiplatform-hierarchy.html#creating-additional-source-sets): ```kotlin build.gradle.kts theme={null} kotlin { applyDefaultHierarchyTemplate() sourceSets { // File saver, open with default app, file operations, app directories val nonWebMain by creating { dependsOn(commonMain.get()) } androidMain.get().dependsOn(nonWebMain) jvmMain.get().dependsOn(nonWebMain) iosMain.get().dependsOn(nonWebMain) // Camera picker, share sheet val mobileMain by creating { dependsOn(nonWebMain) } androidMain.get().dependsOn(mobileMain) iosMain.get().dependsOn(mobileMain) } } ``` Keep only the targets your project declares, and add others (such as `macosMain` or `linuxMain`) the same way. Kotlin doesn't support a source set shared only by Android and JVM: if those are your only non-web targets, call these APIs from `androidMain` and `jvmMain`, or wrap them in an `expect`/`actual` function. Code in `nonWebMain` can then call `FileKit.openFileSaver()`, and code in `mobileMain` can call `FileKit.openCameraPicker()`. ## Notes 1. **Web files are read-only browser handles.** Directories report a size of `0`. `list`, `walk`, and `sizeRecursively` work on folders returned by the directory picker. 2. **Web writing is experimental.** `write`, `writeString`, and `delete` work only on entries in the browser's origin private file system (OPFS) and require `@OptIn(ExperimentalFileKitApi::class)` and a secure context. Writing to a picked file throws. 3. **Android `content://` files:** `atomicMove` falls back to copy-then-delete and doesn't support directories. 4. **Linux native:** `createdAt()` always returns `null`. 5. **macOS native:** the output size of `compressImage` depends on the screen's backing scale factor. 6. **Throws at runtime** on these targets: `compressImage` on Linux native, Windows native, and watchOS; gallery saving on watchOS returns a failed `Result`. 7. **Desktop "gallery":** the image or video is written to the user's Pictures or Videos folder. 8. **Path-based bookmarks:** on Windows, Linux, and non-macOS JVM, bookmark data stores the file path. Access is not security-scoped, so the bookmark breaks if the file moves. 9. **JVM dialogs** use `NSOpenPanel`/`NSSavePanel` on macOS, `IFileDialog` on Windows, and the XDG Desktop Portal on Linux, falling back to AWT/Swing dialogs when no portal is running. 10. **Linux native dialogs** require a running XDG Desktop Portal (`org.freedesktop.portal.Desktop`, available on GNOME, KDE, and most modern desktops) and `libdbus-1`. There is no fallback dialog and no parent window. See [Dialogs setup](/dialogs/setup#linux-setup). 11. **Web picker** uses ``. The type filter is applied through the `accept` attribute; `directory` and dialog settings are ignored. 12. **Media types outside Android and iOS** open the regular file dialog filtered by extension: `png`, `jpg`, `jpeg`, `gif`, `bmp` for images and `mp4`, `mov`, `avi`, `mkv`, `webm` for videos. On the web, they filter with `image/*` and `video/*`. 13. **Web directory picker** returns a read-only tree of the selected folder. An empty folder returns `null`, like a cancellation. 14. **Mobile savers:** on Android, several `allowedExtensions` fall back to the `*/*` MIME type. On iOS, `allowedExtensions` is ignored. 15. **iOS dialog settings:** the presenter view controller and asset representation mode are applied; `title` and `canCreateDirectories` are currently ignored. 16. **Web serialization compiles but fails:** `PlatformFileSerializer` is declared for JS and Wasm, but serializing or deserializing a `PlatformFile` throws at runtime. Store something else, such as the file name or bytes. Something wrong or missing? [Open an issue](https://github.com/vinceglb/FileKit/issues) and include the platform and FileKit version. # Quickstart Source: https://filekit.mintlify.app/quickstart Pick, read, and save your first file with FileKit in a few minutes This guide opens a native file picker, reads the selected file, and saves a new file to a location the user chooses. > [!TIP] > > Want to see FileKit before installing it? [Try it in your browser](/live-demo): the FileKit sample app runs live, with no install. **1. Add the dependency** FileKit can helps you to display dialogs to the user like file, directory and photo pickers, save dialogs, camera and more. FileKit dialogs are available in two flavors: ```kotlin build.gradle.kts theme={null} // Enables FileKit dialogs without Compose dependencies implementation("io.github.vinceglb:filekit-dialogs:0.17.0") // Enables FileKit dialogs with Composable utilities implementation("io.github.vinceglb:filekit-dialogs-compose:0.17.0") ``` ```toml libs.versions.toml theme={null} [versions] filekit = "0.17.0" [libraries] filekit-dialogs = { module = "io.github.vinceglb:filekit-dialogs", version.ref = "filekit" } filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" } ``` Both artifacts include `filekit-core`. For Compose apps, add `filekit-dialogs-compose`. To call the dialogs from ViewModels or non-Compose code, `filekit-dialogs` is enough. See [Installation](/installation) for every module. **2. Initialize FileKit where needed** Most targets work without setup. Android (without Compose), desktop JVM, and Linux or Windows native need one line at startup. **Android** If you only use Compose launchers (`remember…Launcher`), nothing to do. If you call the suspend dialog APIs (`FileKit.openFilePicker()`…), register your activity so FileKit can launch dialogs: ```kotlin MainActivity.kt theme={null} class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) FileKit.init(this) } } ``` **JVM desktop** Give FileKit your application ID before showing UI. It is used for the app's files and cache directories. ```kotlin main.kt theme={null} fun main() { FileKit.init(appId = "com.example.myapp") application { Window(onCloseRequest = ::exitApplication) { App() } } } ``` On Linux, packaged apps also need the `jdk.security.auth` module. See [Dialogs setup](/dialogs/setup#linux-setup). **Linux & Windows native** ```kotlin main.kt theme={null} fun main() { FileKit.init(appId = "com.example.myapp") } ``` On Linux, dialogs use the XDG Desktop Portal and link against `libdbus-1`. See [Dialogs setup](/dialogs/setup#linux-setup). **iOS, macOS, JS, Wasm** Nothing to do. FileKit works out of the box. **3. Pick a file** ```kotlin Compose theme={null} val launcher = rememberFilePickerLauncher( onError = { failure -> println("Picker failed: ${failure.message}") }, ) { file -> // file is a PlatformFile, or null when the user cancelled println("Picked ${file?.name}") } Button(onClick = { launcher.launch() }) { Text("Pick a file") } ``` ```kotlin Suspend API theme={null} // Call from a coroutine: a ViewModel, a LaunchedEffect, a click handler with scope.launch... val file: PlatformFile? = FileKit.openFilePicker() println("Picked ${file?.name}") ``` Restrict the selection with `type = FileKitType.Image`, `FileKitType.Video`, or `FileKitType.File(extensions = listOf("pdf"))`. Pick several files with `mode = FileKitMode.Multiple()`. See [File picker](/dialogs/file-picker) for all options. **4. Read it** `PlatformFile` exposes the same read API on every platform. Read the file once the picker returns it: inside `onResult` (with `scope.launch`) in Compose, or after `openFilePicker()` returns a non-null file: ```kotlin theme={null} suspend fun readPicked(file: PlatformFile) { val name: String = file.name val size: Long = file.size() val text: String = file.readString() val bytes: ByteArray = file.readBytes() } ``` For an image in Compose, call `file.toImageBitmap()` or use [Coil](/integrations/coil). **5. Save a file** Ask the user where to save, then write to the returned file: ```kotlin Compose theme={null} val scope = rememberCoroutineScope() val saver = rememberFileSaverLauncher( dialogSettings = FileKitDialogSettings.createDefault(), onError = { failure -> println("Saver failed: ${failure.message}") }, ) { file -> scope.launch { file?.writeString("Hello FileKit!") } } Button(onClick = { saver.launch(suggestedName = "hello", defaultExtension = "txt") }) { Text("Save") } ``` ```kotlin Suspend API theme={null} val file = FileKit.openFileSaver(suggestedName = "hello", defaultExtension = "txt") file?.writeString("Hello FileKit!") ``` > [!NOTE] > > The file saver is available on Android, iOS, macOS, JVM, and Linux and Windows native, so call it from a non-web source set. Browsers don't let you choose a save location, so on JS and Wasm use `FileKit.download(bytes, fileName)` instead. See [Files on the web](/core/web#download-files). That's a complete round trip. FileKit uses each platform's native dialog, so the picker looks and behaves the way your users expect. ## Where to go next > **[File operations](/core/file-operations)** > > List, walk, copy, move, and delete files and folders. > **[Reading and writing](/core/write-file)** > > Text, bytes, and streaming with kotlinx-io for large files. > **[Gallery and camera](/dialogs/gallery-picker)** > > Native photo pickers on Android and iOS, plus camera capture. > **[Directories](/dialogs/directory-picker)** > > Let users pick a folder and keep access with bookmarks. > **[Image utilities](/core/image-and-video-utils)** > > Compress and resize images, and save them to the gallery. > **[Platform support](/platform-support)** > > Check which features each platform supports. # Roadmap to 1.0 Source: https://filekit.mintlify.app/roadmap Where FileKit is heading, what 1.0 will guarantee, and how you can help FileKit is already used in production on Android, iOS, desktop and the web. The goal of 1.0 is not a big list of new features. It is a **promise**: a reviewed public API, clear compatibility guarantees, predictable behavior across platforms, and documentation that tells you exactly what to expect. > [!NOTE] > > **Target: FileKit 1.0 in Q1 2027.** This is a best-effort target, not a fixed date. The order of the steps below matters more than the calendar, and this page is updated as work lands. ## At a glance | Step | Version | Focus | Can it break your code? | | - | - | - | - | | 1. Release 0.17 | 0.17 | Recursive file helpers, web storage, iOS fixes | No API removals. Android `minSdk` goes from 23 to 24 | | 2. API freeze | 0.18 | Final API review, deprecated APIs removed | **Yes**: deprecated APIs are removed, and the API review may change others, with migration notes | | 3. Release candidates | 1.0.0-RC | Reliability, documentation, your feedback | No removals. API changes only to fix problems found in the RCs | | 4. FileKit 1.0 | 1.0.0 | Stable release and stabilization period | No | | 5. After 1.0 | 1.x | New features and platforms | No breaks for stable APIs on stable platforms. Minimum Kotlin, SDK or JDK versions may rise | > [!TIP] > > **What should I do today?** > > * Move to 0.17 (plan for Android `minSdk` 24) and **fix your deprecation warnings**: deprecated APIs are removed in 0.18. > * Look for `@ExperimentalFileKitApi` warnings and opt-ins in your code: those APIs may still change. > * If a name, default or behavior feels wrong, [tell us now](https://github.com/vinceglb/FileKit/discussions). ## Guiding principle Removals and breaking changes to stable APIs happen **before** 1.0, with migration notes for every change. After 1.0, they wait for 2.0. Everything additive, like a new function, a new platform implementation or a new integration, doesn't break anyone. So it can ship in any 1.x release and doesn't need to block 1.0. ## The plan ### 1. Release 0.17 Adds recursive helpers (`walk()`, `sizeRecursively()`, `copyRecursively()`), an experimental Origin Private File System API on the web, and a fix for iOS folder access persisted across restarts. It also raises Android `minSdk` from 23 to 24. See the [releases](https://github.com/vinceglb/FileKit/releases) for details. ### 2. API freeze: 0.18 The last release before 1.0 that removes APIs. * **Deprecated APIs removed**, such as the old `downloadDir`, `pictureDir` and `videoDir` desktop properties and the file saver overloads that take `extension` instead of `defaultExtension`. * **Public API review** of `filekit-core`, `filekit-dialogs`, `filekit-dialogs-compose` and `filekit-coil`, against the [Kotlin library authors' guidelines](https://kotlinlang.org/docs/api-guidelines-introduction.html): naming, defaults, nullability, exceptions and visibility. If the review finds a better name, the old one is deprecated with an IDE replacement and keeps compiling until 2.0. * **Automatic API checks in CI**, so every change to the public API is visible in review and accidental breaking changes fail the build. * **Experimental APIs reviewed.** Each `@ExperimentalFileKitApi` declaration is either promoted to stable or explicitly stays experimental through 1.0. * **Compatibility policy** published in these docs (see "What 1.0 guarantees" below). ### 3. Release candidates: 1.0.0-RC No more removals: existing APIs stay as they were in 0.18, except to fix problems found in the RCs. The focus moves to reliability and documentation, with builds you can try in your app or library. * **Regression tests** where users report the most issues: Android content URIs and picker lifecycle, Apple security-scoped resources and bookmarks, desktop dialogs, and browser file handling. Manual verification steps are documented where system UI cannot be automated. * **Fixes for the highest-impact confirmed bugs** on stable platforms, for example missing dismissal events from some iOS pickers ([#138](https://github.com/vinceglb/FileKit/issues/138)). * **Platform support matrix**: every feature, on every platform, with its known limitations. * **1.0 migration guide** covering every breaking change in 0.18 and every API deprecated since 0.17. * **API reference** generated from KDoc and published next to these docs. These additions are targeted for the release candidates. They are additive, so if they are not ready by the last RC, they ship in a 1.x release instead: * **Native save dialog on the web** ([#348](https://github.com/vinceglb/FileKit/issues/348)), using the File System Access API where available, with the current download behavior as a fallback. * **Blob and W3C `File` interop on the web** ([#173](https://github.com/vinceglb/FileKit/issues/173)), so web code can move between browser types and `PlatformFile` without boilerplate. ### 4. FileKit 1.0 The stable release, with the guarantees below, followed by a stabilization period focused on regressions. ### 5. After 1.0: 1.x * **Chunked reading on the web**, for uploading or processing large files without loading them fully in memory. * **More consistent image processing** across platforms, for compression and resizing results that match more closely. * **Kotlin/Native targets promoted to stable** as their test coverage and real-world usage grow. `linuxX64` and `linuxArm64` are already published. The open part of [#609](https://github.com/vinceglb/FileKit/issues/609) is validation on distributions such as Aurora OS. * **More integrations and recipes** with popular Kotlin Multiplatform libraries. ## What 1.0 guarantees ### Platform tiers | Tier | Platforms | What it means | | - | - | - | | **Stable** | Android, iOS, JVM desktop (Windows, macOS, Linux), JS, Wasm | Covered by the compatibility policy below. Regressions are treated as bugs. | | **Experimental** | Kotlin/Native macOS (`macosArm64`), Linux (`linuxX64`, `linuxArm64`), Windows (`mingwX64`) and watchOS (`watchosArm64`, `watchosDeviceArm64`, `watchosSimulatorArm64`) | Published and built in CI, with tests on some targets, but not covered by the compatibility policy: APIs and behavior on these targets may change in minor releases. They can move to Stable in any 1.x release. | > [!NOTE] > > Compose Multiplatform desktop apps run on the JVM, so they are in the **Stable** tier on Windows, macOS and Linux. The Experimental tier only concerns apps compiled with Kotlin/Native for those systems. Not every module ships every Kotlin/Native target: watchOS is available in `filekit-core` only, and Linux and Windows native in `filekit-core` and `filekit-dialogs`. ### Compatibility policy From 1.0 on, FileKit follows [semantic versioning](https://semver.org/) for stable APIs on stable platforms: * **Patch releases** (1.x.y) contain bug fixes only. * **Minor releases** (1.x.0) add features. They don't change or remove existing stable APIs, and libraries built against an earlier 1.x keep working. * **Major releases** (2.0.0) are the only ones allowed to remove APIs or break source or binary compatibility. * **Deprecations come first.** An API that will go away is deprecated in a 1.x release, with a migration note and an IDE replacement when there is a mechanical one. Your code keeps compiling, and removal waits for the next major release. * **Checked automatically.** CI compares the public API with the previous release, so accidental breaking changes fail the build instead of reaching you. * **Fixes land in the latest 1.x release.** Older minor versions are not patched. ### Not covered * APIs marked with `@ExperimentalFileKitApi`. Using them shows a compiler warning until you opt in, and they may change in minor releases. * APIs marked with `@FileKitDialogsInternalApi`. They exist for coordination between FileKit modules and are not meant for apps. * Platforms in the Experimental tier. * **Toolchain requirements.** Minimum Kotlin, Compose Multiplatform, Android SDK or JDK versions may be raised in minor releases, to keep up with the ecosystem. Every bump is called out in the release notes. ## How to help > **[Review the API](https://github.com/vinceglb/FileKit/discussions)** > > If a name, default or behavior feels wrong, now is the time to say it. After 1.0 it gets much harder to change. > **[Report platform bugs](https://github.com/vinceglb/FileKit/issues)** > > Include the FileKit version, the platform and OS version, and a minimal reproduction. A sample project helps the most. > **[Try the release candidates](https://github.com/vinceglb/FileKit/releases)** > > Test the 1.0 RCs in your app or library when they land, and report anything that breaks or behaves differently from one platform to another. > **[Contribute](https://github.com/vinceglb/FileKit/issues?q=is%3Aissue+is%3Aopen+label%3A%22needs+help%22)** > > Issues labeled `needs help` are open for contributions. Comment on the issue first so we can agree on the approach. Using FileKit in production? [Tell us about it](https://github.com/vinceglb/FileKit/discussions/343). Knowing which platforms and features you rely on helps set priorities. # Who's using FileKit Source: https://filekit.mintlify.app/showcase Organizations, open source apps, and commercial products that use FileKit, each with a public source FileKit ships in apps on mobile, desktop, and the web. Each entry says how the project uses FileKit and links to a public source: the build file that declares the dependency or, for closed-source apps, the developer's own post. ## Trusted by > **Thunderbird** > > Thunderbird for Android and K-9 Mail use FileKit Core in their logging stack to write and export sync debug logs. Included from Thunderbird 26, currently in Daily builds. [Source](https://github.com/thunderbird/thunderbird-android/blob/main/app-common/build.gradle.kts) > **Leboncoin** > > Spark, Leboncoin's Android design system, builds its FileUpload components on FileKit pickers. [Source](https://github.com/leboncoin/spark-android/blob/main/spark/build.gradle.kts) > **Gradle** > > Gradle Client, a desktop app for the Gradle Tooling API, picks build directories with FileKit. [Source](https://github.com/gradle/gradle-client/blob/main/gradle-client/build.gradle.dcl) > **Blockstream Green** > > The Green Bitcoin wallet imports and exports PSBT files and shares files with FileKit, on Android and desktop. [Source](https://github.com/Blockstream/green_android/blob/master/data/build.gradle.kts) > **Square** > > `workflow-trace-viewer`, an internal desktop tool in the Workflow repository, loads trace files with the FileKit file picker. [Source](https://github.com/square/workflow-kotlin/blob/main/workflow-trace-viewer/build.gradle.kts) > **Android FHIR SDK** > > The project started by Google, now under the OHS Software Foundation, uses FileKit camera and file pickers for attachments in its in-progress multiplatform Structured Data Capture module. [Source](https://github.com/ohs-foundation/android-fhir/blob/master/datacapture-kmp/build.gradle.kts) > **DroidKaigi** > > The official DroidKaigi 2025 conference app picks profile images with FileKit and loads them with Coil. [Source](https://github.com/DroidKaigi/conference-app-2025/blob/main/feature/profile/build.gradle.kts) > **Mifos** > > Open source banking apps for Apache Fineract upload ID, KYC, and loan documents with FileKit: [Mifos Mobile](https://github.com/openMF/mifos-mobile/blob/dev/feature/auth/build.gradle.kts), [Mifos Pay](https://github.com/openMF/mifos-pay/blob/dev/feature/kyc/build.gradle.kts), and [Mifos X Field Officer](https://github.com/openMF/mifos-x-field-officer-app/blob/dev/feature/client/build.gradle.kts). > **OSS Review Toolkit** > > ORT Workbench, the desktop viewer for ORT results, opens result files and directories with FileKit dialogs. [Source](https://github.com/oss-review-toolkit/ort-workbench/blob/main/build.gradle.kts) > **Scanbot SDK** > > The official Kotlin Multiplatform example app for the Scanbot SDK picks images and PDFs with FileKit. [Source](https://github.com/doo/scanbot-sdk-example-kmp/blob/main/composeApp/build.gradle.kts) ## Open source apps Sorted by GitHub stars. The source link points to the build file that declares FileKit. | Project | Stars | How it uses FileKit | Source | | - | - | - | - | | [Spotube](https://github.com/team-spotube/spotube) | ![GitHub stars](https://img.shields.io/github/stars/team-spotube/spotube?style=flat) | Music streaming app. Its Kotlin Multiplatform rewrite picks the download and cache folders, playlist covers, and plugin files | [Source](https://github.com/team-spotube/spotube/blob/master/composeApp/build.gradle.kts) | | [Animeko](https://github.com/open-ani/animeko) | ![GitHub stars](https://img.shields.io/github/stars/open-ani/animeko?style=flat) | Anime tracking and streaming app: image attachments in comments, avatar upload, saving images, desktop cache folder | [Source](https://github.com/open-ani/animeko/blob/main/app/shared/application/build.gradle.kts) | | [AB Download Manager](https://github.com/amir1376/ab-download-manager) | ![GitHub stars](https://img.shields.io/github/stars/amir1376/ab-download-manager?style=flat) | Desktop download manager: download location folder picker and custom sound files | [Source](https://github.com/amir1376/ab-download-manager/blob/master/desktop/app/build.gradle.kts) | | [StreetComplete](https://github.com/streetcomplete/StreetComplete) | ![GitHub stars](https://img.shields.io/github/stars/streetcomplete/StreetComplete?style=flat) | OpenStreetMap editor for Android and iOS: note photos, image compression, sharing log files | [Source](https://github.com/streetcomplete/StreetComplete/blob/master/app/build.gradle.kts) | | [MuJing](https://github.com/tangshimin/MuJing) | ![GitHub stars](https://img.shields.io/github/stars/tangshimin/MuJing?style=flat) | Desktop app to learn English words from movies and documents: opens subtitle, video, and vocabulary files | [Source](https://github.com/tangshimin/MuJing/blob/master/build.gradle.kts) | | [AutoDev Xiuper](https://github.com/phodal/auto-dev) | ![GitHub stars](https://img.shields.io/github/stars/phodal/auto-dev?style=flat) | AI coding app for desktop, mobile, and web: file and directory pickers, web downloads | [Source](https://github.com/phodal/auto-dev/blob/master/mpp-ui/build.gradle.kts) | | [Keyguard](https://github.com/AChep/keyguard-app) | ![GitHub stars](https://img.shields.io/github/stars/AChep/keyguard-app?style=flat) | Bitwarden and KeePass client: open and save dialogs for vaults and attachments on desktop | [Source](https://github.com/AChep/keyguard-app/blob/master/common/build.gradle.kts) | | [CrossPaste](https://github.com/CrossPaste/crosspaste-desktop) | ![GitHub stars](https://img.shields.io/github/stars/CrossPaste/crosspaste-desktop?style=flat) | Cross-device clipboard sync: storage path, import, and export dialogs | [Source](https://github.com/CrossPaste/crosspaste-desktop/blob/main/app/build.gradle.kts) | | [Twine](https://github.com/msasikanth/twine) | ![GitHub stars](https://img.shields.io/github/stars/msasikanth/twine?style=flat) | Multiplatform RSS reader: OPML feed import and export | [Source](https://github.com/msasikanth/twine/blob/main/core/data/build.gradle.kts) | | [Husi](https://github.com/xchacha20-poly1305/husi) | ![GitHub stars](https://img.shields.io/github/stars/xchacha20-poly1305/husi?style=flat) | Multiplatform proxy tool: backup import and export, saving QR codes | [Source](https://github.com/xchacha20-poly1305/husi/blob/dev/composeApp/build.gradle.kts) | | [Bifrost](https://github.com/zacharee/Bifrost) | ![GitHub stars](https://img.shields.io/github/stars/zacharee/Bifrost?style=flat) | Samsung firmware downloader: file and folder pickers, save dialog | [Source](https://github.com/zacharee/Bifrost/blob/master/common/build.gradle.kts) | | [Tomato](https://github.com/nsh07/Tomato) | ![GitHub stars](https://img.shields.io/github/stars/nsh07/Tomato?style=flat) | Pomodoro timer: backup and restore files | [Source](https://github.com/nsh07/Tomato/blob/dev/shared/build.gradle.kts) | | [qBitController](https://github.com/Bartuzen/qBitController) | ![GitHub stars](https://img.shields.io/github/stars/Bartuzen/qBitController?style=flat) | qBittorrent remote control: adds torrent files and exports data | [Source](https://github.com/Bartuzen/qBitController/blob/master/composeApp/build.gradle.kts) | | [Kai](https://github.com/SimonSchubert/Kai) | ![GitHub stars](https://img.shields.io/github/stars/SimonSchubert/Kai?style=flat) | AI assistant on Android, iOS, desktop, and web: chat attachments, file export, sandbox file browser | [Source](https://github.com/SimonSchubert/Kai/blob/main/composeApp/build.gradle.kts) | | [PodAura](https://github.com/SkyD666/PodAura) | ![GitHub stars](https://img.shields.io/github/stars/SkyD666/PodAura?style=flat) | Podcast and RSS app: OPML import and export, media folders, sharing | [Source](https://github.com/SkyD666/PodAura/blob/master/shared/build.gradle.kts) | | [Grit](https://github.com/shub39/Grit) | ![GitHub stars](https://img.shields.io/github/stars/shub39/Grit?style=flat) | Habit tracker: backup export and restore | [Source](https://github.com/shub39/Grit/blob/master/shared/logic/build.gradle.kts) | | [Bangumi for CMP](https://github.com/xiaoyvyv/bangumi) | ![GitHub stars](https://img.shields.io/github/stars/xiaoyvyv/bangumi?style=flat) | Unofficial bangumi.tv client: image pickers and Coil loading | [Source](https://github.com/xiaoyvyv/bangumi/blob/main/buildSrc/src/main/kotlin/bgm.library.gradle.kts) | | [Rush](https://github.com/shub39/Rush) | ![GitHub stars](https://img.shields.io/github/stars/shub39/Rush?style=flat) | Lyrics search app: backups, background images, saving lyrics cards | [Source](https://github.com/shub39/Rush/blob/master/shared/ui/build.gradle.kts) | | [IReader](https://github.com/IReaderorg/IReader) | ![GitHub stars](https://img.shields.io/github/stars/IReaderorg/IReader?style=flat) | Novel reader: storage folder picker on desktop and iOS | [Source](https://github.com/IReaderorg/IReader/blob/master/presentation/build.gradle.kts) | | [Hammer Editor](https://github.com/Darkrock-Studios/hammer-editor) | ![GitHub stars](https://img.shields.io/github/stars/Darkrock-Studios/hammer-editor?style=flat) | Story and novel writing tool: imports, exports, project images | [Source](https://github.com/Darkrock-Studios/hammer-editor/blob/develop/composeUi/build.gradle.kts) | | [Edconv](https://github.com/edneyosf/Edconv) | ![GitHub stars](https://img.shields.io/github/stars/edneyosf/Edconv?style=flat) | FFmpeg interface for video and audio conversion: picks input media files | [Source](https://github.com/edneyosf/Edconv/blob/main/composeApp/build.gradle.kts) | | [kitshn](https://github.com/kitshn-app/kitshn) | ![GitHub stars](https://img.shields.io/github/stars/kitshn-app/kitshn?style=flat) | Tandoor recipes client for Android, iOS, and desktop: recipe photo picker | [Source](https://github.com/kitshn-app/kitshn/blob/main/shared/build.gradle.kts) | | [Morphe Desktop](https://github.com/MorpheApp/morphe-desktop) | ![GitHub stars](https://img.shields.io/github/stars/MorpheApp/morphe-desktop?style=flat) | Desktop APK patching tool: file and directory pickers | [Source](https://github.com/MorpheApp/morphe-desktop/blob/main/build.gradle.kts) | | [Heron](https://github.com/tunjid/heron) | ![GitHub stars](https://img.shields.io/github/stars/tunjid/heron?style=flat) | Bluesky client: media picker for posts and profiles, Coil previews, saving images to the gallery | [Source](https://github.com/tunjid/heron/blob/main/ui/media/build.gradle.kts) | | [Askimo](https://github.com/askimo-ai/askimo) | ![GitHub stars](https://img.shields.io/github/stars/askimo-ai/askimo?style=flat) | AI desktop app: file and folder pickers, file saver | [Source](https://github.com/askimo-ai/askimo/blob/main/desktop-shared/build.gradle.kts) | | [Komelia](https://github.com/Snd-R/Komelia) | ![GitHub stars](https://img.shields.io/github/stars/Snd-R/Komelia?style=flat) | Komga media client: poster uploads, fonts, offline downloads, drag and drop | [Source](https://github.com/Snd-R/Komelia/blob/main/komelia-ui/build.gradle.kts) | | [Flocon](https://github.com/openflocon/Flocon) | ![GitHub stars](https://img.shields.io/github/stars/openflocon/Flocon?style=flat) | Desktop debugging companion for Android and KMP apps: CSV, Markdown, and image exports | [Source](https://github.com/openflocon/Flocon/blob/main/FloconDesktop/composeApp/build.gradle.kts) | | [Pixelix](https://github.com/ghostbyte-dev/pixelix) | ![GitHub stars](https://img.shields.io/github/stars/ghostbyte-dev/pixelix?style=flat) | Pixelfed client: image and video picker in the post editor | [Source](https://github.com/ghostbyte-dev/pixelix/blob/dev/sharedUI/build.gradle.kts) | | [AndroidToolKit](https://github.com/LazyIonEs/AndroidToolKit) | ![GitHub stars](https://img.shields.io/github/stars/LazyIonEs/AndroidToolKit?style=flat) | Desktop tools for Android developers: file and directory pickers | [Source](https://github.com/LazyIonEs/AndroidToolKit/blob/main/shared/build.gradle.kts) | | [DevAnalyzer](https://github.com/Coding-Meet/DevAnalyzer) | ![GitHub stars](https://img.shields.io/github/stars/Coding-Meet/DevAnalyzer?style=flat) | Project analysis tool for developers: picks the project directory to analyze | [Source](https://github.com/Coding-Meet/DevAnalyzer/blob/main/composeApp/build.gradle.kts) | | [PixelSafe](https://github.com/StefanOltmann/pixelsafe) | ![GitHub stars](https://img.shields.io/github/stars/StefanOltmann/pixelsafe?style=flat) | Steganography tool for PNG images: picks the image and saves the result | [Source](https://github.com/StefanOltmann/pixelsafe/blob/main/app/build.gradle.kts) | | [Butler](https://github.com/HLCaptain/butler) | ![GitHub stars](https://img.shields.io/github/stars/HLCaptain/butler?style=flat) | AI chat app: picks chat attachments | [Source](https://github.com/HLCaptain/butler/blob/master/composeApp/feature/chat/build.gradle.kts) | | [Gluky](https://github.com/N7ghtm4r3/Gluky-Clients), [Refy](https://github.com/N7ghtm4r3/Refy-Clients), [Pandoro](https://github.com/N7ghtm4r3/Pandoro-Clients), [Neutron](https://github.com/N7ghtm4r3/Neutron-Clients) | | Self-hosted apps: profile pictures and image uploads | [Gluky](https://github.com/N7ghtm4r3/Gluky-Clients/blob/main/composeApp/build.gradle.kts), [Refy](https://github.com/N7ghtm4r3/Refy-Clients/blob/main/composeApp/build.gradle.kts), [Pandoro](https://github.com/N7ghtm4r3/Pandoro-Clients/blob/main/composeApp/build.gradle.kts), [Neutron](https://github.com/N7ghtm4r3/Neutron-Clients/blob/main/composeApp/build.gradle.kts) | ### Libraries | Library | Stars | How it uses FileKit | Source | | - | - | - | - | | [ComposeMediaPlayer](https://github.com/kdroidFilter/ComposeMediaPlayer) | ![GitHub stars](https://img.shields.io/github/stars/kdroidFilter/ComposeMediaPlayer?style=flat) | Media player for Compose Multiplatform: opens a `PlatformFile` in the player | [Source](https://github.com/kdroidFilter/ComposeMediaPlayer/blob/master/mediaplayer/build.gradle.kts) | | [kmpalette](https://github.com/jordond/kmpalette) | ![GitHub stars](https://img.shields.io/github/stars/jordond/kmpalette?style=flat) | Color palettes from images: builds a palette from a `PlatformFile` | [Source](https://github.com/jordond/kmpalette/blob/main/extensions-file/build.gradle.kts) | ## Commercial and closed-source apps The developers of these apps shared how they use FileKit in the [showcase discussion](https://github.com/vinceglb/FileKit/discussions/343). | App | How it uses FileKit | Source | | - | - | - | | [Ashampoo Photo Organizer](https://www.ashampoo.com/en-us/photo-organizer-free) | Photo management for large collections | [Discussion post](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13911389) | | [Splital](https://splital.com/) | Shared expenses app on [Android](https://play.google.com/store/apps/details?id=com.splital.project) and [iOS](https://apps.apple.com/us/app/splital-settle-up-easily/id6501984581): receipt and profile photos, image compression, PDF and CSV export | [Discussion post](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13923299) | | [Tracker for Bluesky](https://play.google.com/store/apps/details?id=com.bluesky.followers.analyzer) | Bluesky follower analytics on [Android](https://play.google.com/store/apps/details?id=com.bluesky.followers.analyzer) and [iOS](https://apps.apple.com/us/app/tracker-manager-for-bluesky/id6740998282): JSON export and import to move data between phones | [Discussion post](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13936833) | | [Parrot Workout Tracker](https://parrotworkouttracker.com/) | Fitness log on [Android](https://play.google.com/store/apps/details?id=de.kreiniker.parrotworkouttracker) and [iOS](https://apps.apple.com/us/app/workout-tracker-log-parrot/id6741729053): data and routine import and export, sharing workout images | [Discussion post](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13960874) | | [Authmeister](https://monoroh.com/authmeister) | OTP authenticator on [Android](https://play.google.com/store/apps/details?id=com.kopylovis.authmeister) and [iOS](https://apps.apple.com/us/app/authmeister/id6742833866) | [Discussion post](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-15861152) | | [Dancing Decibels](https://www.dancingdecibels.com) | Music festival app: artist image imports, schedule import and export | [Discussion post](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-15865180) | | [Composables SVG to Compose](https://composables.com/svg-to-compose) | Web tool to convert SVG to Compose code. Its Compose version used FileKit to pick files | [Discussion post](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13905300) | ## Add your project Using FileKit in your project? Share it in the [showcase discussion](https://github.com/vinceglb/FileKit/discussions/343) with a link to the code or a short note on how you use FileKit, and it can be added to this page. See what developers say on the [wall of love](/wall-of-love). # Wall of love Source: https://filekit.mintlify.app/wall-of-love What developers say about FileKit, quoted from public posts, issues, and discussions > 📁 Adding photo uploads, document attachments, or file exports to a Kotlin Multiplatform app? > > FileKit handles file picking, saving, and creation across platforms – from photos and documents to folders, media capture, and sharing. Kotlin by JetBrains Official Kotlin account [@kotlin on X, Sep 3, 2026](https://x.com/kotlin/status/2095550914991902856) > The API just makes sense and I can just focus on my app instead of fighting with platform APIs ![](https://github.com/alexstyl.png?size=96) Alex Styl Composables [@alexstyl on X, Jun 2, 2025](https://x.com/alexstyl/status/1929504630490239214) > Thank you for the project. You literally saved me hours and hours of time 👍 > > Happily using FileKit on multiplatform android. iOS, desktop and web in production ![](https://github.com/SimonSchubert.png?size=96) Simon Schubert Kai [@SimonSchubert on GitHub, Mar 15, 2026](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-16142366) > Currently this project is unmaintained. If you want an alternative I will recommend FileKit ![](https://github.com/shalva97.png?size=96) Giorgi Shalvashvili Co-maintainer of Compose Multiplatform File Picker [@shalva97 on GitHub, Jun 23, 2024](https://github.com/Wavesonics/compose-multiplatform-file-picker/commit/5ed5d20dde86f1b0067129f14273110fd7298e68) > It's been fantastic for helping people choose images to import for their artists and importing and exporting schedule content into the app. Thanks for making this and letting me focus on building the app instead of having to spend time dealing with the headaches of file management in a multi-platform setting. ![](https://github.com/babramovitch.png?size=96) Benjamin Abramovitch Dancing Decibels [@babramovitch on GitHub, Feb 19, 2026](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-15865180) > Thanks a lot Vincent. I also want to +1 on what Maurizio said above, you have done an excellent job and your documentation is outstanding. Finally a human friendly documentation 😅 ![](https://github.com/PavlosTze.png?size=96) Pavlos Tzegiannakis Tracker for Bluesky [@PavlosTze on GitHub, Mar 18, 2025](https://github.com/vinceglb/FileKit/issues/215#issuecomment-2734341542) > I use FileKit on Composables's SVG to Compose tool. Love how simple you made it to just pick files. > > It was particularly handy because I build my Compose apps on JVM and I needed the tool to be published on my site. Debugging apps on the web is a pain right now, so FileKit made it a breeze. Kudos! ![](https://github.com/alexstyl.png?size=96) Alex Styl Composables [@alexstyl on GitHub, Jul 28, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13905300) > I want to thank Vincent for the great work done with this library. It's just amazing. I've also appreciated the well-written doc of the new version. The migration from version 0.8.8 was very easy. Thank you! ![](https://github.com/mauriziofaleo.png?size=96) Maurizio Faleo Splital [@mauriziofaleo on GitHub, Mar 14, 2025](https://github.com/vinceglb/FileKit/issues/215#issuecomment-2725626597) > I use FileKit in PixelSafe, a Compose Multiplatform Steganography app. > > It works really well, so I will soon switch to it in Ashampoo Photo Organizer, which is a Compose Multiplatform photo management tool. > > EDIT: Migration is done for my Photo Organizer and the macOS version (using your lib) online. :) ![](https://github.com/StefanOltmann.png?size=96) Stefan Oltmann Ashampoo Photo Organizer and PixelSafe [@StefanOltmann on GitHub, Jul 28, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13911389) > Now it works like a charm on both platforms! \[...] Now with PlatformFile and .readBytes() it makes it easier to read csv file to String without writing platform-specific code. > > Thank you for your help and this library! It's inspired and gives future for kotlin/compose multiplatform! ![](https://github.com/brucemax.png?size=96) brucemax [@brucemax on GitHub, Feb 27, 2025](https://github.com/vinceglb/FileKit/issues/201#issuecomment-2689283732) > I’m using FileKit in AB Download Manager for letting users select download locations. > > the native file picker looks great! -- I really appreciate your work. Keep it up! ![](https://github.com/amir1376.png?size=96) AmirHossein Abdolmotallebi AB Download Manager [@amir1376 on GitHub, Oct 18, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-14714611) > Built with KMP and CMP, FileKit came handy in order to provide my users the ability to export some of their data as .json files and import them with your file picker, so that the users can easily move their data between phones. > > Thanks a lot for your work, everything is working perfectly so far! 👏 ![](https://github.com/PavlosTze.png?size=96) Pavlos Tzegiannakis Tracker for Bluesky [@PavlosTze on GitHub, Jul 30, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13936833) > FileKit is an external library made to apply file operations on Kotlin and KMP projects. This simplified the development process of allowing file selection Workflow trace viewer README in square/workflow-kotlin [GitHub, Aug 5, 2025](https://github.com/square/workflow-kotlin/blob/main/workflow-trace-viewer/README.md) > We've been using FileKit since February 2025, and our experience has been amazing! 👍🏻 > > Thank you for your work 🙏 ![](https://github.com/mauriziofaleo.png?size=96) Maurizio Faleo Splital [@mauriziofaleo on GitHub, Jul 29, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13923299) > OMG, that was fast and thorough, thank you so much! ![](https://github.com/janexner.png?size=96) Jan Exner [@janexner on GitHub, May 2, 2025](https://github.com/vinceglb/FileKit/issues/257#issuecomment-2847049236) > Your library enabled me to more quickly implement importing and exporting data, routines, and images of my workout completion screen so users can share their completed workout without having to take a screenshot. ![](https://github.com/coalacorey.png?size=96) Corey Kreiniker Parrot Workout Tracker [@coalacorey on GitHub, Aug 1, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13960874) > For me, FileKit meets all my needs for file selection, whether it is folder or file selection, it is very easy to use ![](https://github.com/LazyIonEs.png?size=96) LazyIonEs AndroidToolKit [@LazyIonEs on GitHub, Jul 29, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13916467) > Needless to say this is a fantastic library and I'm only filing a few bugs to help polish it :) > Thank you so much for your hard work. ![](https://github.com/licryle.png?size=96) Licryle [@licryle on GitHub, Sep 28, 2025](https://github.com/vinceglb/FileKit/issues/400) > Hello! First of all, I just want to say this is an awesome library, thank you for your great work! ![](https://github.com/ujizin.png?size=96) Lucas Yuji Yoshimine [@ujizin on GitHub, Feb 20, 2026](https://github.com/vinceglb/FileKit/issues/490) > Hi, thank you for creating this library. It's been very helpful in my project. I've implemented this feature as part of my work and wanted to port it back to the library. ![](https://github.com/sittisak-kij.png?size=96) sittisak-kij [@sittisak-kij on GitHub, Jul 27, 2025](https://github.com/vinceglb/FileKit/pull/340) > I use FileKit for Heron, an open source KMP bluesky client. Specifically Filekit is used for fetching images for previewing when uploading images or videos to bluesky. ![](https://github.com/tunjid.png?size=96) Adetunji Dahunsi Heron [@tunjid on GitHub, Jul 28, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13904385) > I use filekit on Twine to support importing and exporting OPML files for RSS feeds. ![](https://github.com/msasikanth.png?size=96) Sasikanth Twine [@msasikanth on GitHub, Jul 28, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-13905373) > The applications are many, but this library integrates perfectly in different contexts ;) ![](https://github.com/N7ghtm4r3.png?size=96) N7ghtm4r3 Gluky, Refy, Pandoro, and Neutron [@N7ghtm4r3 on GitHub, Dec 17, 2025](https://github.com/vinceglb/FileKit/discussions/343#discussioncomment-15279776) > Hi there, very useful library! ![](https://github.com/iamcalledrob.png?size=96) Rob Mason [@iamcalledrob on GitHub, May 13, 2024](https://github.com/vinceglb/FileKit/issues/18) > Hi! Thanks for the libray it's great to see more and more features coming to KMP and Compose Multiplaform! ![](https://github.com/alorma.png?size=96) Bernat Borrás Paronella [@alorma on GitHub, May 29, 2024](https://github.com/vinceglb/FileKit/issues/33) > Vincent Guillebaud brings us pickFile(), pickDirectory(), and saveFile() functions for Kotlin Multiplatform and Compose Multiplatform, providing a consistent bridge to platform-specific file and directory pickers. jetc.dev Jetpack Compose newsletter, issue #220 [jetc.dev, Jun 25, 2024](https://jetc.dev/issues/220.html) ## Share yours Built something with FileKit? Tell us in [GitHub Discussions](https://github.com/vinceglb/FileKit/discussions) or mention [@vinceglb](https://x.com/vinceglb) on X. To list your project, see the [showcase](/showcase).