<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sifiso Fakude</title>
    <description>The latest articles on DEV Community by Sifiso Fakude (@slambyte).</description>
    <link>https://dev.to/slambyte</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4083495%2Ff4852a62-2f6f-4d10-a9db-c86456472579.jpg</url>
      <title>DEV Community: Sifiso Fakude</title>
      <link>https://dev.to/slambyte</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/slambyte"/>
    <language>en</language>
    <item>
      <title>Building a Plug-and-Play JVM Compiler for Android and Desktop with Bytesmith</title>
      <dc:creator>Sifiso Fakude</dc:creator>
      <pubDate>Mon, 24 Aug 2026 03:41:37 +0000</pubDate>
      <link>https://dev.to/slambyte/building-a-plug-and-play-jvm-compiler-for-android-and-desktop-with-bytesmith-oeo</link>
      <guid>https://dev.to/slambyte/building-a-plug-and-play-jvm-compiler-for-android-and-desktop-with-bytesmith-oeo</guid>
      <description>&lt;p&gt;What if adding Kotlin and Java compilation to your application didn't mean building an entire compilation pipeline yourself?&lt;/p&gt;

&lt;p&gt;What if you could add Bytesmith, configure the filesystem once, provide your source files and output destination, and simply compile?&lt;/p&gt;

&lt;p&gt;That's the idea behind &lt;strong&gt;Bytesmith&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Bytesmith is a Kotlin and Java compiler toolkit designed for JVM and Android applications. It provides a unified API for Kotlin, Java, and mixed-language compilation, while also supporting filesystem abstraction, custom classpaths, boot classpaths, compiler plugins, packaging, and diagnostics.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Configure the environment, provide the source, specify the output, and compile.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Compiler tooling can become surprisingly difficult when it is tightly coupled to the environment in which it was originally designed to run.&lt;/p&gt;

&lt;p&gt;You might need to deal with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kotlin compiler versions&lt;/li&gt;
&lt;li&gt;Kotlin standard libraries&lt;/li&gt;
&lt;li&gt;Java compilation&lt;/li&gt;
&lt;li&gt;Bootclasspath configuration&lt;/li&gt;
&lt;li&gt;Dependency classpaths&lt;/li&gt;
&lt;li&gt;Source discovery&lt;/li&gt;
&lt;li&gt;Output handling&lt;/li&gt;
&lt;li&gt;Android storage&lt;/li&gt;
&lt;li&gt;Storage Access Framework URIs&lt;/li&gt;
&lt;li&gt;Packaging&lt;/li&gt;
&lt;li&gt;Compiler diagnostics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then there is the question of where those files actually live.&lt;/p&gt;

&lt;p&gt;On a desktop JVM, you might have traditional filesystem paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/user/project/src/Main.kt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Android, you might be working with application storage or files selected through the Storage Access Framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your compiler API directly depends on &lt;code&gt;java.io.File&lt;/code&gt;, your compilation code becomes coupled to one filesystem model.&lt;/p&gt;

&lt;p&gt;Bytesmith takes a different approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Bytesmith
&lt;/h2&gt;

&lt;p&gt;The goal is to make compilation something you can plug into an application.&lt;/p&gt;

&lt;p&gt;With Gradle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.sifisofakude.bytesmith:bytesmith-common:1.0.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After adding Bytesmith, configure the filesystem your application wants to use.&lt;/p&gt;

&lt;p&gt;For a JVM application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileSystems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JvmFileSystem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileSystems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AndroidSafFileSystem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the filesystem is configured, the rest of the compilation layer can operate through the filesystem abstraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic compilation example
&lt;/h2&gt;

&lt;p&gt;Bytesmith provides a high-level compilation entry point.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;JvmFileSystem&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"-d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"build/classes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"src/main/java"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"src/main/kotlin"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is that you provide the filesystem and compilation arguments.&lt;/p&gt;

&lt;p&gt;Bytesmith handles the compiler orchestration.&lt;/p&gt;

&lt;p&gt;You don't need to build an entire compilation pipeline yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling Kotlin
&lt;/h2&gt;

&lt;p&gt;Bytesmith also exposes the Kotlin compiler directly when you need more control.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KotlinCompiler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JvmFileSystem&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;outputDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"build/classes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;kotlinSources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kotlinSources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;javaSources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;classpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;bootClasspath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;warningsAsErrors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives your application control over the compilation environment while still allowing Bytesmith to handle the underlying compiler integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiling Java
&lt;/h2&gt;

&lt;p&gt;Bytesmith isn't just a Kotlin compiler wrapper.&lt;/p&gt;

&lt;p&gt;Java compilation is supported through ECJ.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaCompiler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JvmFileSystem&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;outputDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"build/classes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;javaSources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;javaSources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;classpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;bootClasspath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;warningsAsErrors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means an application can use Bytesmith for Java compilation without implementing its own ECJ integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixed Kotlin and Java projects
&lt;/h2&gt;

&lt;p&gt;Bytesmith can also compile projects containing both Kotlin and Java source files.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
└── main/
    ├── kotlin/
    │   └── Example.kt
    └── java/
        └── JavaExample.java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can provide both types of source files to the compilation process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;sources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"src/main/kotlin/Example.kt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"src/main/java/JavaExample.java"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Kotlin compiler and Java compiler are orchestrated as part of the compilation process.&lt;/p&gt;

&lt;p&gt;This is useful for applications that generate source code, developer tools, code editors, custom build systems, or other environments where compilation itself is an application feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The filesystem abstraction
&lt;/h2&gt;

&lt;p&gt;This is one of the most important parts of Bytesmith.&lt;/p&gt;

&lt;p&gt;A compiler shouldn't necessarily care where a source file physically comes from.&lt;/p&gt;

&lt;p&gt;A desktop application might use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/user/project/Main.kt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An Android application might use application storage.&lt;/p&gt;

&lt;p&gt;Or the source might come from a user-selected SAF document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are fundamentally different storage mechanisms.&lt;/p&gt;

&lt;p&gt;But the compilation operation doesn't need to be.&lt;/p&gt;

&lt;p&gt;The application chooses the filesystem implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileSystems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JvmFileSystem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileSystems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AndroidSafFileSystem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bytesmith then works through the filesystem abstraction.&lt;/p&gt;

&lt;p&gt;The architecture looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;             Application
                  |
                  v
          FileSystems.current
                  |
        +---------+---------+
        |                   |
        v                   v
 JvmFileSystem       AndroidSafFileSystem
        |                   |
        +---------+---------+
                  |
                  v
              Bytesmith
                  |
          +-------+-------+
          |               |
          v               v
     Kotlin compiler     ECJ
          |               |
          +-------+-------+
                  |
                  v
              Output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler doesn't have to know whether the underlying storage is a JVM filesystem or SAF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android and SAF
&lt;/h2&gt;

&lt;p&gt;This becomes particularly useful on Android.&lt;/p&gt;

&lt;p&gt;The Storage Access Framework doesn't behave like a traditional filesystem.&lt;/p&gt;

&lt;p&gt;Instead of receiving a normal filesystem path, your application may receive a URI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may then need to work with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ContentResolver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Document providers&lt;/li&gt;
&lt;li&gt;Document tree URIs&lt;/li&gt;
&lt;li&gt;Persisted permissions&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DocumentFile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DocumentsContract&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bytesmith's filesystem abstraction allows those details to stay inside the filesystem implementation.&lt;/p&gt;

&lt;p&gt;The compilation code doesn't need to become filled with Android-specific storage logic.&lt;/p&gt;

&lt;p&gt;You configure the filesystem once.&lt;/p&gt;

&lt;p&gt;Then Bytesmith can work through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoped Storage, SAF, or JVM paths?
&lt;/h2&gt;

&lt;p&gt;From the perspective of the compilation API, it shouldn't fundamentally matter whether your application is using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JVM filesystem
    |
    +-- /home/user/project/Main.kt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android application storage
    |
    +-- /data/user/0/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Storage Access Framework
    |
    +-- content://...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are different storage mechanisms.&lt;/p&gt;

&lt;p&gt;They don't have to result in completely different compiler implementations.&lt;/p&gt;

&lt;p&gt;The filesystem implementation handles the difference.&lt;/p&gt;

&lt;p&gt;That's one of the reasons I built the filesystem abstraction separately from the compilation logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android usage
&lt;/h2&gt;

&lt;p&gt;An Android application can configure the appropriate filesystem implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileSystems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AndroidSafFileSystem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the same Bytesmith compilation layer can operate against the configured filesystem.&lt;/p&gt;

&lt;p&gt;If SAF resources need to be materialized for a particular compilation operation, the filesystem implementation can handle that as well.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;local&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;materialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Sources"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clearMaterialized&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sources"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important thing is that the compilation layer doesn't need to implement its own SAF handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classpaths
&lt;/h2&gt;

&lt;p&gt;Real compilation usually requires dependencies.&lt;/p&gt;

&lt;p&gt;Bytesmith allows you to provide a custom classpath.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;outputDir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"build/classes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;kotlinSources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kotlinSources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;javaSources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;javaSources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;classpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"libs/library.jar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"libs/another-library.jar"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;bootClasspath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;warningsAsErrors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application decides what libraries should be visible to the compiler.&lt;/p&gt;

&lt;p&gt;Bytesmith handles passing those dependencies into the appropriate compiler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kotlin standard library versions
&lt;/h2&gt;

&lt;p&gt;This separation is also useful when working with Kotlin's standard library.&lt;/p&gt;

&lt;p&gt;The Kotlin compiler and Kotlin standard library are related, but they aren't exactly the same thing.&lt;/p&gt;

&lt;p&gt;If your generated source needs APIs from a newer Kotlin standard library, you can provide the appropriate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kotlin-stdlib.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;through the compilation classpath.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;classpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"libs/kotlin-stdlib.jar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"libs/my-library.jar"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means an update to the standard library doesn't automatically mean Bytesmith itself needs to be updated simply because the library JAR changed.&lt;/p&gt;

&lt;p&gt;There is an important limitation.&lt;/p&gt;

&lt;p&gt;Providing a newer &lt;code&gt;kotlin-stdlib.jar&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; make an older Kotlin compiler understand new Kotlin language features.&lt;/p&gt;

&lt;p&gt;If a newer Kotlin release introduces new syntax, compiler functionality, or lowering requirements, the compiler itself needs to support those features.&lt;/p&gt;

&lt;p&gt;The goal is not to pretend compiler versions don't matter.&lt;/p&gt;

&lt;p&gt;The goal is to avoid coupling things that can evolve independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootclasspath
&lt;/h2&gt;

&lt;p&gt;Compilation environments sometimes require a bootclasspath.&lt;/p&gt;

&lt;p&gt;Bytesmith can detect the appropriate bootclasspath where possible.&lt;/p&gt;

&lt;p&gt;If you need explicit control, you can provide one yourself.&lt;/p&gt;

&lt;p&gt;This gives you two options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple case
&lt;/h3&gt;

&lt;p&gt;Let Bytesmith determine the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced case
&lt;/h3&gt;

&lt;p&gt;The application supplies the bootclasspath.&lt;/p&gt;

&lt;p&gt;You get convenience without losing control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compiler plugins
&lt;/h2&gt;

&lt;p&gt;Bytesmith also supports Kotlin compiler plugins.&lt;/p&gt;

&lt;p&gt;You can provide plugin JARs and plugin options as part of the compilation environment.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pluginClasspath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"plugins/my-plugin.jar"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;pluginOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"plugin:my.plugin:key=value"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes Bytesmith useful for more than straightforward source compilation.&lt;/p&gt;

&lt;p&gt;An application can construct a compilation environment specifically for the code it needs to compile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Packaging the output
&lt;/h2&gt;

&lt;p&gt;Compilation doesn't necessarily have to end with a directory containing &lt;code&gt;.class&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;Bytesmith can also package compiled output into JAR or ZIP archives.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/classes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can be used as a directory output, while an application can also produce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful when the compiled result needs to be consumed as an artifact rather than simply left in a classes directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostics
&lt;/h2&gt;

&lt;p&gt;Compilation errors are only useful if your application can actually report them.&lt;/p&gt;

&lt;p&gt;Bytesmith exposes compilation results and listener callbacks.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Listener&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ICompilationListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;hasErrors&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onProblem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CompilationProblem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;printMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onClassCompiled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compiledClass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CompiledClass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compiledClass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compilation result can also provide information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;errorCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warningCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compiledClassCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;elapseTimeMillis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This becomes particularly useful when Bytesmith is embedded inside a UI.&lt;/p&gt;

&lt;p&gt;An Android code editor, for example, could take compiler diagnostics and display them directly to the user rather than dumping compiler output to a terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Command-line usage
&lt;/h2&gt;

&lt;p&gt;Bytesmith isn't limited to an embedded API.&lt;/p&gt;

&lt;p&gt;It also provides a command-line interface.&lt;/p&gt;

&lt;p&gt;A compilation can look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bytesmith   -cp libs/*   -bc jmods   -d build/classes   src/main/java   src/main/kotlin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI exposes options for classpaths, boot classpaths, module paths, source paths, compiler plugins, plugin options, output destinations, and warnings-as-errors.&lt;/p&gt;

&lt;p&gt;This means the same compilation capabilities can be used programmatically or from the command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use Gradle?
&lt;/h2&gt;

&lt;p&gt;Gradle is excellent when you're building a conventional project.&lt;/p&gt;

&lt;p&gt;But that's not necessarily the problem Bytesmith is trying to solve.&lt;/p&gt;

&lt;p&gt;Imagine an application where the &lt;strong&gt;application itself&lt;/strong&gt; needs to compile source code.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Android IDE&lt;/li&gt;
&lt;li&gt;A code editor&lt;/li&gt;
&lt;li&gt;A developer tool&lt;/li&gt;
&lt;li&gt;A code generator&lt;/li&gt;
&lt;li&gt;A scripting environment&lt;/li&gt;
&lt;li&gt;A custom build tool&lt;/li&gt;
&lt;li&gt;An educational programming environment&lt;/li&gt;
&lt;li&gt;An application that generates Kotlin or Java code dynamically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these situations, starting a complete Gradle build just to compile a set of source files can be excessive.&lt;/p&gt;

&lt;p&gt;You may simply want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source files
     |
     v
  Compile
     |
     v
Classes / JAR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bytesmith provides that compilation layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The architecture can be summarized as four responsibilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Your application
&lt;/h3&gt;

&lt;p&gt;Decides what it wants to compile and where the result should go.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The filesystem abstraction
&lt;/h3&gt;

&lt;p&gt;Decides how files are actually accessed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JvmFileSystem
AndroidSafFileSystem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Bytesmith
&lt;/h3&gt;

&lt;p&gt;Orchestrates compilation, classpaths, compiler configuration, diagnostics, and packaging.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Language compilers
&lt;/h3&gt;

&lt;p&gt;Kotlin is compiled through the Kotlin compiler.&lt;/p&gt;

&lt;p&gt;Java is compiled through ECJ.&lt;/p&gt;

&lt;p&gt;This separation is what makes the whole thing composable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;For a simple use case, the workflow is essentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Configure filesystem
        |
        v
Provide sources
        |
        v
Compile
        |
        v
Get result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For advanced applications, you can progressively add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Classpath
Bootclasspath
Compiler plugins
Plugin options
Diagnostics
Packaging
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without having to replace the underlying compilation architecture.&lt;/p&gt;

&lt;p&gt;That's the balance Bytesmith is aiming for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple when you need simple. Configurable when you need control.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I built Bytesmith around a fairly simple idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Compilation should be a capability an application can plug in, not an entire infrastructure the application has to reinvent.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It can compile Kotlin.&lt;/p&gt;

&lt;p&gt;It can compile Java.&lt;/p&gt;

&lt;p&gt;It can handle mixed Kotlin/Java projects.&lt;/p&gt;

&lt;p&gt;It can work with custom classpaths and boot classpaths.&lt;/p&gt;

&lt;p&gt;It supports compiler plugins and packaging.&lt;/p&gt;

&lt;p&gt;And, perhaps most importantly for Android, it doesn't assume that every file is a traditional JVM filesystem path.&lt;/p&gt;

&lt;p&gt;The application chooses the filesystem implementation.&lt;/p&gt;

&lt;p&gt;Bytesmith works through that abstraction.&lt;/p&gt;

&lt;p&gt;So whether you're compiling files from a desktop filesystem, Android application storage, or resources exposed through the Storage Access Framework, the compilation layer can remain the same.&lt;/p&gt;

&lt;p&gt;Configure the filesystem.&lt;/p&gt;

&lt;p&gt;Give Bytesmith the source.&lt;/p&gt;

&lt;p&gt;Configure dependencies when necessary.&lt;/p&gt;

&lt;p&gt;Choose the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's Bytesmith.&lt;/p&gt;

</description>
      <category>android</category>
      <category>java</category>
      <category>kotlin</category>
      <category>software</category>
    </item>
    <item>
      <title>Android Storage in 2026: Scoped Storage, SAF, and Why Filesystem Abstractions Still Matter</title>
      <dc:creator>Sifiso Fakude</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:21:32 +0000</pubDate>
      <link>https://dev.to/slambyte/android-storage-in-2026-scoped-storage-saf-and-why-filesystem-abstractions-still-matter-30e</link>
      <guid>https://dev.to/slambyte/android-storage-in-2026-scoped-storage-saf-and-why-filesystem-abstractions-still-matter-30e</guid>
      <description>&lt;p&gt;Android storage has changed dramatically.&lt;/p&gt;

&lt;p&gt;For a long time, Android developers could think about storage in familiar terms:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/some/path/file.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You had a path. You opened a stream. You copied a file. You created a directory.&lt;/p&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;Modern Android is different.&lt;/p&gt;

&lt;p&gt;Today, an application may interact with application-private storage, shared media through &lt;code&gt;MediaStore&lt;/code&gt;, user-selected documents through the Storage Access Framework (SAF), cloud-backed document providers, and traditional JVM filesystem APIs.&lt;/p&gt;

&lt;p&gt;The difficult part isn't learning any one of these APIs.&lt;/p&gt;

&lt;p&gt;The difficult part is building software that can work with &lt;strong&gt;different kinds of storage without forcing the entire application to understand the differences&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the problem that led me to build a filesystem abstraction for Android and the JVM.&lt;/p&gt;

&lt;h2&gt;
  
  
  The filesystem isn't always a filesystem anymore
&lt;/h2&gt;

&lt;p&gt;On the JVM, this is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/home/user/documents/example.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inputStream&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// read&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application has a path that refers to something on the filesystem.&lt;/p&gt;

&lt;p&gt;Android's Storage Access Framework changes that model.&lt;/p&gt;

&lt;p&gt;Instead of receiving a normal filesystem path, an application can receive something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://com.android.externalstorage.documents/tree/primary%3ADocuments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That isn't a filesystem path.&lt;/p&gt;

&lt;p&gt;It is a &lt;strong&gt;URI representing a document-provider resource&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You don't meaningfully resolve it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operating system expects you to interact with it through APIs such as &lt;code&gt;ContentResolver&lt;/code&gt; and &lt;code&gt;DocumentsContract&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This creates an important architectural distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Traditional filesystem

Path
  ↓
File
  ↓
InputStream / OutputStream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Storage Access Framework

URI
  ↓
ContentResolver / DocumentsContract
  ↓
Provider
  ↓
InputStream / OutputStream
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final result may still be a stream, but everything before that stream is fundamentally different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoped Storage solved one problem, but not every storage problem
&lt;/h2&gt;

&lt;p&gt;Scoped Storage was an important change in Android's storage model.&lt;/p&gt;

&lt;p&gt;For many applications, it makes storage much simpler.&lt;/p&gt;

&lt;p&gt;An application can use its own private directories and appropriate platform APIs without needing unrestricted access to the user's entire shared filesystem.&lt;/p&gt;

&lt;p&gt;For applications working with photos, videos, audio and other supported media, &lt;code&gt;MediaStore&lt;/code&gt; can also provide an appropriate abstraction.&lt;/p&gt;

&lt;p&gt;And this is where an important point gets overlooked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not every Android application needs SAF.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an application only needs its own application data, use application-private storage.&lt;/p&gt;

&lt;p&gt;If it needs to work with supported shared media, &lt;code&gt;MediaStore&lt;/code&gt; may be the right solution.&lt;/p&gt;

&lt;p&gt;But there are applications where the requirement is different:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let the user choose a directory and allow the application to work with files inside that directory."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where SAF becomes particularly interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  SAF isn't just a file picker
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It is much more than that.&lt;/p&gt;

&lt;p&gt;With the appropriate user-granted permissions, an application can work with a selected document tree.&lt;/p&gt;

&lt;p&gt;That tree can represent storage managed by different document providers.&lt;/p&gt;

&lt;p&gt;And those providers don't necessarily have to represent the device's local storage.&lt;/p&gt;

&lt;p&gt;The underlying provider could potentially represent remote or cloud-backed storage.&lt;/p&gt;

&lt;p&gt;That means the application isn't necessarily interacting with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Android device → physical filesystem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It can be closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    ↓
ContentResolver
    ↓
Document Provider
    ↓
Storage implementation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application doesn't need to know whether the provider ultimately represents local storage or another storage service.&lt;/p&gt;

&lt;p&gt;That's one of the most powerful ideas behind SAF.&lt;/p&gt;

&lt;h2&gt;
  
  
  But this creates a problem for filesystem-oriented code
&lt;/h2&gt;

&lt;p&gt;Imagine a library with an API like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"documents/example.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the JVM, that's easy.&lt;/p&gt;

&lt;p&gt;But what does &lt;code&gt;"documents/example.txt"&lt;/code&gt; mean on Android if the actual storage root is a SAF URI?&lt;/p&gt;

&lt;p&gt;Suppose the user selected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://.../tree/primary%3ADocuments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the library needs to interpret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;documents/example.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;relative to that selected document tree.&lt;/p&gt;

&lt;p&gt;It cannot simply construct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://.../documents/example.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because SAF URIs aren't ordinary paths.&lt;/p&gt;

&lt;p&gt;The library has to resolve the path through the document provider.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Selected SAF root
        ↓
Documents
        ↓
projects
        ↓
example.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step represents a document-provider operation rather than a normal filesystem traversal.&lt;/p&gt;

&lt;p&gt;This is where filesystem abstraction becomes useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abstract the storage, not the application
&lt;/h2&gt;

&lt;p&gt;The goal isn't to pretend that SAF and the JVM filesystem are identical.&lt;/p&gt;

&lt;p&gt;They aren't.&lt;/p&gt;

&lt;p&gt;Trying to hide every difference can actually make an abstraction worse.&lt;/p&gt;

&lt;p&gt;Instead, the abstraction should hide the &lt;strong&gt;mechanics of accessing the storage&lt;/strong&gt; while exposing operations that make sense to the application.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FileSystemUtil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Source&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Sink&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JVM implementation can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;File&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while an Android SAF implementation can use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Uri
ContentResolver
DocumentsContract
DocumentFile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application doesn't need to know which implementation is currently providing the storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: cross-filesystem operations
&lt;/h2&gt;

&lt;p&gt;This is where things become considerably more interesting.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JVM filesystem
      ↓
      ↓ copy
      ↓
SAF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The source might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/user/file.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the destination might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://.../tree/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no useful way to make both sides behave like &lt;code&gt;java.io.File&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, the operation has to understand both storage systems.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JVM source
   ↓
InputStream
   ↓
buffer
   ↓
OutputStream
   ↓
SAF destination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the reverse is equally important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SAF source
   ↓
InputStream
   ↓
buffer
   ↓
OutputStream
   ↓
JVM destination
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why I eventually became less interested in simply wrapping &lt;code&gt;File&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The abstraction needs to represent &lt;strong&gt;storage operations&lt;/strong&gt;, not just filesystem paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Relative paths become interesting with SAF
&lt;/h2&gt;

&lt;p&gt;Another problem appears when working with a selected SAF tree.&lt;/p&gt;

&lt;p&gt;Suppose an application asks the user to select a root directory.&lt;/p&gt;

&lt;p&gt;The application can then operate relative to that root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;selected root/
    projects/
        demo/
            example.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application might want to say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;createFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"projects/demo/example.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than repeatedly passing the root URI around.&lt;/p&gt;

&lt;p&gt;This creates a useful model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Selected root
     +
Relative path
     ↓
Resolved document
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation can traverse the document tree and create missing directories where appropriate.&lt;/p&gt;

&lt;p&gt;That gives application code a filesystem-like experience without pretending the underlying storage is actually a filesystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use DocumentFile everywhere?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DocumentFile&lt;/code&gt; is extremely useful.&lt;/p&gt;

&lt;p&gt;It provides a convenient object-oriented API for working with document trees and makes traversal considerably easier.&lt;/p&gt;

&lt;p&gt;But abstraction layers also have to consider performance and API boundaries.&lt;/p&gt;

&lt;p&gt;There are situations where direct use of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;DocumentsContract&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;ContentResolver&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is more appropriate.&lt;/p&gt;

&lt;p&gt;In my implementation, I ended up using both approaches depending on the operation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DocumentFile&lt;/code&gt; is useful for traversal and convenient document operations.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DocumentsContract&lt;/code&gt; provides lower-level access to document-provider operations.&lt;/p&gt;

&lt;p&gt;The important lesson is that a good abstraction doesn't necessarily mean choosing one Android API and hiding everything else behind it.&lt;/p&gt;

&lt;p&gt;It means choosing the appropriate mechanism for each operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  SAF also changes what a "path" means
&lt;/h2&gt;

&lt;p&gt;This is probably the biggest conceptual difference.&lt;/p&gt;

&lt;p&gt;On a traditional filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/user/project/file.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is a path.&lt;/p&gt;

&lt;p&gt;With SAF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content://provider/tree/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is an identifier for a resource managed by a provider.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;p&gt;A URI can contain information that looks path-like, but it shouldn't be treated as if it were a normal filesystem path.&lt;/p&gt;

&lt;p&gt;This is why APIs such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are fundamentally the wrong abstraction for SAF.&lt;/p&gt;

&lt;p&gt;A robust storage abstraction has to understand that there are different &lt;strong&gt;resource addressing models&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about cloud storage?
&lt;/h2&gt;

&lt;p&gt;This is another reason SAF is interesting.&lt;/p&gt;

&lt;p&gt;A document provider can abstract storage that isn't necessarily represented as a normal local filesystem.&lt;/p&gt;

&lt;p&gt;From the application's perspective, the interaction can still look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;open document
read stream
write stream
create directory
delete document
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider handles the underlying storage.&lt;/p&gt;

&lt;p&gt;This is one of the reasons I don't think of SAF simply as "Android's annoying file picker."&lt;/p&gt;

&lt;p&gt;It is closer to a provider-based storage interface.&lt;/p&gt;

&lt;p&gt;The application requests operations against documents.&lt;/p&gt;

&lt;p&gt;The provider determines how those documents are actually represented.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where does MediaStore fit?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;MediaStore&lt;/code&gt; and SAF solve different problems.&lt;/p&gt;

&lt;p&gt;If you're building an application that primarily works with shared photos, videos, audio or other supported media, &lt;code&gt;MediaStore&lt;/code&gt; is often the appropriate API.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A modern Android storage architecture therefore isn't necessarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SAF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MediaStore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is often:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Application
                      |
          +-----------+-----------+
          |           |           |
     App storage   MediaStore    SAF
          |           |           |
       private      shared      documents
                                 |
                         Document Provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right choice depends on the application's requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built a filesystem abstraction
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That led to my filesystem library.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileSystems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JvmFileSystem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;FileSystems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AndroidSafFileSystem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then higher-level operations can work through the abstraction.&lt;/p&gt;

&lt;p&gt;The JVM implementation deals with traditional filesystem paths.&lt;/p&gt;

&lt;p&gt;The Android implementation understands SAF URIs, document trees, &lt;code&gt;ContentResolver&lt;/code&gt;, &lt;code&gt;DocumentsContract&lt;/code&gt;, and document traversal.&lt;/p&gt;

&lt;p&gt;The application gets a consistent API while the implementation deals with the storage-specific complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  This doesn't make Android storage "simple"
&lt;/h2&gt;

&lt;p&gt;And that's important.&lt;/p&gt;

&lt;p&gt;A filesystem abstraction doesn't magically turn SAF into &lt;code&gt;java.io.File&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are still differences.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SAF resources don't behave like normal filesystem paths.&lt;/li&gt;
&lt;li&gt;Some operations depend on what the document provider supports.&lt;/li&gt;
&lt;li&gt;Permissions are granted by the user.&lt;/li&gt;
&lt;li&gt;A provider may have different capabilities from a local filesystem.&lt;/li&gt;
&lt;li&gt;A URI may represent storage outside the traditional local filesystem.&lt;/li&gt;
&lt;li&gt;Operations such as renaming, creating documents and traversing directories have provider-specific semantics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good abstraction should expose those realities rather than hide them until they become bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem is storage integration
&lt;/h2&gt;

&lt;p&gt;The difficult question isn't:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I read a file on Android?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's easy.&lt;/p&gt;

&lt;p&gt;The difficult questions are:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I design an application that can work with multiple storage models?"&lt;/p&gt;

&lt;p&gt;"How do I copy between a traditional JVM filesystem and a document provider?"&lt;/p&gt;

&lt;p&gt;"How do I expose relative paths when the underlying storage is a URI?"&lt;/p&gt;

&lt;p&gt;"How do I keep storage-specific code from leaking throughout the application?"&lt;/p&gt;

&lt;p&gt;"How do I build a filesystem-like API without pretending every storage backend is actually a filesystem?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are architecture problems.&lt;/p&gt;

&lt;p&gt;And that's where filesystem abstraction becomes useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The future isn't necessarily one filesystem API
&lt;/h2&gt;

&lt;p&gt;Modern applications increasingly interact with storage through abstractions.&lt;/p&gt;

&lt;p&gt;A local filesystem is one abstraction.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MediaStore&lt;/code&gt; is another.&lt;/p&gt;

&lt;p&gt;SAF is another.&lt;/p&gt;

&lt;p&gt;Cloud document providers introduce another layer.&lt;/p&gt;

&lt;p&gt;The important skill isn't memorizing every API.&lt;/p&gt;

&lt;p&gt;It's understanding &lt;strong&gt;what storage model you're actually working with&lt;/strong&gt; and designing your application around that model.&lt;/p&gt;

&lt;p&gt;That's what my work on filesystem integration has taught me.&lt;/p&gt;

&lt;p&gt;Android storage isn't just about files anymore.&lt;/p&gt;

&lt;p&gt;It's about &lt;strong&gt;resources, providers, permissions, URIs, streams, and abstractions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And sometimes, the best solution is to build an abstraction that lets the rest of the application stop worrying about which one it's dealing with.&lt;/p&gt;




&lt;h3&gt;
  
  
  About the author
&lt;/h3&gt;

&lt;p&gt;I'm Sifiso Fakude, a software developer focused on Android storage, filesystem integration, and developer tooling.&lt;/p&gt;

&lt;p&gt;My work includes building filesystem abstractions that bridge traditional JVM filesystems with Android's Storage Access Framework and exploring the architectural challenges created by modern Android storage.&lt;/p&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>kotlin</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
