DEV Community

Sifiso Fakude
Sifiso Fakude

Posted on

Android Storage Is More Complicated Than File Paths

A single API for working with Android SAF, regular filesystems, and other platforms.

If you've worked with Android storage, you probably know that working with files isn't always as simple as creating a File object and calling mkdirs().

On the JVM, you might use java.io.File. On Android, you might need the Storage Access Framework (SAF), DocumentFile, or DocumentsContract. If your application needs to work with user-selected directories, the implementation becomes even more complicated.

Now imagine maintaining all of that logic across multiple platforms.

That's one of the problems I wanted to address with filesystem, a Kotlin Multiplatform library that provides a unified API for filesystem operations.

The Problem With Android Storage

Consider a simple task: create a file at projects/compiler/output.txt, including any missing parent directories.

With a conventional filesystem, the implementation might look like this:

val file = File("projects/compiler/output.txt")

file.parentFile?.mkdirs()
file.createNewFile()
Enter fullscreen mode Exit fullscreen mode

That's manageable.

But what happens when the destination is inside a directory selected by the user through SAF?

You can't simply treat a content:// URI as a regular filesystem path.

Depending on the operation, you may need to work with ContentResolver, DocumentsContract, document IDs, and Android's document-provider APIs. You also need to account for the capabilities of the selected provider.

The problem becomes more noticeable when you need to support operations such as:

Creating files and directories.

Resolving relative paths.

Traversing directory trees.

Copying or moving files.

Reading and writing streams.

Working with metadata.

Handling both ordinary filesystem paths and SAF URIs.

Each operation can introduce another layer of platform-specific code.

And SAF isn't limited to local storage. A document provider may expose files from cloud services, remote storage, or other document sources.

The challenge isn't merely accessing a file. It's providing consistent file operations across different storage systems.

What If You Could Use One API?

That's the idea behind filesystem.

The library provides a common abstraction for filesystem operations while allowing platform-specific implementations to handle the underlying storage mechanisms.

Instead of scattering storage-specific logic throughout your application, you work through a consistent API.

For example:

val file = FileOperation("projects/compiler/output.txt")

file.createNewFile()
Enter fullscreen mode Exit fullscreen mode

The important detail is that createNewFile() can create missing parent directories as part of the operation.

You don't need to manually resolve the parent, create its directories, and then create the file yourself.

The library's goal is to keep application code focused on what it needs to do rather than how each storage system implements the operation.

Beyond java.io.File

You might reasonably ask: why not use java.io.File everywhere?

For ordinary JVM files, java.io.File remains useful. The problem arises when your application needs to work with storage that doesn't behave like an ordinary filesystem.

A SAF document is not necessarily backed by a conventional file on disk. A document provider may expose remote content, and its operations may have different capabilities from those of a local filesystem.

Simply wrapping a SAF URI in a File object doesn't turn it into a regular file.

You need an abstraction that understands the storage system behind the operation.

That's where filesystem comes in.

A Consistent API Across Platforms

filesystem is built with Kotlin Multiplatform.

The project targets:

JVM

Android

iOS

The intention is to let application code reuse the same filesystem API while platform-specific implementations handle the differences underneath.

On the JVM, operations can use conventional filesystem functionality.

On Android, the library can work with ordinary paths as well as SAF-backed storage.

On iOS, the platform implementation provides the corresponding filesystem functionality.

This doesn't mean every storage provider supports every operation identically. Different platforms and document providers have different capabilities. The abstraction exists to reduce the amount of platform-specific logic application developers need to write.

For applications that already use Kotlin Multiplatform, that can make filesystem access easier to organize and maintain.

Working With Metadata

File operations aren't limited to creating files or reading their contents.

Applications often need to inspect names, extensions, sizes, and other properties.

For example:

val file = FileOperation("documents/report.pdf")

println(file.name)
println(file.nameWithoutExtension)
println(file.extension)
println(file.length)
Enter fullscreen mode Exit fullscreen mode

A consistent API for these operations helps keep application logic separate from the details of the underlying storage implementation.

This becomes especially useful in applications such as file managers, archive tools, document processors, and development utilities.

Why I Built It

I built filesystem to address the complexity of working with different storage systems through a consistent API.

Android storage is an important part of that motivation, but the problem isn't exclusive to Android.

Applications increasingly need to work with different kinds of storage without tying every operation to a specific platform API.

I wanted an abstraction that could accommodate ordinary filesystem paths, Android's Storage Access Framework, and other supported platforms without requiring the entire application to be rewritten for each environment.

The goal isn't to replace every platform API. Those APIs remain necessary for platform-specific capabilities and permissions.

Instead, the goal is to reduce repetitive storage-handling code and provide a consistent foundation for applications that need filesystem operations.

Who Might Find It Useful?

filesystem may be useful if you're building:

Android applications that work with both regular paths and user-selected document trees.

Kotlin Multiplatform applications that need a shared filesystem API.

File managers that create, inspect, copy, or move files.

Archive utilities that traverse directories and process files.

Development tools that need to resolve paths and manipulate project files.

Document-processing applications that work with different storage sources.

If your application only reads one fixed file from internal storage, a dedicated abstraction may be unnecessary.

But if storage operations are spread throughout your application, or you need to support different storage mechanisms, centralizing that logic can make the codebase easier to maintain.

Try It

filesystem 1.0 is available on Maven Central.

Explore the source code, installation instructions, and usage examples on GitHub:

GitHub: https://github.com/sifisofakude/filesystem

The project is open source, and feedback, issues, and contributions are welcome.

Final Thoughts

File access looks simple until your application needs to work across different storage systems.

On Android, the distinction between conventional filesystem paths and SAF-backed documents makes that especially apparent. Add cross-platform requirements, and the amount of platform-specific code can grow quickly.

I built filesystem to make those operations easier to manage through a consistent Kotlin API.

Different storage systems. Different platforms. One filesystem API.

Top comments (0)