|
| 1 | +# Migrating from SimpleStorage 2.x to 3.0 |
| 2 | + |
| 3 | +Version 3.0 introduces a single abstraction over Android's three file worlds and one vocabulary |
| 4 | +for every long-running operation. The 2.x API keeps compiling throughout the 3.x cycle (parts of |
| 5 | +it as `@Deprecated`), so you can migrate incrementally. |
| 6 | + |
| 7 | +Platform changes: |
| 8 | + |
| 9 | +| | 2.x | 3.0 | |
| 10 | +|---|---|---| |
| 11 | +| minSdk | 23 | **26** | |
| 12 | +| compileSdk / targetSdk | 36 | **37** (Android 17) | |
| 13 | +| AGP / Gradle (to build this repo) | 8.13 / 8.14 | 9.2 / 9.4 | |
| 14 | + |
| 15 | +## The one-minute overview |
| 16 | + |
| 17 | +```kotlin |
| 18 | +// 2.x |
| 19 | +ioScope.launch { |
| 20 | + file.copyFileTo(context, targetFolder, |
| 21 | + onConflict = object : SingleFileConflictCallback<DocumentFile>(uiScope) { |
| 22 | + override fun onFileConflict(destFile: DocumentFile, action: FileConflictAction) { |
| 23 | + action.confirmResolution(ConflictResolution.REPLACE) |
| 24 | + } |
| 25 | + } |
| 26 | + ).collect { result -> when (result) { /* 8 branches */ } } |
| 27 | +} |
| 28 | + |
| 29 | +// 3.0 — from any thread, no scope juggling |
| 30 | +val result = file.copyTo(targetFolder) { |
| 31 | + onConflict { ConflictResolution.REPLACE } |
| 32 | + onProgress { progressBar.progress = it.percent.toInt() } |
| 33 | +} |
| 34 | +when (result) { |
| 35 | + is TransferResult.Success -> toast("Copied ${result.result.name}") |
| 36 | + is TransferResult.Failure -> log(result.errorCode, result.cause) |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +## API mapping |
| 41 | + |
| 42 | +### Obtaining files |
| 43 | + |
| 44 | +| 2.x | 3.0 | |
| 45 | +|---|---| |
| 46 | +| `DocumentFileCompat.fromUri(context, uri)` | `StorageFile.from(context, uri)` | |
| 47 | +| `DocumentFileCompat.fromFile(context, file)` | `StorageFile.from(context, file)` | |
| 48 | +| `DocumentFileCompat.fromFullPath(context, path)` | `StorageFile.fromPath(context, absolutePath)` | |
| 49 | +| `DocumentFileCompat.fromSimplePath(context, storageId, basePath)` | `StorageFile.fromPath(context, StoragePath(storageId, basePath))` | |
| 50 | +| `DocumentFileCompat.fromPublicFolder(context, type)` | `StorageFile.fromPublicDirectory(context, type)` | |
| 51 | +| `MediaStoreCompat.fromMediaId(context, ...)` → `MediaFile` | `StorageFile.from(context, mediaUri)` | |
| 52 | +| `FileFullPath(context, storageId, basePath)` | `StoragePath(storageId, basePath)` — no `Context` needed | |
| 53 | + |
| 54 | +`StorageFile` holds its `Context` internally: none of its members ask for one. `absolutePath` |
| 55 | +returns `null` (not `""`) when a physical path cannot be resolved. Escape hatches: |
| 56 | +`asDocumentFile()`, `asMediaFile()`, `asRawFile()`. |
| 57 | + |
| 58 | +### File operations |
| 59 | + |
| 60 | +| 2.x | 3.0 one-shot | 3.0 Flow | |
| 61 | +|---|---|---| |
| 62 | +| `DocumentFile.copyFileTo(context, target, …)` | `StorageFile.copyTo(target) { }` | `copyToAsFlow(target)` | |
| 63 | +| `DocumentFile.moveFileTo(context, target, …)` | `StorageFile.moveTo(target) { }` | `moveToAsFlow(target)` | |
| 64 | +| `DocumentFile.copyFolderTo/moveFolderTo(…)` | same `copyTo`/`moveTo` — folders are detected | same | |
| 65 | +| `List<DocumentFile>.compressToZip(context, zip, …)` | `List<StorageFile>.zipTo(zipFile) { }` | `zipToAsFlow(zipFile)` | |
| 66 | +| `DocumentFile.decompressZip(context, folder, …)` | `StorageFile.unzipTo(folder) { }` | `unzipToAsFlow(folder)` | |
| 67 | +| `DocumentFile.deleteRecursively(context)` | `StorageFile.deleteRecursively()` (suspend) | — | |
| 68 | +| `DocumentFile.search(…)` | — | `StorageFile.search(…)` | |
| 69 | + |
| 70 | +Options that used to be positional parameters (`updateInterval`, `skipEmptyFiles`, |
| 71 | +`fileDescription`, space checking) now live in the `TransferSpec` lambda. |
| 72 | + |
| 73 | +### Results |
| 74 | + |
| 75 | +| 2.x | 3.0 | |
| 76 | +|---|---| |
| 77 | +| `SingleFileResult` / `SingleFolderResult` / `MultipleFilesResult` / `ZipCompressionResult` / `ZipDecompressionResult` | `TransferEvent` (`PhaseChanged`, `Progress`, `Completed`) | |
| 78 | +| `...Result.Completed(result: Any)` + casting | `TransferResult.Success<StorageFile>` — typed | |
| 79 | +| `...Result.Error(errorCode, message, cause)` | `TransferResult.Failure(errorCode, message, cause, partialStats)` | |
| 80 | +| `writeSpeed: Int` (bytes per update interval) | `Progress.bytesPerSecond: Long` | |
| 81 | + |
| 82 | +### Conflict handling |
| 83 | + |
| 84 | +| 2.x | 3.0 | |
| 85 | +|---|---| |
| 86 | +| `object : SingleFileConflictCallback<DocumentFile>(uiScope) { override fun onFileConflict(...) { action.confirmResolution(...) } }` | `onConflict { conflict -> ConflictResolution.REPLACE }` | |
| 87 | +| `SingleFolderConflictCallback.onParentConflict/onContentConflict` | same single resolver — receives `Conflict.TargetFolder(canMerge)` first, then a `Conflict.TargetFile` per conflicting child | |
| 88 | + |
| 89 | +The resolver is a `suspend` function: show a dialog with |
| 90 | +`withContext(Dispatchers.Main) { … }` and simply return the answer. There is no `uiScope`, no |
| 91 | +`GlobalScope` default, and no zombie-thread hazard. |
| 92 | + |
| 93 | +### Storage access & pickers (Views) |
| 94 | + |
| 95 | +| 2.x (`@Deprecated`) | 3.0 | |
| 96 | +|---|---| |
| 97 | +| `SimpleStorageHelper(activity)` + 4 callbacks + `onSaveInstanceState` + `onActivityResult` | `StorageAccessManager(activity)` — suspend functions, nothing to forward | |
| 98 | +| `helper.requestStorageAccess()` + `onStorageAccessGranted` | `val access = manager.ensureAccess(StoragePath.primary("Documents"))` | |
| 99 | +| `helper.openFolderPicker()` + `onFolderSelected` | `val result = manager.pickFolder()` | |
| 100 | +| `helper.openFilePicker()` + `onFileSelected` | `val result = manager.pickFiles(allowMultiple = true)` | |
| 101 | +| `helper.createFile(mimeType)` + `onFileCreated` | `val result = manager.createFile(mimeType)` | |
| 102 | +| — | `manager.pickMedia()` — system Photo Picker, no permission needed | |
| 103 | + |
| 104 | +`StorageAccessManager` has no built-in dialogs: `ensureAccess` returns `WrongRootSelected` and you |
| 105 | +decide how to explain and retry. If you want ready-made dialogs, keep using `SimpleStorageHelper` |
| 106 | +until you migrate. |
| 107 | + |
| 108 | +### Compose |
| 109 | + |
| 110 | +Existing `rememberLauncherFor*` composables are unchanged. New in 3.0: |
| 111 | +`rememberLauncherForMediaPicker(maxItems) { files -> … }` for the system Photo Picker. |
| 112 | + |
| 113 | +## Deprecation timeline |
| 114 | + |
| 115 | +| Phase | What happens | |
| 116 | +|---|---| |
| 117 | +| 3.0.0-alpha | `SimpleStorage`, `SimpleStorageHelper`, and the picker/access callback interfaces are `@Deprecated` | |
| 118 | +| 3.0.0-rc | `DocumentFile`/`MediaFile` operation extensions become `@Deprecated`, delegating to the v3 engine | |
| 119 | +| 4.0 | Deprecated 2.x API is removed | |
0 commit comments