DEV Community

Android Storage in 2026: Scoped Storage, SAF, and Why Filesystem Abstractions Still Matter

Android storage has changed dramatically. For a long time, Android developers could think about storage in familiar terms: val file = File("/some/path/file.txt") You had a path. You opened a stream. You copied a file. You created a directory. Simple. Modern Android is different. Today, an application may interact with application-private storage, shared media through MediaStore , user-selected documents through the Storage Access Framework (SAF), cloud-backed document providers, and traditional JVM filesystem APIs. The difficult part isn't learning any one of these APIs. The difficult part is building software that can work with different kinds of storage without forcing the entire application to understand the differences. That is the problem that led me to build a filesystem abstraction for Android and the JVM. The filesystem isn't always a filesystem anymore On the JVM, this is straightforward: val file = File("/home/user/documents/example.txt") file.inputStream().use { // read } The application has a path that refers to something on the filesystem. Android's Storage Access Framework changes that model. Instead of receiving a normal filesystem path, an application can receive something like: content://com.android.externalstorage.documents/tree/primary%3ADocuments That isn't a filesystem path. It is a URI representing a document-provider resource. You don't meaningfully resolve it with: File(uri.path) The operating system expects you to interact with it through APIs such as ContentResolver and DocumentsContract . This creates an important architectural distinction: Traditional filesystem Path โ†“ File โ†“ InputStream / OutputStream versus: Storage Access Framework URI โ†“ ContentResolver / DocumentsContract โ†“ Provider โ†“ InputStream / OutputStream The final result may still be a stream, but everything before that stream is fundamentally different. Scoped Storage solved one problem, but not every storage problem Scoped Storage was an important change in Android's storage model. For many applications, it makes storage much simpler. An application can use its own private directories and appropriate platform APIs without needing unrestricted access to the user's entire shared filesystem. For applications working with photos, videos, audio and other supported media, MediaStore can also provide an appropriate abstraction. And this is where an important point gets overlooked: Not every Android application needs SAF. If an application only needs its own application data, use application-private storage. If it needs to work with supported shared media, MediaStore may be the right solution. But there are applications where the requirement is different: "Let the user choose a directory and allow the application to work with files inside that directory." That's where SAF becomes particularly interesting. SAF isn't just a file picker One of the things that surprised me while working deeply with SAF is how easy it is to think of it as merely a mechanism for selecting a file. It is much more than that. With the appropriate user-granted permissions, an application can work with a selected document tree. That tree can represent storage managed by different document providers. And those providers don't necessarily have to represent the device's local storage. The underlying provider could potentially represent remote or cloud-backed storage. That means the application isn't necessarily interacting with: Android device โ†’ physical filesystem It can be closer to: Application โ†“ ContentResolver โ†“ Document Provider โ†“ Storage implementation The application doesn't need to know whether the provider ultimately represents local storage or another storage service. That's one of the most powerful ideas behind SAF. But this creates a problem for filesystem-oriented code Imagine a library with an API like: FileOperation("documents/example.txt") On the JVM, that's easy. But what does "documents/example.txt" mean on Android if the actual storage root is a SAF URI? Suppose the user selected: content://.../tree/primary%3ADocuments Now the library needs to interpret: documents/example.txt relative to that selected document tree. It cannot simply construct: content://.../documents/example.txt because SAF URIs aren't ordinary paths. The library has to resolve the path through the document provider. For example: Selected SAF root โ†“ Documents โ†“ projects โ†“ example.txt Each step represents a document-provider operation rather than a normal filesystem traversal. This is where filesystem abstraction becomes useful. Abstract the storage, not the application The goal isn't to pretend that SAF and the JVM filesystem are identical. They aren't. Trying to hide every difference can actually make an abstraction worse. Instead, the abstraction should hide the mechanics of accessing the storage while exposing operations that make sense to the application. For example: interface FileSystemUtil { fun read(path: String): Source fun write(path: String): Sink fun createFile(path: String) fun createDirectory(path: String) fun delete(path: String) fun copy(source: String, destination: String) } The JVM implementation can use: java.io.File while an Android SAF implementation can use: Uri ContentResolver DocumentsContract DocumentFile The application doesn't need to know which implementation is currently providing the storage. The interesting part: cross-filesystem operations This is where things become considerably more interesting. Consider: JVM filesystem โ†“ โ†“ copy โ†“ SAF The source might be: /home/user/file.zip while the destination might be: content://.../tree/... There is no useful way to make both sides behave like java.io.File . Instead, the operation has to understand both storage systems. Conceptually: JVM source โ†“ InputStream โ†“ buffer โ†“ OutputStream โ†“ SAF destination And the reverse is equally important: SAF source โ†“ InputStream โ†“ buffer โ†“ OutputStream โ†“ JVM destination This is why I eventually became less interested in simply wrapping File . The abstraction needs to represent storage operations, not just filesystem paths. Relative paths become interesting with SAF Another problem appears when working with a selected SAF tree. Suppose an application asks the user to select a root directory. The application can then operate relative to that root: selected root/ projects/ demo/ example.txt The application might want to say: createFile("projects/demo/example.txt") rather than repeatedly passing the root URI around. This creates a useful model: Selected root + Relative path โ†“ Resolved document The implementation can traverse the document tree and create missing directories where appropriate. That gives application code a filesystem-like experience without pretending the underlying storage is actually a filesystem. Why not just use DocumentFile everywhere? DocumentFile is extremely useful. It provides a convenient object-oriented API for working with document trees and makes traversal considerably easier. But abstraction layers also have to consider performance and API boundaries. There are situations where direct use of: DocumentsContract and: ContentResolver is more appropriate. In my implementation, I ended up using both approaches depending on the operation. DocumentFile is useful for traversal and convenient document operations. DocumentsContract provides lower-level access to document-provider operations. The important lesson is that a good abstraction doesn't necessarily mean choosing one Android API and hiding everything else behind it. It means choosing the appropriate mechanism for each operation. SAF also changes what a "path" means This is probably the biggest conceptual difference. On a traditional filesystem: /home/user/project/file.txt is a path. With SAF: content://provider/tree/... is an identifier for a resource managed by a provider. That distinction matters. A URI can contain information that looks path-like, but it shouldn't be treated as if it were a normal filesystem path. This is why APIs such as: File(uri.path) are fundamentally the wrong abstraction for SAF. A robust storage abstraction has to understand that there are different resource addressing models. What about cloud storage? This is another reason SAF is interesting. A document provider can abstract storage that isn't necessarily represented as a normal local filesystem. From the application's perspective, the interaction can still look like: open document read stream write stream create directory delete document The provider handles the underlying storage. This is one of the reasons I don't think of SAF simply as "Android's annoying file picker." It is closer to a provider-based storage interface. The application requests operations against documents. The provider determines how those documents are actually represented. So where does MediaStore fit? MediaStore and SAF solve different problems. If you're building an application that primarily works with shared photos, videos, audio or other supported media, MediaStore is often the appropriate API. SAF becomes more relevant when the application needs the user to choose arbitrary documents or directory trees and then work with them through the document-provider model. A modern Android storage architecture therefore isn't necessarily: File or: SAF or: MediaStore It is often: Application | +-----------+-----------+ | | | App storage MediaStore SAF | | | private shared documents | Document Provider The right choice depends on the application's requirements. Why I built a filesystem abstraction After working with these differences, I wanted application-level code to be able to perform filesystem-like operations without constantly knowing which storage mechanism was underneath. That led to my filesystem library. The idea is simple: FileSystems.current = JvmFileSystem() or: FileSystems.current = AndroidSafFileSystem(...) Then higher-level operations can work through the abstraction. The JVM implementation deals with traditional filesystem paths. T

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.