|
| 1 | +<div align="center"> |
| 2 | + <img src="https://github.com/vinceglb/FileKit/assets/24540801/5ad7fc2d-04fd-4ba7-b5de-b4a7ada753c9" alt="FileKit for Kotlin Multiplatform and Compose Multiplatform" /> |
| 3 | + |
| 4 | + <br> |
| 5 | + |
| 6 | + <h1>FileKit</h1> |
| 7 | + <p>Files, Medias, Folder Picker and File saver library for Kotlin Multiplatform and Compose Multiplatform</p> |
| 8 | + |
| 9 | + <div> |
| 10 | + <img src="https://img.shields.io/maven-central/v/io.github.vinceglb/filekit-core" alt="FileKit Kotlin Maven Version" /> |
| 11 | + <img src="https://img.shields.io/badge/Platform-Android-brightgreen.svg?logo=android" alt="Badge Android" /> |
| 12 | + <img src="https://img.shields.io/badge/Platform-iOS%20%2F%20macOS-lightgrey.svg?logo=apple" alt="Badge iOS" /> |
| 13 | + <img src="https://img.shields.io/badge/Platform-JVM-8A2BE2.svg?logo=openjdk" alt="Badge JVM" /> |
| 14 | + <img src="https://img.shields.io/badge/Platform-WASM%20%2F%20JS-yellow.svg?logo=javascript" alt="Badge JS" /> |
| 15 | + </div> |
| 16 | + |
| 17 | + <br> |
| 18 | +</div> |
| 19 | + |
| 20 | +FileKit is a library that allows you to pick and save files in a simple way. On each platform, it uses the native file picker API to provide a consistent experience. |
| 21 | + |
| 22 | +## 🚀 Quick Start |
| 23 | + |
| 24 | +Pick a file, a directory or save a file in common code: |
| 25 | + |
| 26 | +```kotlin |
| 27 | +// Pick a file |
| 28 | +val file = FileKit.pickFile() |
| 29 | + |
| 30 | +// Pick a directory |
| 31 | +val directory = FileKit.pickDirectory() |
| 32 | + |
| 33 | +// Save a file |
| 34 | +val file = FileKit.saveFile( |
| 35 | + extension = "txt", |
| 36 | + bytes = "Hello, World!".encodeToByteArray() |
| 37 | +) |
| 38 | +``` |
| 39 | + |
| 40 | +Get file information in common code: |
| 41 | + |
| 42 | +```kotlin |
| 43 | +val filePath = file?.path |
| 44 | +val fileName = file?.name |
| 45 | +val bytes = file?.readBytes() |
| 46 | +``` |
| 47 | + |
| 48 | +Compose Multiplatform integration made simple: |
| 49 | + |
| 50 | +```kotlin |
| 51 | +// Pick files from Compose |
| 52 | +val launcher = rememberFilePickerLauncher(mode = PickerMode.Multiple()) { files -> |
| 53 | + // Handle picked files |
| 54 | +} |
| 55 | + |
| 56 | +// Use the pickerLauncher |
| 57 | +Button(onClick = { launcher.launch() }) { |
| 58 | + Text("Pick files") |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | + |
| 63 | + |
| 64 | +## 📦 Installation |
| 65 | + |
| 66 | +```gradle |
| 67 | +repositories { |
| 68 | + mavenCentral() |
| 69 | +} |
| 70 | +
|
| 71 | +dependencies { |
| 72 | + // Enables FileKit without Compose dependencies |
| 73 | + implementation("io.github.vinceglb:filekit-core:0.8.8") |
| 74 | +
|
| 75 | + // Enables FileKit with Composable utilities |
| 76 | + implementation("io.github.vinceglb:filekit-compose:0.8.8") |
| 77 | +} |
| 78 | +``` |
| 79 | + |
| 80 | +If using JVM target and Linux distribution, you need to add the following module: |
| 81 | + |
| 82 | +```gradle |
| 83 | +compose.desktop { |
| 84 | + application { |
| 85 | + nativeDistributions { |
| 86 | + linux { |
| 87 | + modules("jdk.security.auth") |
| 88 | + } |
| 89 | + } |
| 90 | + } |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +## ⚡ Initialization |
| 95 | + |
| 96 | +Using **FileKit Core methods on Android** requires an initialization: |
| 97 | + - `FileKit.pickFile()` |
| 98 | + - `FileKit.pickDirectory()` |
| 99 | + - `FileKit.saveFile()` |
| 100 | + |
| 101 | +In this case, only if using Android, you need to initialize FileKit in your `ComponentActivity`: |
| 102 | + |
| 103 | +```kotlin |
| 104 | +// MainActivity.kt |
| 105 | +class MainActivity : ComponentActivity() { |
| 106 | + override fun onCreate() { |
| 107 | + super.onCreate() |
| 108 | + FileKit.init(this) |
| 109 | + } |
| 110 | +} |
| 111 | +``` |
| 112 | + |
| 113 | +In all other cases, you can use FileKit without initialization. |
| 114 | + |
| 115 | +## 📄 File Picker |
| 116 | + |
| 117 | +### Picker types |
| 118 | + |
| 119 | +You can pick different types of files with `PickerType`: |
| 120 | +- `Image`: Pick an image file. |
| 121 | +- `Video`: Pick a video file. |
| 122 | +- `ImageAndVideo`: Pick an image or a video file. |
| 123 | +- `File`: Pick any file. It is the default type. It's possible to specify a list of extensions. |
| 124 | + |
| 125 | +```kotlin |
| 126 | +val imageType = PickerType.Image |
| 127 | +val videoType = PickerType.Video |
| 128 | +val imageAndVideoType = PickerType.ImageAndVideo |
| 129 | +val fileType = PickerType.File(extensions = listOf("pdf", "docx")) |
| 130 | +``` |
| 131 | + |
| 132 | +### Picker modes |
| 133 | + |
| 134 | +You can pick files in different modes with `PickerMode`. The mode will change the output type. `Single` is the default mode. |
| 135 | + |
| 136 | +```kotlin |
| 137 | +val singleMode = PickerMode.Single |
| 138 | +val multipleMode = PickerMode.Multiple() |
| 139 | +``` |
| 140 | + |
| 141 | +#### Max items |
| 142 | + |
| 143 | +On Android and iOS, when using `PickerType` `Image`, `Video` or `ImageAndVideo`, we can use `PickerMode.Multiple(maxItems = X)` to limit the number of picked files. The value must be between 1 and 50. Default value is `null` (no limit). |
| 144 | + |
| 145 | +### Launch the picker |
| 146 | + |
| 147 | +You can launch the picker with `FileKit.pickFile` or `rememberFilePickerLauncher`: |
| 148 | + |
| 149 | +```kotlin |
| 150 | +// FileKit Core |
| 151 | +val file = FileKit.pickFile( |
| 152 | + type = PickerType.Image, |
| 153 | + mode = PickerMode.Single, |
| 154 | + title = "Pick an image", |
| 155 | + initialDirectory = "/custom/initial/path" |
| 156 | +) |
| 157 | + |
| 158 | +// FileKit Compose |
| 159 | +val launcher = rememberFilePickerLauncher( |
| 160 | + type = PickerType.ImageAndVideo, |
| 161 | + mode = PickerMode.Multiple(), |
| 162 | + title = "Pick a media", |
| 163 | + initialDirectory = "/custom/initial/path" |
| 164 | +) { files -> |
| 165 | + // Handle the picked files |
| 166 | +} |
| 167 | +launcher.launch() |
| 168 | +``` |
| 169 | + |
| 170 | +## 📁 Directory Picker |
| 171 | + |
| 172 | +You can pick a directory with `FileKit.pickDirectory` or `rememberDirectoryPickerLauncher`: |
| 173 | + |
| 174 | +```kotlin |
| 175 | +// FileKit Core |
| 176 | +val directory = FileKit.pickDirectory( |
| 177 | + title = "Pick a directory", |
| 178 | + initialDirectory = "/custom/initial/path" |
| 179 | +) |
| 180 | + |
| 181 | +// FileKit Compose |
| 182 | +val launcher = rememberDirectoryPickerLauncher( |
| 183 | + title = "Pick a directory", |
| 184 | + initialDirectory = "/custom/initial/path" |
| 185 | +) { directory -> |
| 186 | + // Handle the picked directory |
| 187 | +} |
| 188 | +launcher.launch() |
| 189 | +``` |
| 190 | + |
| 191 | +The directory picker is available on all platforms, expect for WASM / JS. To check if the directory picker is available from the common code, you can use `FileKit.isDirectoryPickerSupported()`. |
| 192 | + |
| 193 | +```kotlin |
| 194 | +val directoryModeSupported = FileKit.isDirectoryPickerSupported() |
| 195 | +``` |
| 196 | + |
| 197 | +## 💾 Save File Picker |
| 198 | + |
| 199 | +You can save a file with `FileKit.saveFile` or `rememberFileSaverLauncher`: |
| 200 | + |
| 201 | +```kotlin |
| 202 | +// FileKit Core |
| 203 | +val file = FileKit.saveFile( |
| 204 | + baseName = "myTextFile", |
| 205 | + extension = "txt", |
| 206 | + initialDirectory = "/custom/initial/path", |
| 207 | + bytes = "Hello, World!".encodeToByteArray() |
| 208 | +) |
| 209 | + |
| 210 | +// FileKit Compose |
| 211 | +val launcher = rememberFileSaverLauncher() { file -> |
| 212 | + // Handle the saved file |
| 213 | +} |
| 214 | +launcher.launch( |
| 215 | + baseName = "myTextFile", |
| 216 | + extension = "txt", |
| 217 | + initialDirectory = "/custom/initial/path", |
| 218 | + bytes = "Hello, World!".encodeToByteArray() |
| 219 | +) |
| 220 | +``` |
| 221 | + |
| 222 | +### Optional bytes argument |
| 223 | + |
| 224 | +Bytes argument is optional. If you don't provide it, the file will be empty. This feature is available on Android, iOS, macOS and JVM. It is not available on WASM / JS. |
| 225 | + |
| 226 | +To check if it's possible to save a file without bytes from the common code, you can use: |
| 227 | + |
| 228 | +```kotlin |
| 229 | +val isSupported: Boolean = FileKit.isSaveFileWithoutBytesSupported() |
| 230 | +``` |
| 231 | + |
| 232 | +## 🧑💻 PlatformFile and PlatformDirectory |
| 233 | + |
| 234 | +The `PlatformFile` and `PlatformDirectory` classes are wrappers around the platform file system. It allows you to get the file name, path and read the file content in common code. |
| 235 | + |
| 236 | +```kotlin |
| 237 | +val platformFile: PlatformFile = ... |
| 238 | + |
| 239 | +val filePath: String? = platformFile.path |
| 240 | +val fileName: String = platformFile.name // Base name with extension |
| 241 | +val baseName: String = platformFile.baseName |
| 242 | +val extension: String = platformFile.extension |
| 243 | +val size: Long = platformFile.getSize() |
| 244 | +val bytes: ByteArray = platformFile.readBytes() // suspend function |
| 245 | + |
| 246 | +val platformDirectory: PlatformDirectory = ... |
| 247 | +val directoryPath: String? = platformDirectory.path |
| 248 | +``` |
| 249 | + |
| 250 | +On each platform, you can get the original platform file: |
| 251 | + |
| 252 | +```kotlin |
| 253 | +// Android |
| 254 | +val uri: Uri = platformFile.uri |
| 255 | +val uri: Uri = platformDirectory.uri |
| 256 | + |
| 257 | +// iOS / macOS |
| 258 | +val nsUrl: NSURL = platformFile.nsUrl |
| 259 | +val nsUrl: NSURL = platformDirectory.nsUrl |
| 260 | + |
| 261 | +// JVM |
| 262 | +val file: java.io.File = platformFile.file |
| 263 | +val file: java.io.File = platformDirectory.file |
| 264 | + |
| 265 | +// WASM / JS |
| 266 | +val file: org.w3c.files.File = platformFile.file |
| 267 | +val file: org.w3c.files.File = // PlatformDirectory not supported on WASM / JS |
| 268 | +``` |
| 269 | + |
| 270 | +## 🕺 Going further with files |
| 271 | + |
| 272 | +[KMPFile](https://github.com/zacharee/KMPFile) is a library built by [@zacharee](https://github.com/zacharee) that provides a common API to work with files in Kotlin Multiplatform. |
| 273 | + |
| 274 | +It mimics the Java File API available on Android, JVM, iOS and macOS. It's a great companion to FileKit to work with files in a consistent way across all platforms. |
| 275 | + |
| 276 | +Also, KMPFile provides built-in support for FileKit's `PlatformFile` and `PlatformDirectory` classes thanks to [a dedicated library](https://github.com/zacharee/KMPFile?tab=readme-ov-file#picking-files). |
| 277 | + |
| 278 | +Get started with KMPFile by visiting this repository: https://github.com/zacharee/KMPFile |
| 279 | + |
| 280 | +## 🤏 Proguard & obfuscation |
| 281 | + |
| 282 | +If using Proguard or obfuscation on JVM, you need to add the following rules: |
| 283 | + |
| 284 | +```proguard |
| 285 | +-keep class com.sun.jna.** { *; } |
| 286 | +-keep class * implements com.sun.jna.** { *; } |
| 287 | +``` |
| 288 | + |
| 289 | +## 🌱 Sample projects |
| 290 | + |
| 291 | +You can find 2 sample projects in the `samples` directory: |
| 292 | +- `sample-core`: A Kotlin Multiplatform project using FileKit in a shared viewModel targeting Android, JVM, WASM, JS, iOS Swift, macOS Swift and iOS Compose. |
| 293 | +- `sample-compose`: A Compose Multiplatform project using FileKit in a Composable targeting Android, iOS, JVM, WASM, |
| 294 | + |
| 295 | +## ✨ Behind the scene |
| 296 | + |
| 297 | +FileKit uses the native file picker API on each platform: |
| 298 | + |
| 299 | +- On Android, it uses `PickVisualMedia`, `OpenDocument` and `OpenDocumentTree` contracts. |
| 300 | +- On iOS, it uses both `UIDocumentPickerViewController` and `PHPickerViewController` APIs. |
| 301 | +- On macOS, it uses the `NSOpenPanel` API. |
| 302 | +- On JVM, it uses JNA to access the file system on Windows and macOS and XDG Desktop Portal on Linux. |
| 303 | +- On WASM / JS, it uses the `input` element with the `file` type. |
| 304 | + |
| 305 | +Also, FileKit uses the bear minimum of dependencies to be as lightweight as possible. |
| 306 | + |
| 307 | +FileKit Core uses the following libraries: |
| 308 | +- [KotlinX Coroutines](https://github.com/Kotlin/kotlinx.coroutines) |
| 309 | +- Only Android: [AndroidX Activity KTX](https://developer.android.com/jetpack/androidx/releases/activity) |
| 310 | +- Only JVM: [Java Native Access - JNA](https://github.com/java-native-access/jna/tree/master) |
| 311 | +- Only JVM: [XDG Desktop Portal](https://github.com/hypfvieh/dbus-java) |
| 312 | + |
| 313 | +FileKit Compose uses the following libraries: |
| 314 | +- [Jetbrains Compose Runtime](https://github.com/JetBrains/compose-multiplatform) |
| 315 | +- Only Android: [AndroidX Activity Compose](https://developer.android.com/jetpack/androidx/releases/activity) |
| 316 | + |
| 317 | +## 😎 Credits |
| 318 | + |
| 319 | +FileKit is inspired by the following libraries: |
| 320 | + |
| 321 | +- [compose-multiplatform-file-picker](https://github.com/Wavesonics/compose-multiplatform-file-picker) |
| 322 | +- [peekaboo](https://github.com/onseok/peekaboo) |
| 323 | +- [Calf](https://github.com/MohamedRejeb/Calf) |
| 324 | +- [jnafilechooser](https://github.com/steos/jnafilechooser) |
| 325 | +- [swing-jnafilechooser](https://github.com/DJ-Raven/swing-jnafilechooser) |
| 326 | +- [nativefiledialog](https://github.com/mlabbe/nativefiledialog) |
| 327 | +- [IFileDialogImp](https://github.com/dbwiddis/IFileDialogImp) |
| 328 | +- [IntelliJ Community Foundation](https://github.com/JetBrains/intellij-community/blob/master/platform/util/ui/src/com/intellij/ui/mac/foundation/Foundation.java) |
| 329 | +- [file_picker (flutter)](https://pub.dev/packages/file_picker) |
| 330 | + |
| 331 | +--- |
| 332 | + |
| 333 | +Made with ❤️ by Vince |
0 commit comments