Library / SDK
anggrayudi/SimpleStorage avatar
anggrayudi/SimpleStorage

SimpleStorage replaces three Android file APIs with one

💾 Simplify Android Storage Access Framework for file management across API levels.

867 stars103 forksKotlinApache-2.0

At a glance

What is it?
SimpleStorage 3.x is a Kotlin library that hides the difference between SAF documents, MediaStore items and raw java.io.File behind a single StorageFile type. It is worth adopting for new Android storage work, and it is a poor fit for projects that cannot move to coroutines or to minSdk 23.
Who is it for?
Teams writing new Android storage code on API 23 and above should adopt SimpleStorage 3.x, because one StorageFile type removes the per-API branching that StorageFile.from and the suspend operations already absorb. Projects stuck below minSdk 23, unable to use coroutines, or still needing 2.x bugfixes should stay where they are, since the 2.x branch is closed for maintenance.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 45 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One file type standing in for SAF, MediaStore and java.io.File

Android storage access is fragmented by design. The Storage Access Framework exists to keep a user's files safe from malicious apps, and that protection tightens as the API level rises, so an app that talks to storage one way on API 23 often needs a different path on Android 10 and later, where the sample code points at java.io.File being deprecated. SimpleStorage targets that fragmentation directly. Version 3.0.0 introduced StorageFile, a single abstraction that wraps SAF's DocumentFile, MediaStore's MediaFile and java.io.File behind one interface, so calling code stops caring which world a file lives in. The library is written in Kotlin, is published under the Apache-2.0 license, and the 3.x line requires minSdk 23 and is compiled against API 37. Its whole surface is built around Kotlin coroutines, so any operation that touches storage is a suspend function rather than a blocking call or a callback. That constraint is the price of the design: the library fits applications already built on coroutines, and it does not fit ones that are not.

Adding the two artifacts to a Gradle build

SimpleStorage ships as two separate Maven Central artifacts, the core library and a Jetpack Compose companion. The build snippet in the project lists both, with X.Y.Z standing in for the release you pick:

groovy
implementation "com.anggrayudi:storage:X.Y.Z"

// For Jetpack Compose
implementation "com.anggrayudi:storage-compose:X.Y.Z"

The versions are not hardcoded in the snippet on purpose. The badge beside it points at the Maven Central listing for com.anggrayudi/storage, and the overview links out to the version pages for both artifacts on central.sonatype.com. If you want a SNAPSHOT build rather than a released one, the overview shows the extra repository line the root Gradle file needs:

groovy
allprojects {
    repositories {
        google()
        mavenCentral()
        // add this line
        maven { url "https://central.sonatype.com/repository/maven-snapshots/" }
    }
}

Two practical notes. First, pick a concrete version from Maven Central rather than leaving X.Y.Z in place, because the placeholder will not resolve. Second, decide up front whether you need the Compose artifact. The sample application in the repository has its own Gradle module, so it is the fastest way to see how the two artifacts are wired together for a Compose project.

Getting a StorageFile from the object you already hold

The entry point is a set of factory functions on StorageFile, and the point of them is that almost any reference you already have can be converted without special cases:

kotlin
// From whatever you already have:
val a = StorageFile.from(context, uri)                  // SAF, file://, or MediaStore URI
val b = StorageFile.from(context, File("/storage/emulated/0/Download/movie.mp4"))
val c = StorageFile.fromPath(context, "/storage/emulated/0/Download/movie.mp4")
val d = StorageFile.fromPath(context, StoragePath(storageId = "AAAA-BBBB", basePath = "Download/movie.mp4"))
val e = StorageFile.fromPublicDirectory(context, PublicDirectory.DOWNLOADS, "movie.mp4")

The same idea covers objects coming from the previous major version, through documentFile.toStorageFile(context) and mediaFile.toStorageFile(context). StorageFile keeps the Context inside itself, so no member function asks for one. The documented properties are name, mimeType, length, isDirectory, exists, lastModified, canRead, canWrite, plus list(), child("sub/file.txt"), openInputStream() and openOutputStream(). One trap is worth flagging early: absolutePath and path return null when the file has no resolvable physical path, where the 2.x API returned a confusing empty string. If your existing code assumes a non-empty path string, that assumption breaks on the move to 3.x. Escape hatches back to the underlying worlds are asDocumentFile(), asMediaFile() and asRawFile().

createFile and createFolder, and the MediaStore dead end

Creating content is where the abstraction leaks most visibly, and the README is upfront about it. The example shows a folder resolved by path, then a plain file, a file with subfolders in its name, and a folder:

kotlin
val folder = StorageFile.fromPath(context, StoragePath.primary("Documents"))!!

val report = folder.createFile("report.txt", "text/plain")          // report (1).txt if taken
val invoice = folder.createFile("invoices/2026/q3.pdf", "application/pdf")   // parents created
val archive = folder.createFolder("archive")

report?.openOutputStream()?.use { it.write("hello".toByteArray()) }

The name may carry subfolders. Missing intermediate directories are created, existing ones are reused. The CreateMode setting applies to the last segment only: CREATE_NEW, the default, keeps the existing file and writes report (1).txt beside it, REPLACE overwrites it, and REUSE returns the existing file untouched. The limitation is the return type. Both functions return null when the receiver is not a writable folder, and the README states that this is always the case for a MediaStore-backed StorageFile. So a StorageFile that resolved cleanly from a MediaStore URI can still be dead for creation work, and your code has to handle a null rather than treat it as an error condition.

Copy and move as suspend functions with a conflict lambda

Transfer operations were rebuilt around one result type and one optional callback block:

kotlin
lifecycleScope.launch {                       // main-safe: call from any dispatcher
  val result = file.copyTo(targetFolder) {    // this block is optional
    onConflict { ConflictResolution.REPLACE }
    onProgress { progressBar.progress = it.percent.toInt() }
    updateInterval = 250                      // ms between progress events
  }

The operations are named copyTo, moveTo, zipTo and unzipTo, and each returns a TransferResult that you branch on: Success, Skipped when the target already exists, and Failure carrying an error code and a cause. Conflict handling is a suspend lambda rather than a mode flag, so a decision about an existing target can be made inside the operation itself. The progress callback receives a percent value, and updateInterval sets the millisecond gap between progress events, with the first event arriving one interval later. When you need the complete event stream rather than a final answer, the library also offers a Flow<TransferEvent> form of each operation. Calls are main-safe and can be issued from any dispatcher, which is a meaningful difference from the older request-code and callback style.

StorageAccessManager replaces request codes and pickers

The third of the 3.x ideas is a single access entry point. StorageAccessManager takes SAF grants and pickers and turns them into plain suspend calls, with no request codes, no onActivityResult and no callbacks. That removes a category of plumbing from an app, and it removes the kind of state a user can break by rotating the screen mid-flow. The overview frames the redesign as three ideas: one file type, one operation vocabulary and one access entry point, and StorageAccessManager is the third. The repository also carries a JAVA_COMPATIBILITY.md document at the top level, alongside MIGRATION.md, README-2.x.md and V3_TEST_CASES.md, so the Java interop story is written down somewhere even though the core API is Kotlin-first. Projects on the older API can keep using it: the 2.x API keeps working during the 3.x cycle, and the sample module in the repository shows the newer usage patterns.

The 2.x branch is closed and the release cadence is visible

The maintenance picture is unusually explicit. The 2.x API remains functional, but the 2.x branch is closed for maintenance, and no bugfix releases will be cut from it, so fixes land in 3.x only. The overview encourages moving to v3 and points at MIGRATION.md for the steps. That is worth weighing against the recent release list: 3.0.0 on 2026-08-12, 3.0.1 on 2026-08-13, and 3.1.0 on 2026-09-08, with the last push to the repository on 2026-09-08. The repository is not archived, and the last push is recent enough to suggest the work is still moving, but the version numbering is the part to plan around, because a 2.x app that stays on 2.x will not receive corrections. Other limits are fixed by the design: minSdk 23, Kotlin coroutines everywhere, and the MediaStore creation dead end described earlier. Apache-2.0 governs reuse, including the obligations attached to redistribution, and that is a licensing question for your own counsel rather than something the library settles.

Editorial conclusion

Teams writing new Android storage code on API 23 and above should adopt SimpleStorage 3.x, because one StorageFile type removes the per-API branching that StorageFile.from and the suspend operations already absorb. Projects stuck below minSdk 23, unable to use coroutines, or still needing 2.x bugfixes should stay where they are, since the 2.x branch is closed for maintenance. Before migrating, check the MIGRATION.md guide, confirm the newest published version on Maven Central, and verify that no code path assumes a non-null absolutePath, since that property returns null whenever the file has no resolvable physical path.

Frequently asked questions

How do I add SimpleStorage to an Android project?

Add the core artifact com.anggrayudi:storage to your Gradle dependencies, and add com.anggrayudi:storage-compose as well if you use Jetpack Compose. The snippet in the project uses X.Y.Z as a placeholder, so take the concrete version from the Maven Central listing linked in the overview.

What Android SDK level does SimpleStorage 3.x require?

Version 3.x requires minSdk 23 and is compiled against API 37. It also requires Kotlin coroutines, since every long-running operation is a suspend function.

How do I copy a file with SimpleStorage?

Call copyTo inside a coroutine, optionally passing a block with onConflict, onProgress and updateInterval, and branch on the returned TransferResult for Success, Skipped or Failure. A Flow<TransferEvent> variant is available when you need the full event stream.

Why does createFile return null for my file?

createFile and createFolder return null when the receiver is not a writable folder, and the project states that a MediaStore-backed StorageFile is always in that situation. Check whether the StorageFile you resolved can act as a writable parent directory before writing to it.

Can I keep using the SimpleStorage 2.x API during the 3.x cycle?

Yes, the 2.x API keeps working, but the 2.x branch is closed for maintenance and no bugfix releases will be cut from it, so fixes land in 3.x only. The repository includes MIGRATION.md and README-2.x.md to support the move.

Which underlying file types does StorageFile wrap?

StorageFile wraps SAF's DocumentFile, MediaStore's MediaFile and java.io.File behind one interface. You can convert back with asDocumentFile(), asMediaFile() and asRawFile() when you need the original object.

Official sources

  1. anggrayudi/SimpleStorage on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/anggrayudi-simplestorage.svg)](https://hysenlabs.com/projects/anggrayudi-simplestorage)

Was this useful?