<?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: Anubhav</title>
    <description>The latest articles on DEV Community by Anubhav (@_anubhav).</description>
    <link>https://dev.to/_anubhav</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%2F3966708%2Fda594f12-f8f3-4f1b-a496-87b74405ca5e.png</url>
      <title>DEV Community: Anubhav</title>
      <link>https://dev.to/_anubhav</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_anubhav"/>
    <language>en</language>
    <item>
      <title>Should Your Thread Keep the JVM Alive?</title>
      <dc:creator>Anubhav</dc:creator>
      <pubDate>Sat, 12 Sep 2026 08:55:28 +0000</pubDate>
      <link>https://dev.to/_anubhav/should-your-thread-keep-the-jvm-alive-3khn</link>
      <guid>https://dev.to/_anubhav/should-your-thread-keep-the-jvm-alive-3khn</guid>
      <description>&lt;h3&gt;
  
  
  Understanding daemon vs non-daemon threads
&lt;/h3&gt;

&lt;p&gt;Daemon vs non-daemon is one of those distinctions that seems trivial ("just a boolean flag") until you get bitten by it, and then you never forget it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core difference
&lt;/h2&gt;

&lt;p&gt;The JVM has one simple rule for when to shut down: &lt;strong&gt;the JVM exits when all non-daemon threads have finished.&lt;/strong&gt; Daemon threads are ignored in that calculation entirely.&lt;/p&gt;

&lt;p&gt;That is it. That is the whole distinction. Everything else follows from that one rule.&lt;/p&gt;

&lt;p&gt;Put another way: non-daemon threads &lt;em&gt;keep the JVM alive&lt;/em&gt;. Daemon threads &lt;em&gt;do not&lt;/em&gt;. When the last non-daemon thread finishes, the JVM stops caring what daemon threads are doing and terminates the process; mid-execution, mid-loop, mid-I/O, whatever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By default, all threads are non-daemon.&lt;/strong&gt; The &lt;code&gt;main&lt;/code&gt; thread is non-daemon, and any thread you create inherits its parent's daemon status. So threads started from &lt;code&gt;main&lt;/code&gt; are also non-daemon unless you explicitly say otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete demo of the rule
&lt;/h2&gt;

&lt;p&gt;Say you write this:&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="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;worker&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Thread&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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="s"&gt;"Working... $it"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&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="s"&gt;"Done!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;isDaemon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;  &lt;span class="c1"&gt;// non-daemon (the default)&lt;/span&gt;
      &lt;span class="nf"&gt;start&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="s"&gt;"main() returning"&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;&lt;code&gt;main()&lt;/code&gt; returns almost immediately. But you will see all &lt;code&gt;10 "Working..." lines and "Done!"&lt;/code&gt; print, and &lt;em&gt;then&lt;/em&gt; the JVM exits. Why? Because the worker thread is non-daemon, and the JVM waits for it before shutting down.&lt;/p&gt;

&lt;p&gt;Now flip one line:&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;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isDaemon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;// daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;main()&lt;/code&gt; returns, prints &lt;code&gt;"main() returning"&lt;/code&gt;, and the JVM &lt;em&gt;immediately terminates;&lt;/em&gt; you will typically see zero "Working..." lines, or maybe one or two if you are lucky with timing. The daemon worker is killed mid-execution because nothing was keeping the JVM alive.&lt;/p&gt;

&lt;h2&gt;
  
  
  When each one matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Non-daemon (the default): Use when losing the work would be a problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your actual application work should be non-daemon. If a user tapped "Send message" and your app is halfway through the HTTP POST, you do not want the JVM to exit and drop the request just because &lt;code&gt;main()&lt;/code&gt; (or on Android, some other lifecycle callback) happened to return. The whole point of "waiting for work to finish" is what non-daemon threads give you.&lt;/p&gt;

&lt;p&gt;This is why the general-purpose networking pool should have non-daemon threads. If a user initiates a request, your library owes them either a response or a failure; silently vanishing because the process exited is the worst possible outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daemon: Use when the work is optional/best-effort/background-only.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Daemon threads make sense when the work is &lt;em&gt;conceptually&lt;/em&gt; subservient to the main application. If the main app is done, this work is by definition irrelevant, and there is no harm in killing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 1: Analytics uploader
&lt;/h3&gt;

&lt;p&gt;You have an analytics library that batches events and uploads them every 30 seconds. If the user closes the app while a batch is mid-upload, what should happen?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the uploader is non-daemon:&lt;/strong&gt; the JVM will stubbornly wait for the current upload to finish before actually exiting. From the user's perspective, they closed the app but the process is still there, using battery, holding a socket, until the analytics call completes. If the network is slow, this could be seconds. If the endpoint is down and the request is stuck waiting for a timeout, this could be minutes. Worse, if the uploader has a polling loop that runs forever, the JVM &lt;em&gt;never&lt;/em&gt; exits. Every 30 seconds, forever, until the OS force-kills the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the uploader is daemon:&lt;/strong&gt; when the app closes, the uploader is killed mid-upload, and the JVM exits cleanly. The in-flight batch is lost; but that is the &lt;em&gt;right&lt;/em&gt; tradeoff for analytics. Losing one batch of "user tapped button X" events is fine; blocking the user's app shutdown to preserve them is not.&lt;/p&gt;

&lt;p&gt;The principle: &lt;strong&gt;the loss of the work is less bad than the cost of waiting for it.&lt;/strong&gt; For analytics, that is almost always true.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 2: Best-effort telemetry / heartbeats
&lt;/h3&gt;

&lt;p&gt;Your app pings a health-check endpoint every 60 seconds so your backend knows the client is alive. Same reasoning: if the app is shutting down, the client is &lt;em&gt;by definition&lt;/em&gt; no longer alive, and sending one last heartbeat is pointless. You want that thread to just die when the app dies. Daemon is the right call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario 3: File writer
&lt;/h3&gt;

&lt;p&gt;Suppose you have a background thread that writes the user's document to disk. Should it be daemon?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Absolutely not.&lt;/strong&gt; If the JVM exits mid-write, the file is corrupted and the user's work is lost. This thread must be non-daemon, so the JVM waits for the write to complete before exiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  General decision heuristic
&lt;/h3&gt;

&lt;p&gt;Ask yourself: &lt;em&gt;If the JVM exits right now while this thread is mid-task, is that bad?&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yes, bad&lt;/strong&gt; → non-daemon. (User work, financial transactions, writes to persistent storage, in-flight API calls that the user initiated.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No, fine&lt;/strong&gt; → daemon. (Analytics, telemetry, cache warmers, background metric collection, prefetch that will just re-run next launch.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The wrinkle on Android specifically
&lt;/h2&gt;

&lt;p&gt;On Android, the JVM's "exit when all non-daemon threads finish" rule &lt;em&gt;technically&lt;/em&gt; still applies, but in practice the Android runtime keeps your process alive based on component lifecycle (Activities, Services, foreground state), not thread counts. The system can also kill your process at any time regardless of what threads are running.&lt;/p&gt;

&lt;p&gt;So on Android, the daemon flag matters less for &lt;em&gt;JVM shutdown&lt;/em&gt; and more for a related concern: &lt;strong&gt;whether a lingering non-daemon thread prevents your process from being cleanly reclaimable.&lt;/strong&gt; A rogue non-daemon thread pool that keeps threads alive after your Activity is destroyed can leak the entire Activity (and its context, view hierarchy, and everything else) until GC. Making pools daemon-flagged when their work is truly disposable helps the runtime clean up more predictably.&lt;/p&gt;

&lt;p&gt;But the general principle still holds: user-initiated work → non-daemon; background/optional work → daemon.&lt;/p&gt;

</description>
      <category>java</category>
      <category>kotlin</category>
      <category>android</category>
      <category>programming</category>
    </item>
    <item>
      <title>What jvmToolchain(N) actually does (and why it replaces the compileOptions block)</title>
      <dc:creator>Anubhav</dc:creator>
      <pubDate>Tue, 25 Aug 2026 17:11:37 +0000</pubDate>
      <link>https://dev.to/_anubhav/what-jvmtoolchainn-actually-does-and-why-it-replaces-the-compileoptions-block-4lb0</link>
      <guid>https://dev.to/_anubhav/what-jvmtoolchainn-actually-does-and-why-it-replaces-the-compileoptions-block-4lb0</guid>
      <description>&lt;p&gt;Older Android modules typically pinned their Java version like this:&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;android&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;compileOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sourceCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_17&lt;/span&gt;
        &lt;span class="n"&gt;targetCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_17&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;kotlinOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;jvmTarget&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"17"&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;Newer modules replace all of that with a single line:&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;kotlin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;jvmToolchain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17&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;At first glance this looks like a cosmetic cleanup. It is not. The one-line version is doing strictly more work than the block it replaced, and it closes a class of runtime bug that the older setup silently allowed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three moving parts
&lt;/h2&gt;

&lt;p&gt;To see why this matters, three things need to be separated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Which JDK runs the compiler.&lt;/strong&gt; The &lt;code&gt;javac&lt;/code&gt; binary that actually turns your source into bytecode. Traditionally this is whichever JDK your &lt;code&gt;JAVA_HOME&lt;/code&gt; points at, or whichever launched Gradle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which Java language features your source code is allowed to use.&lt;/strong&gt; This is what &lt;code&gt;sourceCompatibility&lt;/code&gt; controls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which Java version can execute the bytecode you produce.&lt;/strong&gt; This is what &lt;code&gt;targetCompatibility&lt;/code&gt; controls.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These three can all be different from each other, and that is exactly what causes the confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;sourceCompatibility&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;Java the language keeps evolving with every new version. Each release adds new keywords and new grammar rules. Java 10 added &lt;code&gt;var&lt;/code&gt;. Java 14 added &lt;code&gt;record&lt;/code&gt;. Java 17 added &lt;code&gt;sealed&lt;/code&gt; classes. Setting &lt;code&gt;sourceCompatibility = 11&lt;/code&gt; tells the compiler: "when you read my source files, pretend you only understand Java up to version 11". If you try to use &lt;code&gt;record&lt;/code&gt; or &lt;code&gt;sealed&lt;/code&gt; in your code, the compiler will reject those files and refuse to build.&lt;/p&gt;

&lt;p&gt;This gives you a safety net. Even if the JDK doing the compilation is JDK 21 (which knows about all the newer keywords), &lt;code&gt;sourceCompatibility = 11&lt;/code&gt; stops you from accidentally using them.&lt;/p&gt;

&lt;p&gt;But that safety net only covers source syntax: the shape of your code, the keywords, the grammar, the spelling. It does not police which methods or classes you call. Those live in the standard library, and the standard library is chosen by whichever JDK is doing the compilation. The split looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing &lt;code&gt;var x = 10&lt;/code&gt; is syntax (the &lt;code&gt;var&lt;/code&gt; keyword). Controlled by &lt;code&gt;sourceCompatibility&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Writing &lt;code&gt;"hello".indent(4)&lt;/code&gt; is a method call. Not controlled by &lt;code&gt;sourceCompatibility&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both were added in newer Java versions (&lt;code&gt;var&lt;/code&gt; in 10, &lt;code&gt;indent()&lt;/code&gt; in 12), but only the first one gets caught by &lt;code&gt;sourceCompatibility = 11&lt;/code&gt;. The second one slips through.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;targetCompatibility&lt;/code&gt; actually does
&lt;/h2&gt;

&lt;p&gt;Setting &lt;code&gt;targetCompatibility = 11&lt;/code&gt; sounds like it guarantees your code will run on JVM 11. It does not. All it really does is write a version number inside your compiled &lt;code&gt;.class&lt;/code&gt; files that says "this bytecode is in Java 11 format". That number tells any JVM loading the file "you need to be at least Java 11 to run me". What it does not do is check whether the code inside those files uses features or APIs that only exist in newer Java versions. It is a label, not a verifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What neither of them controls
&lt;/h2&gt;

&lt;p&gt;To compile your code, Gradle needs &lt;code&gt;javac&lt;/code&gt;, the Java compiler. But &lt;code&gt;javac&lt;/code&gt; is not one fixed program. Every JDK ships with its own copy of it. Gradle uses whichever JDK is on your machine (usually the one your &lt;code&gt;JAVA_HOME&lt;/code&gt; points to). If that happens to be JDK 21, then JDK 21's &lt;code&gt;javac&lt;/code&gt; does the compilation, even if you have set &lt;code&gt;sourceCompatibility = 11&lt;/code&gt; and &lt;code&gt;targetCompatibility = 11&lt;/code&gt;. Neither of those settings has any say in which JDK is picked.&lt;/p&gt;

&lt;p&gt;That matters, because each JDK also brings its own standard library: the built-in &lt;code&gt;String&lt;/code&gt;, &lt;code&gt;List&lt;/code&gt;, &lt;code&gt;Map&lt;/code&gt;, and hundreds of other classes. When JDK 21's &lt;code&gt;javac&lt;/code&gt; compiles your code, JDK 21's standard library is what is visible to you. So you can type &lt;code&gt;String.indent()&lt;/code&gt;, a method that was only added in Java 12. Your IDE autocomplete will show it, and the compiler will accept it, because from its point of view the method exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap this leaves open
&lt;/h2&gt;

&lt;p&gt;Here is the concrete failure mode. Suppose you are building a library with the older block:&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;compileOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sourceCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_11&lt;/span&gt;
    &lt;span class="n"&gt;targetCompatibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JavaVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;VERSION_11&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your &lt;code&gt;JAVA_HOME&lt;/code&gt; points to JDK 21. You write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything looks fine. Autocomplete offers &lt;code&gt;indent()&lt;/code&gt;. The compiler accepts it. Gradle builds a JAR whose bytecode is stamped "Java 11 format". You publish the library.&lt;/p&gt;

&lt;p&gt;A consumer picks up your library and runs it on JVM 11. On the first call to &lt;code&gt;cleanup()&lt;/code&gt;, the JVM tries to resolve &lt;code&gt;String.indent()&lt;/code&gt; and throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java.lang.NoSuchMethodError: 'java.lang.String java.lang.String.indent(int)'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bytecode was valid for JVM 11. The problem is what the bytecode contained: a call to a method that JVM 11's &lt;code&gt;String&lt;/code&gt; class simply does not have. Nothing in the build caught it, because at compile time the classpath had JDK 21's &lt;code&gt;String&lt;/code&gt;, which does have &lt;code&gt;indent()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;jvmToolchain(N)&lt;/code&gt; does differently
&lt;/h2&gt;

&lt;p&gt;When you write &lt;code&gt;jvmToolchain(17)&lt;/code&gt;, Gradle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds (or downloads and caches) an actual JDK 17 on your machine.&lt;/li&gt;
&lt;li&gt;Uses JDK 17's &lt;code&gt;javac&lt;/code&gt; to compile your code.&lt;/li&gt;
&lt;li&gt;Puts JDK 17's standard library on the classpath at compile time.&lt;/li&gt;
&lt;li&gt;Emits JVM 17 bytecode by default.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The loophole from the earlier scenario closes at step 3. If your target were 11 instead of 17, then JDK 11's &lt;code&gt;String&lt;/code&gt; would be what the compiler sees, and &lt;code&gt;String.indent()&lt;/code&gt; would not exist to be called. Autocomplete would not offer it. The compiler would reject it. The error moves from production runtime back to your local build, which is where it belongs.&lt;/p&gt;

&lt;p&gt;The single call is doing the job of three separate settings, plus one guarantee the old settings could not give you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping the responsibilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Controls&lt;/th&gt;
&lt;th&gt;Does not control&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sourceCompatibility&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which source language features are allowed&lt;/td&gt;
&lt;td&gt;Which JDK compiles, which library APIs are visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;targetCompatibility&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bytecode format version for Java &lt;code&gt;.class&lt;/code&gt; files&lt;/td&gt;
&lt;td&gt;Which library APIs are visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;kotlinOptions.jvmTarget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bytecode format version for Kotlin &lt;code&gt;.class&lt;/code&gt; files&lt;/td&gt;
&lt;td&gt;Which library APIs are visible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jvmToolchain(N)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compiler JDK, source features, bytecode version for both Java and Kotlin, standard library on the classpath&lt;/td&gt;
&lt;td&gt;(nothing left over)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that &lt;code&gt;kotlinOptions.jvmTarget&lt;/code&gt; had to be set separately in the old block, because &lt;code&gt;compileOptions&lt;/code&gt; only covers Java files. &lt;code&gt;jvmToolchain&lt;/code&gt; covers both languages at once, which is another reason it collapses the whole &lt;code&gt;compileOptions { ... }&lt;/code&gt; and &lt;code&gt;kotlinOptions { ... }&lt;/code&gt; pair into a single line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The migration from &lt;code&gt;compileOptions&lt;/code&gt; plus &lt;code&gt;kotlinOptions&lt;/code&gt; to &lt;code&gt;jvmToolchain&lt;/code&gt; is not just fewer lines. It is a different guarantee.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sourceCompatibility&lt;/code&gt; limits what source syntax the compiler accepts. It does not decide which JDK compiles or which standard library APIs are visible.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;targetCompatibility&lt;/code&gt; stamps a version number into the bytecode. It does not verify that the code inside actually runs on that version.&lt;/li&gt;
&lt;li&gt;Neither setting stops you from calling a method that only exists in a newer JDK's standard library. That is a runtime crash waiting to happen.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jvmToolchain(N)&lt;/code&gt; uses an actual JDK N to compile, which puts JDK N's standard library on the classpath. Post-N APIs literally are not visible to be called. The gap the older settings left open is closed at compile time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are maintaining a library or a shared module, this is the difference between "hope it runs on the target JVM" and "cannot compile against anything the target JVM will not have".&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>androiddev</category>
    </item>
    <item>
      <title>Retrofit's `callFactory`: What `.client(okHttpClient)` Is really doing</title>
      <dc:creator>Anubhav</dc:creator>
      <pubDate>Sun, 02 Aug 2026 07:22:50 +0000</pubDate>
      <link>https://dev.to/_anubhav/-retrofits-callfactory-what-clientokhttpclient-is-really-doing-21mg</link>
      <guid>https://dev.to/_anubhav/-retrofits-callfactory-what-clientokhttpclient-is-really-doing-21mg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojqw66ae65plhtxjixw5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojqw66ae65plhtxjixw5.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;Look at the two lines you write in almost every Retrofit setup:&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;Retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;okHttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.client(okHttpClient)&lt;/code&gt; line is one you probably wrote once and never thought about again. It has a sibling on the same builder called &lt;code&gt;.callFactory(...)&lt;/code&gt;, and most Android developers never touch it. This article is about what &lt;code&gt;.callFactory&lt;/code&gt; is, why it exists, and the things it lets you do that no interceptor ever will.&lt;/p&gt;

&lt;p&gt;To get there properly, we need to start at a lower altitude than Retrofit and climb back up. By the time we reach &lt;code&gt;.callFactory&lt;/code&gt;, it will feel obvious what it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four types that carry the whole model
&lt;/h2&gt;

&lt;p&gt;Before we talk about factories at all, it helps to know what actually composes an OkHttp request. Making an HTTP call breaks down into a handful of concerns: describing what you want to send, having something that can actually send it, holding onto that specific send while it is in flight, and reading what comes back. OkHttp gives each of those its own type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OkHttpClient&lt;/code&gt;&lt;/strong&gt; is the HTTP engine. It owns the connection pool, the dispatcher (a thread pool), the cache, the timeouts, and the interceptor list. Building one is expensive, so you make one and share it across your app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Request&lt;/code&gt;&lt;/strong&gt; is an immutable description of one HTTP request: a URL, a method, headers, an optional body. Building a &lt;code&gt;Request&lt;/code&gt; sends nothing over the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Call&lt;/code&gt;&lt;/strong&gt; is a &lt;code&gt;Request&lt;/code&gt; bound to an OkHttp engine (a specific &lt;code&gt;OkHttpClient&lt;/code&gt;), primed to fire but not yet fired. Single-use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Response&lt;/code&gt;&lt;/strong&gt; is what comes back after a &lt;code&gt;Call&lt;/code&gt; fires: status, headers, body.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful mental model: &lt;code&gt;OkHttpClient&lt;/code&gt; is the post office. &lt;code&gt;Request&lt;/code&gt; is a sealed, addressed envelope sitting on your desk. &lt;code&gt;Call&lt;/code&gt; is that envelope in the outbox, assigned to a specific post office, waiting for the mailman. &lt;code&gt;Response&lt;/code&gt; is the letter that arrives back.&lt;/p&gt;

&lt;p&gt;A few points worth holding onto before we move on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Request&lt;/code&gt; is inert data; &lt;code&gt;Call&lt;/code&gt; is a fireable action.&lt;/strong&gt; A &lt;code&gt;Request&lt;/code&gt; on its own has no engine and cannot send itself anywhere. A &lt;code&gt;Call&lt;/code&gt; is what you get when you hand a &lt;code&gt;Request&lt;/code&gt; to an &lt;code&gt;OkHttpClient&lt;/code&gt;, which binds it to the engine that will actually fire it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OkHttpClient&lt;/code&gt; is meant to be a singleton.&lt;/strong&gt; The connection pool and thread pool are the reason it is cheap to reuse and expensive to duplicate. To vary its configuration, call &lt;code&gt;client.newBuilder()&lt;/code&gt;, which produces a variant that shares the underlying pool, cache, and dispatcher.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Request&lt;/code&gt; is immutable too.&lt;/strong&gt; To tweak one, call &lt;code&gt;request.newBuilder()&lt;/code&gt;, change what you need, and &lt;code&gt;.build()&lt;/code&gt; a new instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sending a request without Retrofit
&lt;/h2&gt;

&lt;p&gt;Here is the OkHttp lifecycle with no Retrofit in the picture:&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="c1"&gt;// 1. The engine. Expensive; build once, share everywhere.&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Describe WHAT to send. Just data: a URL, a method, headers.&lt;/span&gt;
&lt;span class="c1"&gt;//    Building this sends nothing.&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;request&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com/user/42"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Hand the request to the engine. It returns a Call.&lt;/span&gt;
&lt;span class="c1"&gt;//    Still nothing on the network. The Call is primed but unfired.&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Fire it. THIS is where a socket opens and bytes move.&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// 5. Read the result.&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the shape of it deliberately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 2 builds a description. No I/O. You are writing the envelope.&lt;/li&gt;
&lt;li&gt;Step 3, &lt;code&gt;client.newCall(request)&lt;/code&gt;, produces a &lt;code&gt;Call&lt;/code&gt;. Still no I/O. The request is now bound to this specific engine and ready.&lt;/li&gt;
&lt;li&gt;Step 4, &lt;code&gt;call.execute()&lt;/code&gt;, is the only line that touches the network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The interesting question is why step 3 exists as a separate step. Why isn't there just one method, &lt;code&gt;client.send(request): Response&lt;/code&gt;? The answer to that question is the entire point of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;Call&lt;/code&gt; exists as an object
&lt;/h2&gt;

&lt;p&gt;Between the moment you decide to send a request and the moment the response arrives, a lot can happen. You might want to cancel it. You might want to check whether it has been fired yet. You might want to clone it and send it again. All of that needs an object that represents the request &lt;em&gt;while it is in flight&lt;/em&gt;, something you can hold a reference to and call methods on. That object is &lt;code&gt;Call&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interface, trimmed to essentials:&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;Call&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Cloneable&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;request&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;                  &lt;span class="c1"&gt;// read back the request I represent&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;                 &lt;span class="c1"&gt;// fire synchronously; block until the response returns&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseCallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// fire asynchronously; call me back later&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;                            &lt;span class="c1"&gt;// abort, even mid-flight&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;isExecuted&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;               &lt;span class="c1"&gt;// have I already been fired?&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;isCanceled&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;                       &lt;span class="c1"&gt;// give me a fresh, unfired copy of the same request&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how many of these methods only make sense on something in flight. Take &lt;code&gt;cancel()&lt;/code&gt;: you cannot cancel a &lt;code&gt;Response&lt;/code&gt;, because by the time you hold one, the request is already over. Take &lt;code&gt;isExecuted()&lt;/code&gt;: you cannot ask a &lt;code&gt;Request&lt;/code&gt; whether it has been fired, because a &lt;code&gt;Request&lt;/code&gt; has no engine and no live state. It is just data describing what to send. These are questions about the request's live state, not about its inputs or outputs. They need somewhere to live that is neither the &lt;code&gt;Request&lt;/code&gt; (which has no engine) nor the &lt;code&gt;Response&lt;/code&gt; (which is too late). The &lt;code&gt;Call&lt;/code&gt; is that somewhere.&lt;/p&gt;

&lt;p&gt;Before we move on to factories, two properties of &lt;code&gt;Call&lt;/code&gt; are worth calling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A &lt;code&gt;Call&lt;/code&gt; fires exactly once.&lt;/strong&gt; Calling &lt;code&gt;execute()&lt;/code&gt; or &lt;code&gt;enqueue()&lt;/code&gt; on a &lt;code&gt;Call&lt;/code&gt; that has already been fired throws &lt;code&gt;IllegalStateException: Already Executed&lt;/code&gt;. To send the same request again, make a new &lt;code&gt;Call&lt;/code&gt;, either by calling &lt;code&gt;client.newCall(request)&lt;/code&gt; again or by calling &lt;code&gt;call.clone()&lt;/code&gt;:&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;call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// throws IllegalStateException: Already Executed&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;fresh&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// a new, unfired Call for the same request&lt;/span&gt;
&lt;span class="n"&gt;fresh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;            &lt;span class="c1"&gt;// fine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A &lt;code&gt;Call&lt;/code&gt; can be fired synchronously or asynchronously, and the only difference is which thread runs the network call.&lt;/strong&gt; &lt;code&gt;execute()&lt;/code&gt; is synchronous. The calling thread blocks until the full response arrives, which on Android means calling it from the main thread throws &lt;code&gt;NetworkOnMainThreadException&lt;/code&gt;. &lt;code&gt;enqueue()&lt;/code&gt; is asynchronous. You hand OkHttp a &lt;code&gt;Callback&lt;/code&gt;, the line returns immediately, and OkHttp runs the request on one of its own dispatcher threads. Same &lt;code&gt;Call&lt;/code&gt;, same request, only the waiting model differs. Retrofit's &lt;code&gt;suspend&lt;/code&gt; functions use &lt;code&gt;enqueue&lt;/code&gt; under the hood and bridge the callback into coroutine suspension.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;Call.Factory&lt;/code&gt;: the formal name for what you have been doing
&lt;/h2&gt;

&lt;p&gt;Now the reframe. Look at step 3 again:&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;call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain English: "client, take this request and produce a &lt;code&gt;Call&lt;/code&gt; for me." The client, in that moment, is doing exactly one job. It is manufacturing a &lt;code&gt;Call&lt;/code&gt; from a &lt;code&gt;Request&lt;/code&gt;. OkHttp has a one-method interface for exactly that responsibility, nested inside the &lt;code&gt;Call&lt;/code&gt; interface itself:&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;Call&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... execute(), enqueue(), cancel(), and the rest we saw earlier&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Factory&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;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Call&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;That nested interface is the whole contract. One method. &lt;code&gt;Request&lt;/code&gt; in, &lt;code&gt;Call&lt;/code&gt; out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have been using this interface the whole time&lt;/strong&gt;, because &lt;code&gt;OkHttpClient&lt;/code&gt; implements it:&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="k"&gt;open&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&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;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="nc"&gt;RealCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forWebSocket&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;That gives &lt;code&gt;OkHttpClient&lt;/code&gt; two identities at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As the &lt;strong&gt;HTTP engine&lt;/strong&gt;, it owns the pool, dispatcher, cache, timeouts, and interceptors.&lt;/li&gt;
&lt;li&gt;As a &lt;strong&gt;&lt;code&gt;Call.Factory&lt;/code&gt;&lt;/strong&gt;, it exposes exactly one method: &lt;code&gt;newCall(request): Call&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;fun interface&lt;/code&gt; matters
&lt;/h3&gt;

&lt;p&gt;The declaration says &lt;code&gt;fun interface&lt;/code&gt;, not plain &lt;code&gt;interface&lt;/code&gt;. That marks it a &lt;strong&gt;functional interface&lt;/strong&gt; with a single abstract method, and it changes what the Kotlin compiler will accept. You can pass a lambda anywhere a &lt;code&gt;Call.Factory&lt;/code&gt; is expected, and the compiler will wrap the lambda body as the implementation of &lt;code&gt;newCall&lt;/code&gt;. The two forms below are identical to the compiler:&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="c1"&gt;// Explicit object&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="err"&gt;: &lt;/span&gt;&lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&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;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;someClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Lambda; Kotlin's SAM conversion produces exactly the object above&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;someClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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 is what makes the &lt;code&gt;.callFactory { ... }&lt;/code&gt; lambda form on Retrofit's builder compile so cleanly. The lambda's &lt;code&gt;request&lt;/code&gt; parameter &lt;em&gt;is&lt;/em&gt; &lt;code&gt;newCall&lt;/code&gt;'s argument. Whatever the lambda returns &lt;em&gt;is&lt;/em&gt; the returned &lt;code&gt;Call&lt;/code&gt;. Hold onto this. It is why the code later in the article is as short as it is.&lt;/p&gt;

&lt;p&gt;Small note on versioning: &lt;code&gt;Call.Factory&lt;/code&gt; was declared &lt;code&gt;fun interface&lt;/code&gt; in OkHttp 4.9. If you are on an older version, you need the anonymous-object form instead of the lambda form.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Retrofit actually needs from OkHttp
&lt;/h2&gt;

&lt;p&gt;Retrofit's job is to turn a Kotlin interface method into the five-step lifecycle we walked through earlier under &lt;em&gt;Sending a request without Retrofit&lt;/em&gt;. Given:&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;UserApi&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user/{id}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;id&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;UserDto&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when you call &lt;code&gt;api.getUser("42")&lt;/code&gt;, Retrofit internally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Builds a &lt;code&gt;Request&lt;/code&gt; from the &lt;code&gt;@GET("user/{id}")&lt;/code&gt; annotation and the argument. (Step 2 of that lifecycle.)&lt;/li&gt;
&lt;li&gt;Gets a &lt;code&gt;Call&lt;/code&gt; by calling &lt;code&gt;newCall(request)&lt;/code&gt; on a factory it holds. (Step 3.)&lt;/li&gt;
&lt;li&gt;Fires it with &lt;code&gt;execute()&lt;/code&gt; or &lt;code&gt;enqueue()&lt;/code&gt;, depending on whether the method returns &lt;code&gt;Call&amp;lt;T&amp;gt;&lt;/code&gt; or is &lt;code&gt;suspend&lt;/code&gt;. (Step 4.)&lt;/li&gt;
&lt;li&gt;Parses the body through your converter factory into &lt;code&gt;UserDto&lt;/code&gt;. (Step 5.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 2 is where the interesting design choice lives. To do &lt;code&gt;newCall(request)&lt;/code&gt;, Retrofit needs &lt;em&gt;something that has a &lt;code&gt;newCall&lt;/code&gt; method&lt;/em&gt;. In other words, a &lt;code&gt;Call.Factory&lt;/code&gt;. And Retrofit stores exactly that, as the &lt;strong&gt;interface type&lt;/strong&gt;:&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="c1"&gt;// Simplified from retrofit2.Retrofit&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Retrofit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;callFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;okhttp3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the type. It is not &lt;code&gt;OkHttpClient&lt;/code&gt;. It is &lt;code&gt;Call.Factory&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Retrofit depends on the interface, not the concrete client"
&lt;/h3&gt;

&lt;p&gt;That phrase gets thrown around, and it is worth unpacking precisely. Retrofit's field is typed &lt;code&gt;Call.Factory&lt;/code&gt;. Ask what Retrofit actually needs from the client, and the answer is: only the ability to turn a &lt;code&gt;Request&lt;/code&gt; into a &lt;code&gt;Call&lt;/code&gt;. Retrofit never reads the client's &lt;code&gt;.cache()&lt;/code&gt;, &lt;code&gt;.dispatcher()&lt;/code&gt;, &lt;code&gt;.connectionPool()&lt;/code&gt;, or its timeouts. Its entire per-request interaction with the client is one line:&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;call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;callFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typing the field as &lt;code&gt;OkHttpClient&lt;/code&gt; would demand more than Retrofit actually uses. Typing it as &lt;code&gt;Call.Factory&lt;/code&gt;, the smallest interface that provides &lt;code&gt;newCall&lt;/code&gt;, means Retrofit accepts &lt;em&gt;any&lt;/em&gt; implementation. The real &lt;code&gt;OkHttpClient&lt;/code&gt;, a custom class you wrote, a test double, a lambda. This is dependency inversion in one line: depend on the capability (&lt;code&gt;newCall&lt;/code&gt;), not on the class that happens to provide it. And that openness is exactly what makes the &lt;code&gt;.callFactory(...)&lt;/code&gt; hook meaningful. You are allowed to substitute your own implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;.client()&lt;/code&gt; is sugar for &lt;code&gt;.callFactory()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here is the reveal that makes everything else click. Look at Retrofit's builder in the current source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From retrofit2.Retrofit.Builder, actual code, not simplified&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Builder&lt;/span&gt; &lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OkHttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;callFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"client == null"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Builder&lt;/span&gt; &lt;span class="nf"&gt;callFactory&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;okhttp3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Factory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requireNonNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"factory == null"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.client(okHttpClient)&lt;/code&gt; is pure sugar. It works because &lt;code&gt;OkHttpClient&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; a &lt;code&gt;Call.Factory&lt;/code&gt;, so Retrofit drops it straight into the &lt;code&gt;callFactory&lt;/code&gt; field. There is no separate "client path" inside Retrofit. Everything funnels to that one field. If you supply neither, &lt;code&gt;build()&lt;/code&gt; fabricates a default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;okhttp3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Factory&lt;/span&gt; &lt;span class="n"&gt;callFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callFactory&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callFactory&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;callFactory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ways in, one field, one method eventually invoked:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You wrote&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;callFactory&lt;/code&gt; holds&lt;/th&gt;
&lt;th&gt;Per request, Retrofit runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;a default &lt;code&gt;OkHttpClient()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;defaultClient.newCall(request)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.client(myClient)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;your &lt;code&gt;OkHttpClient&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;myClient.newCall(request)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.callFactory(f)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;whatever &lt;code&gt;f&lt;/code&gt; is&lt;/td&gt;
&lt;td&gt;&lt;code&gt;f.newCall(request)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row terminates in &lt;code&gt;something.newCall(request)&lt;/code&gt;. That is the same call you wrote by hand at Step 3 of the lifecycle in &lt;em&gt;Sending a request without Retrofit&lt;/em&gt;. Retrofit is doing that step for you on every method call. &lt;code&gt;.callFactory(...)&lt;/code&gt; is you choosing which factory it uses.&lt;/p&gt;

&lt;p&gt;The two builds below are behaviorally identical:&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="c1"&gt;// A: the sugar&lt;/span&gt;
&lt;span class="nc"&gt;Retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;okHttpClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// B: the same thing, longhand. The lambda is a pass-through that adds nothing.&lt;/span&gt;
&lt;span class="nc"&gt;Retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;okHttpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.client(x)&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; &lt;code&gt;.callFactory { x.newCall(it) }&lt;/code&gt; with nothing in the middle. So why would you ever write form B? Because the lambda is a &lt;strong&gt;hook&lt;/strong&gt;. It is a line of your own code that runs at the moment each &lt;code&gt;Call&lt;/code&gt; is created. That one line, executed per outgoing request, is what the three use cases below exploit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do in the hook
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use case 1: defer the cost of building the client
&lt;/h3&gt;

&lt;p&gt;Building an &lt;code&gt;OkHttpClient&lt;/code&gt; with a disk &lt;code&gt;Cache&lt;/code&gt; touches the filesystem. Filesystem I/O is work you probably do not want on your app-startup critical path. But Retrofit is often built eagerly in a DI graph at startup, and &lt;code&gt;.client(x)&lt;/code&gt; demands a fully-built client &lt;em&gt;right then&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The hook lets you defer building the client until the first request actually fires:&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="c1"&gt;// The client is NOT built here. lazy {} only stores the recipe.&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lazyClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cacheDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;MAX_CACHE_SIZE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// disk I/O we want off the startup path&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authInterceptor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;retrofit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="c1"&gt;// .value runs the recipe the first time a request fires,&lt;/span&gt;
        &lt;span class="c1"&gt;// then caches the client forever.&lt;/span&gt;
        &lt;span class="n"&gt;lazyClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;span class="nf"&gt;addConverterFactory&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;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timing shift is where the win comes from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.client(okHttpClient)         build client → build Retrofit → (later) first request
                              ▲ disk cache opens at STARTUP

.callFactory { lazy.value }   build Retrofit → (later) first request → build client HERE
                                                                      ▲ disk cache opens on first use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same field on Retrofit, same eventual &lt;code&gt;newCall(request)&lt;/code&gt;. Only &lt;em&gt;when&lt;/em&gt; the expensive object is constructed moves. The hook is what let it move.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use case 2: use different clients for different requests
&lt;/h3&gt;

&lt;p&gt;This is the strongest justification for &lt;code&gt;callFactory&lt;/code&gt; existing at all, and it is worth spending the most time on.&lt;/p&gt;

&lt;p&gt;Some request-level concerns are actually &lt;strong&gt;client-level&lt;/strong&gt; properties in OkHttp. Timeouts are the clearest example. &lt;code&gt;readTimeout&lt;/code&gt;, &lt;code&gt;writeTimeout&lt;/code&gt;, &lt;code&gt;connectTimeout&lt;/code&gt;, and &lt;code&gt;callTimeout&lt;/code&gt; are all set on &lt;code&gt;OkHttpClient.Builder&lt;/code&gt;. They are baked into the client at construction time.&lt;/p&gt;

&lt;p&gt;That leads to a very common problem. Suppose you have one endpoint that runs an on-demand report and reliably takes ninety seconds to respond. Every other endpoint in the app responds in under a second, and you have set a 10-second read timeout on your main client to catch stuck requests early. The report endpoint will always time out.&lt;/p&gt;

&lt;p&gt;You cannot solve this with an interceptor. An interceptor runs &lt;em&gt;inside&lt;/em&gt; the client. It has access to the request and the response. It does not have access to the timeouts of the client hosting it, and even if it did, changing them mid-chain would not be safe. The property lives one level above where the interceptor executes.&lt;/p&gt;

&lt;p&gt;What you need is to pick a &lt;em&gt;different client instance&lt;/em&gt; for that one call. That is exactly what the hook is for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: create two client instances.&lt;/strong&gt; Use &lt;code&gt;newBuilder()&lt;/code&gt; so that the second client shares the connection pool, dispatcher, and cache with the first. This matters. A fresh &lt;code&gt;OkHttpClient.Builder().build()&lt;/code&gt; would create a second, independent connection pool and thread pool, which is wasteful and defeats the point of &lt;code&gt;OkHttpClient&lt;/code&gt; being a heavy singleton.&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;baseClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authInterceptor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// newBuilder() clones the config but SHARES the underlying resources.&lt;/span&gt;
&lt;span class="c1"&gt;// This is a config variant, not a second HTTP stack.&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;longRunningClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TimeUnit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SECONDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: mark the endpoint that needs the second client.&lt;/strong&gt; Retrofit's &lt;code&gt;@Tag&lt;/code&gt; parameter annotation attaches an object to the underlying OkHttp &lt;code&gt;Request&lt;/code&gt; as a tag. That tag is later readable via &lt;code&gt;request.tag(SomeClass::class.java)&lt;/code&gt;, which is exactly what the hook can inspect. Define a marker singleton and take a parameter of that type on the endpoint:&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="c1"&gt;// A marker singleton, used only as a request tag.&lt;/span&gt;
&lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;LongRunning&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ReportApi&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reports/{id}/export"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;exportReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;id&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="nd"&gt;@Tag&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LongRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LongRunning&lt;/span&gt;   &lt;span class="c1"&gt;// default so callers do not pass it&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;ReportDto&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@Tag&lt;/code&gt; is a parameter annotation on Retrofit interfaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: in the hook, inspect the tag and pick the right client.&lt;/strong&gt;&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;retrofit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LongRunning&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;longRunningClient&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;baseClient&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every request that flows through &lt;code&gt;exportReport&lt;/code&gt; now gets the 120-second timeout client. Every other request keeps the 10-second timeout. Both share the same connection pool underneath.&lt;/p&gt;

&lt;p&gt;This pattern generalises well. The client variant can differ in anything that lives on &lt;code&gt;OkHttpClient.Builder&lt;/code&gt;, not just timeouts. A different event listener for a specific set of endpoints, a different SSL configuration for a legacy backend, a different socket factory for a niche transport case. The mechanism is always the same: a marker tag on the request, a branch in the hook, a &lt;code&gt;newCall&lt;/code&gt; on the chosen client.&lt;/p&gt;

&lt;p&gt;This is the use case an interceptor structurally cannot cover. It is where &lt;code&gt;callFactory&lt;/code&gt; earns its place in the API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use case 3: rewriting the request before it becomes a &lt;code&gt;Call&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This one is included for completeness because you will see it in the wild, not because it is the recommended pattern. In the hook, you have the &lt;code&gt;Request&lt;/code&gt; in your hand. You can rewrite it before handing it to the client:&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;retrofit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rewritten&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;rewriteHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// route to a regional host at runtime&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rewritten&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;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. It also almost always belongs in an interceptor instead. The next section is about why.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;callFactory&lt;/code&gt; vs interceptors
&lt;/h2&gt;

&lt;p&gt;Both &lt;code&gt;callFactory&lt;/code&gt; and interceptors let you touch a request on its way out, so they look interchangeable. They are not. The decisive difference is &lt;em&gt;where in the lifecycle each one runs&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;callFactory hook           interceptor chain
       │                          │
  BEFORE a Call exists       INSIDE a Call, during execution
  runs ONCE per call         app interceptor: once per call
                             network interceptor: once PER network request
                                                  (each redirect, each retry)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fuller picture:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Call.Factory&lt;/code&gt; hook&lt;/th&gt;
&lt;th&gt;Application interceptor&lt;/th&gt;
&lt;th&gt;Network interceptor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runs&lt;/td&gt;
&lt;td&gt;once, before the &lt;code&gt;Call&lt;/code&gt; exists&lt;/td&gt;
&lt;td&gt;once per &lt;code&gt;Call&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;once per network request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sees redirects and retries&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes, fires again on each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can retry or short-circuit&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can read/rewrite headers and URL&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can swap client-level config (timeouts, pool)&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can choose which client instance runs the call&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access to the served &lt;code&gt;Response&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the table as two columns of "only here":&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use an interceptor when&lt;/strong&gt; the job concerns the request or response &lt;em&gt;content or flow&lt;/em&gt;: adding auth headers, logging, retrying on &lt;code&gt;401&lt;/code&gt;, caching decisions, rewriting URLs or headers, short-circuiting with a canned response. Interceptors are strictly more capable for these. They can see the response, they can retry, they can short-circuit. &lt;code&gt;callFactory&lt;/code&gt; can do none of those. Request mutation (Use case 3 above) belongs here, not in the factory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;callFactory&lt;/code&gt; only when&lt;/strong&gt; the job is structurally impossible for an interceptor because it concerns the &lt;em&gt;client object itself&lt;/em&gt;, not the request flowing through it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deferring client construction (Use case 1). An interceptor cannot defer building the very client it lives inside.&lt;/li&gt;
&lt;li&gt;Choosing which client instance handles the call (Use case 2). Timeouts, socket factories, and pools are client properties. An interceptor runs inside one fixed client and cannot switch to another.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-line rule: &lt;code&gt;callFactory&lt;/code&gt; is a strictly weaker hook than an interceptor for anything to do with request or response content, so reach for it only for the things an interceptor cannot do. If you see &lt;code&gt;callFactory&lt;/code&gt; used just to add a header or log a URL, an interceptor would have been the cleaner choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OkHttpClient&lt;/code&gt;, &lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Call&lt;/code&gt;, and &lt;code&gt;Response&lt;/code&gt; carry the whole OkHttp model. &lt;code&gt;Request&lt;/code&gt; is inert data. &lt;code&gt;Call&lt;/code&gt; is a &lt;code&gt;Request&lt;/code&gt; bound to an &lt;code&gt;OkHttpClient&lt;/code&gt;, ready to fire.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Call&lt;/code&gt; exists as an object because cancellation and in-flight inspection need a handle that is neither a &lt;code&gt;Request&lt;/code&gt; (no engine) nor a &lt;code&gt;Response&lt;/code&gt; (already over).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Call.Factory&lt;/code&gt; is a one-method &lt;code&gt;fun interface&lt;/code&gt; whose whole contract is &lt;code&gt;newCall(request): Call&lt;/code&gt;. &lt;code&gt;OkHttpClient&lt;/code&gt; implements it. When you wrote &lt;code&gt;client.newCall(request)&lt;/code&gt;, you were already calling into &lt;code&gt;Call.Factory&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Retrofit's field is typed &lt;code&gt;Call.Factory&lt;/code&gt;, not &lt;code&gt;OkHttpClient&lt;/code&gt;. It only ever calls &lt;code&gt;newCall&lt;/code&gt;. That is why Retrofit accepts any implementation, including a lambda.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.client(x)&lt;/code&gt; is a one-line forwarder to &lt;code&gt;.callFactory(x)&lt;/code&gt;. Same field. The lambda form of &lt;code&gt;.callFactory { ... }&lt;/code&gt; is a hook where your own code runs on every outgoing &lt;code&gt;Call&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Reserve &lt;code&gt;callFactory&lt;/code&gt; for the things interceptors cannot do: deferring client construction, and choosing which client instance runs the request. Everything else about the request or response belongs in an interceptor.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>okhttp</category>
      <category>mobile</category>
      <category>networking</category>
    </item>
    <item>
      <title>Handling Empty Response Bodies in Retrofit with a Null-on-Empty Converter Factory</title>
      <dc:creator>Anubhav</dc:creator>
      <pubDate>Wed, 29 Jul 2026 16:41:24 +0000</pubDate>
      <link>https://dev.to/_anubhav/handling-empty-response-bodies-in-retrofit-with-a-null-on-empty-converter-factory-358g</link>
      <guid>https://dev.to/_anubhav/handling-empty-response-bodies-in-retrofit-with-a-null-on-empty-converter-factory-358g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wmpj90sgit78fhnwjev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wmpj90sgit78fhnwjev.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Retrofit makes HTTP calls on Android feel almost effortless. You define an interface, annotate the methods, plug in a converter, and it takes care of the rest. But one of the situations where this smooth pipeline can break is when your server returns an empty response body. Your JSON converter throws a parsing exception and your app crashes.&lt;/p&gt;

&lt;p&gt;In this article, we will look at why this happens, what Retrofit already handles for you, and how to build a small custom converter factory (called &lt;code&gt;NullOnEmptyConverterFactory&lt;/code&gt; by convention) that fixes the problem cleanly across your entire API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Error You Will Actually See
&lt;/h2&gt;

&lt;p&gt;Suppose you have a Retrofit interface that is declared to return a &lt;code&gt;User&lt;/code&gt; object after creating a new account:&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="nd"&gt;@POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Body&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server acknowledges the creation with a &lt;code&gt;201 Created&lt;/code&gt;, but sends no body back. When your code runs, you see this in Logcat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java.io.EOFException: End of input at line 1 column 1 path $
    at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1414)
    at com.google.gson.stream.JsonReader.peek(JsonReader.java:429)
    at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:33)
    at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:154)
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gson (or whichever JSON converter you use) is trying to parse the empty stream, sees no content, and reports it as malformed JSON. The crash is confusing because the network call itself succeeded. It is the &lt;code&gt;deserialization&lt;/code&gt; step that failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Happens, and Why Only Sometimes
&lt;/h2&gt;

&lt;p&gt;One detail worth pausing on is that Retrofit &lt;em&gt;does&lt;/em&gt; handle empty responses, but only for two specific HTTP status codes: &lt;code&gt;204 No Content&lt;/code&gt; and &lt;code&gt;205 Reset Content&lt;/code&gt;. To see why, here is a simplified sketch of what Retrofit does internally when a response arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified sketch of what Retrofit does in OkHttpCall.parseResponse&lt;/span&gt;
&lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;okhttp3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Response&lt;/span&gt; &lt;span class="n"&gt;rawResponse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rawResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Empty-body status codes short-circuit here&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;205&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rawResponse&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Every other status code goes through the converter chain&lt;/span&gt;
    &lt;span class="no"&gt;T&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;responseConverter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rawResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rawResponse&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;204&lt;/code&gt; and &lt;code&gt;205&lt;/code&gt;, Retrofit returns &lt;code&gt;null&lt;/code&gt; for the body immediately, without ever calling your converter. The problem shows up when a server returns an empty body under a &lt;em&gt;different&lt;/em&gt; status code, most commonly &lt;code&gt;200 OK&lt;/code&gt; or &lt;code&gt;201 Created&lt;/code&gt;. In those cases, Retrofit assumes there is content to parse, hands the stream to your converter, and the converter fails.&lt;/p&gt;

&lt;p&gt;Strictly speaking, a server should return &lt;code&gt;204&lt;/code&gt; when there is no content, but in practice many APIs return &lt;code&gt;200&lt;/code&gt; or &lt;code&gt;201&lt;/code&gt; with an empty body. You either need to accept that reality and handle it on the client, or push a fix upstream. Since you often cannot change the backend, we need a client-side solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Refresher on &lt;code&gt;Converter.Factory&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before writing the fix, it helps to recall how Retrofit picks a converter. When you call &lt;code&gt;addConverterFactory(...)&lt;/code&gt; on the builder, you are adding to an ordered list. For each response type, Retrofit walks that list from top to bottom, asking each factory whether it can produce a &lt;code&gt;Converter&lt;/code&gt; for the given type. The first factory that returns a non-null converter wins.&lt;/p&gt;

&lt;p&gt;That ordering is what makes the null-on-empty pattern work. If we register our own factory &lt;em&gt;before&lt;/em&gt; the JSON converter, we get first crack at every response. We can then decide whether to short-circuit (when the body is empty) or delegate to the JSON converter (when it is not).&lt;/p&gt;

&lt;p&gt;This is a straightforward application of the decorator pattern to Retrofit's converter chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;Here is the Kotlin 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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NullOnEmptyConverterFactory&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Factory&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;responseBodyConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Annotation&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="n"&gt;retrofit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Retrofit&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="n"&gt;retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nextResponseBodyConverter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentLength&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Java equivalent, for teams still on Java:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NullOnEmptyConverterFactory&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Factory&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseBody&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;responseBodyConverter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;Type&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Annotation&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Retrofit&lt;/span&gt; &lt;span class="n"&gt;retrofit&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseBody&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="n"&gt;retrofit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextResponseBodyConverter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;annotations&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentLength&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is how you wire it into your &lt;code&gt;Retrofit&lt;/code&gt; instance:&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;retrofit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retrofit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.example.com/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addConverterFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NullOnEmptyConverterFactory&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addConverterFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GsonConverterFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;NullOnEmptyConverterFactory&lt;/code&gt; is registered &lt;em&gt;first&lt;/em&gt;. This is not optional. If Gson comes first, it will claim every type and our factory will never be reached.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works, Step by Step
&lt;/h2&gt;

&lt;p&gt;Let us trace what happens when a response comes in.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Retrofit receives the HTTP response and enters &lt;code&gt;parseResponse&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the status code is &lt;code&gt;204&lt;/code&gt; or &lt;code&gt;205&lt;/code&gt;, Retrofit returns &lt;code&gt;null&lt;/code&gt; without consulting the converter chain at all.&lt;/li&gt;
&lt;li&gt;For any other status code, Retrofit needs a &lt;code&gt;Converter&amp;lt;ResponseBody, T&amp;gt;&lt;/code&gt; to translate the body into the declared return type. It walks the registered factories in order.&lt;/li&gt;
&lt;li&gt;Our &lt;code&gt;NullOnEmptyConverterFactory&lt;/code&gt; sits at the front of the list, so Retrofit asks it first.&lt;/li&gt;
&lt;li&gt;Inside our factory, we call &lt;code&gt;retrofit.nextResponseBodyConverter(this, type, annotations)&lt;/code&gt;. The first argument, &lt;code&gt;this&lt;/code&gt;, fills Retrofit's &lt;code&gt;skipPast&lt;/code&gt; parameter. It tells Retrofit to look for the &lt;em&gt;next&lt;/em&gt; factory that can handle this type, skipping ourselves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To see why this argument matters, imagine we passed &lt;code&gt;null&lt;/code&gt; instead. Retrofit's &lt;code&gt;nextResponseBodyConverter&lt;/code&gt; would start iterating the factory list from the beginning, find our &lt;code&gt;NullOnEmptyConverterFactory&lt;/code&gt; again, and call its &lt;code&gt;responseBodyConverter&lt;/code&gt; method. That method would call &lt;code&gt;nextResponseBodyConverter(null, ...)&lt;/code&gt; once more, which would find us yet again, and so on. The call stack would look 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;   NullOnEmptyConverterFactory.responseBodyConverter
     -&amp;gt; Retrofit.nextResponseBodyConverter(skipPast = null)
       -&amp;gt; NullOnEmptyConverterFactory.responseBodyConverter
         -&amp;gt; Retrofit.nextResponseBodyConverter(skipPast = null)
           -&amp;gt; ... (repeats until StackOverflowError)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Passing &lt;code&gt;this&lt;/code&gt; as &lt;code&gt;skipPast&lt;/code&gt; breaks the cycle. Retrofit skips over our factory during the lookup and moves on to the next one in the chain.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The lookup lands on the next matching factory, which in a typical setup is the Kotlinx Serialization (or Moshi, Gson, Jackson, etc.) converter. That becomes our delegate.&lt;/li&gt;
&lt;li&gt;We return a small wrapping converter. When Retrofit invokes it with the response body, we check &lt;code&gt;body.contentLength()&lt;/code&gt;. If it is zero, we return &lt;code&gt;null&lt;/code&gt; and the JSON converter is never called. If it is non-zero, we hand the body to the delegate and let it do its normal work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The net effect is that empty bodies produce a clean &lt;code&gt;null&lt;/code&gt; value, and non-empty bodies flow through exactly as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls to Watch Out For
&lt;/h2&gt;

&lt;p&gt;The pattern is simple, but there are a few details that catch people out.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Your Return Type Must Be Nullable
&lt;/h3&gt;

&lt;p&gt;If your factory returns &lt;code&gt;null&lt;/code&gt; but your Retrofit interface declares a non-null return type, you will still get a crash, just farther down the pipeline. Make sure your interface reflects the reality that the body might be absent:&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="c1"&gt;// Kotlin coroutines&lt;/span&gt;
&lt;span class="nd"&gt;@POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Body&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

&lt;span class="c1"&gt;// Kotlin with Call&lt;/span&gt;
&lt;span class="nd"&gt;@POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"users"&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;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Body&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CreateUserRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Java, you rely on the fact that &lt;code&gt;Response&amp;lt;T&amp;gt;.body()&lt;/code&gt; is already annotated &lt;code&gt;@Nullable&lt;/code&gt;, so you just need to check for &lt;code&gt;null&lt;/code&gt; at the call site.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;contentLength() == 0&lt;/code&gt; Is Not a Perfect Check
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ResponseBody.contentLength()&lt;/code&gt; returns the value of the &lt;code&gt;Content-Length&lt;/code&gt; header when it is present, and &lt;code&gt;-1&lt;/code&gt; when it is not. Servers using chunked transfer encoding often omit the header, so an empty chunked response returns &lt;code&gt;-1&lt;/code&gt;, and our simple check misses it.&lt;/p&gt;

&lt;p&gt;For most real-world REST APIs, contentLength() == 0L is enough. If you know your server uses chunked encoding for some endpoints, you can peek at the underlying stream to be more thorough. Retrofit response bodies use Okio under the hood, a small I/O library from Square that OkHttp uses for all its byte-level work. You can think of it as a friendlier, more efficient alternative to Java's InputStream and OutputStream. Calling body.source() gives us an Okio BufferedSource, which is essentially a stream with a smart buffer in front of it, and we can peek into that buffer before deciding whether to delegate:&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ResponseBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contentLength&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="nd"&gt;@Converter&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;source&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="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&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="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0L&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;delegate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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;Here, source.request(1) asks Okio to try to read at least one byte into the buffer. If nothing arrives, the body is effectively empty. The peeked byte remains in the buffer, so the delegate can still read the full stream normally when we do call it&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Registration Order Is Not Negotiable
&lt;/h3&gt;

&lt;p&gt;I said this above, but it is worth repeating because it is a frequent source of confusion when this pattern seems not to work. Retrofit does not reorder factories by specificity. It walks them in the exact order you register them, and the first one that claims a type wins. &lt;code&gt;NullOnEmptyConverterFactory&lt;/code&gt; must come before the JSON converter, always.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Body Can Only Be Read Once
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ResponseBody&lt;/code&gt; wraps a network stream that is consumed on read. Our &lt;code&gt;contentLength()&lt;/code&gt; check reads header metadata, so it does not touch the stream. If you go with the defensive &lt;code&gt;source.request(1)&lt;/code&gt; version, Okio buffers the peeked byte, so the delegate can still consume the body without missing anything. Either version is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use This Factory
&lt;/h2&gt;

&lt;p&gt;The null-on-empty factory is a broad, cross-cutting fix. There are situations where a narrower solution reads better.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The endpoint always returns no content.&lt;/strong&gt; If a specific endpoint is documented to never return a body, declare it as &lt;code&gt;Call&amp;lt;Unit&amp;gt;&lt;/code&gt; in Kotlin or &lt;code&gt;Call&amp;lt;Void&amp;gt;&lt;/code&gt; in Java. Retrofit already treats these types specially and does not invoke a JSON converter for them. This is more expressive than reaching for the null-on-empty pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can fix the backend.&lt;/strong&gt; If the server is under your control and it is returning &lt;code&gt;200&lt;/code&gt; with an empty body when it means "no content," changing it to &lt;code&gt;204&lt;/code&gt; is the correct HTTP behavior and eliminates the need for this workaround entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reach for &lt;code&gt;NullOnEmptyConverterFactory&lt;/code&gt; when the same endpoint can genuinely return either a populated body or an empty one, or when many endpoints across your API might occasionally return empty bodies and you want a single, uniform fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The null-on-empty converter factory solves a specific but very common problem: JSON converters crash on empty response bodies when the status code is not &lt;code&gt;204&lt;/code&gt; or &lt;code&gt;205&lt;/code&gt;. The key takeaways are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retrofit already handles &lt;code&gt;204&lt;/code&gt; and &lt;code&gt;205&lt;/code&gt; internally by returning &lt;code&gt;null&lt;/code&gt; without invoking the converter. The problem is limited to other status codes like &lt;code&gt;200&lt;/code&gt; and &lt;code&gt;201&lt;/code&gt; that arrive with an empty body.&lt;/li&gt;
&lt;li&gt;The fix is a small &lt;code&gt;Converter.Factory&lt;/code&gt; that registers &lt;em&gt;before&lt;/em&gt; your JSON converter, delegates to the next factory in the chain, and short-circuits to &lt;code&gt;null&lt;/code&gt; when the body is empty.&lt;/li&gt;
&lt;li&gt;The call &lt;code&gt;retrofit.nextResponseBodyConverter(this, type, annotations)&lt;/code&gt; fetches the delegate. The first argument tells Retrofit to skip past our own factory during that lookup, which is what prevents infinite recursion.&lt;/li&gt;
&lt;li&gt;For the pattern to actually surface &lt;code&gt;null&lt;/code&gt; to your code, your Retrofit interface must declare nullable return types.&lt;/li&gt;
&lt;li&gt;Registration order is critical: &lt;code&gt;NullOnEmptyConverterFactory&lt;/code&gt; must come first, before Gson, Moshi, or any other JSON converter factory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those pieces in place, your networking layer becomes resilient to a whole class of real-world server behavior that Retrofit does not handle out of the box, with less than twenty lines of code.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>okhttp</category>
      <category>networking</category>
    </item>
    <item>
      <title>The Honour System Running Your Phone's Speaker</title>
      <dc:creator>Anubhav</dc:creator>
      <pubDate>Fri, 19 Jun 2026 02:33:26 +0000</pubDate>
      <link>https://dev.to/_anubhav/the-honour-system-running-your-phones-speaker-5b64</link>
      <guid>https://dev.to/_anubhav/the-honour-system-running-your-phones-speaker-5b64</guid>
      <description>&lt;p&gt;&lt;em&gt;Part one of a short series on who actually controls the audio coming out of your Android phone, and why almost none of it is the app you think.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few days ago I was listening to music on my phone when I opened an unrelated app, one built around an endless feed. The very first screen autoplayed a short video. My music stopped. Not paused and then resumed, not lowered for a moment under the clip. It simply stopped, and I had to go back and press play again.&lt;/p&gt;

&lt;p&gt;This is the kind of thing that is easy to never think about. It happens constantly. But this time it nagged at me, because the app that silenced my music was not a media app. It had no obvious business being in charge of my audio. And yet a five-second clip I never asked to watch reached across the system and shut down a dedicated music player. I wanted to understand how a random app gets that power, and whether it is even power at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  One speaker and a dozen claimants
&lt;/h2&gt;

&lt;p&gt;At any given moment, there is usually exactly one stream of sound that I actually care about, but there are dozens of apps installed, any number of which might want to make noise at the same time. Two apps deciding to play audio at once is not some rare edge case. It is the ordinary condition of a phone. A navigation prompt needs to talk over a podcast. A video call wants the channel a song is currently using. A game wants to play effects while a streaming app sits paused in the background.&lt;/p&gt;

&lt;p&gt;So someone, somewhere, has to arbitrate. The question that would not leave me alone was where that arbitration lives and what shape it takes. Is there a single authority that hands out the speaker like a token? Does the loudest or newest app simply win? My instinct said this had to be a system-level concern, because no single app can see what every other app is doing. But the thing I had actually watched happen, a non-media app casually overruling a media app, hinted that the rules were stranger than a tidy priority list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system asks, it does not take
&lt;/h2&gt;

&lt;p&gt;The piece I had been missing has a name: &lt;strong&gt;audio focus&lt;/strong&gt;. Once I started thinking in those terms, the behaviour stopped looking like a hostile takeover and started looking like something far more polite, almost to a fault.&lt;/p&gt;

&lt;p&gt;My understanding is that an app does not seize the speaker. It asks for it. When an app wants to play sound, the well-behaved thing to do is request audio focus from the system through &lt;code&gt;AudioManager&lt;/code&gt;, the per-app gateway into Android's audio service. The system tracks who currently holds focus, conceptually a stack of requests, and when a new app asks, the previous holder is told it has lost focus. Here is the part that reframed everything for me: nobody forces the previous app to go quiet. The system taps it on the shoulder and informs it that someone else has asked to play. What happens next is left entirely to the app that was interrupted.&lt;/p&gt;

&lt;p&gt;So my music was never shut down by force. The player that was running received a message saying it had lost focus, and its own code decided to pause. The autoplay video did not reach into the music player and stop it. It asked the system for the floor, and the music player chose to yield.&lt;/p&gt;

&lt;h3&gt;
  
  
  The vocabulary of an interruption
&lt;/h3&gt;

&lt;p&gt;What convinced me this was deliberate design rather than a lucky accident is the vocabulary the system uses for losing focus. It is not a single off switch. When an app loses focus, it is told roughly how it lost it, and the names of those signals read like a small grammar of courtesy.&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="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;focusListener&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AudioManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OnAudioFocusChangeListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;AudioManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AUDIOFOCUS_LOSS&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;// someone took the floor indefinitely&lt;/span&gt;

        &lt;span class="nc"&gt;AudioManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AUDIOFOCUS_LOSS_TRANSIENT&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;// a brief interruption, focus should return&lt;/span&gt;

        &lt;span class="nc"&gt;AudioManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lowerVolume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// keep playing, just step aside quietly&lt;/span&gt;

        &lt;span class="nc"&gt;AudioManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AUDIOFOCUS_GAIN&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="c1"&gt;// the floor is yours again&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;Reading that list told me more about the intent than any specification could. &lt;code&gt;AUDIOFOCUS_LOSS&lt;/code&gt; is a permanent goodbye: another app has taken the floor and does not expect to hand it back soon, so the correct response is to stop and let go. &lt;code&gt;AUDIOFOCUS_LOSS_TRANSIENT&lt;/code&gt; is a short interruption, the kind an incoming call or a navigation prompt creates, with the expectation that focus returns shortly. And then there is the one I find most telling, &lt;code&gt;AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK&lt;/code&gt;, which does not ask the music to stop at all. It asks it to drop its volume and keep playing underneath, the way Maps quiets your music to a murmur while it tells you to turn left, then lets it rise again afterward.&lt;/p&gt;

&lt;p&gt;This is why my music stopped outright instead of ducking or pausing and resuming. My guess is that the autoplay video requested a full, indefinite gain, which handed my music player an &lt;code&gt;AUDIOFOCUS_LOSS&lt;/code&gt;, the permanent kind. The player did the right thing for a permanent loss. It stopped, and it did not attempt to resume on its own. Compare that to a phone call, which requests transient focus, hands the music a transient loss, and lets it resume the instant the call ends. The same machinery, a different degree of politeness, and you feel the difference as a user without ever needing the words for it.&lt;/p&gt;

&lt;p&gt;What makes this almost funny is that I doubt anyone at the company behind that feed app consciously decided to interrupt my music. If their video player is built on one of the common media libraries, requesting audio focus is often the default. Somewhere deep in the stack, a sensible library made a reasonable assumption about how media should behave, and that assumption was enough to stop my music.&lt;/p&gt;

&lt;h2&gt;
  
  
  An honour system, with everything that implies
&lt;/h2&gt;

&lt;p&gt;The detail I keep turning over is that this whole arrangement runs on trust. Audio focus is advisory. The system can tell an app it has lost focus, but through this mechanism alone it cannot force the app to actually fall silent. A lazily written app can simply ignore the loss and keep playing, and you are left with two streams wrestling over your ears. Most of us have met that app.&lt;/p&gt;

&lt;p&gt;So why would the designers choose a cooperative model over a strict one, where the system rips audio away from whoever was holding it? My guess is that the strict version is quietly worse. A forced handover would mean the system decides, for every app, what losing audio ought to mean. Should the sound stop, or pause, or duck? Only the app that was playing knows whether it is a podcast that must pause precisely so you do not miss a sentence, or an ambient track that should simply fade. By making the loss a message rather than a command, the system hands that decision to the one party with enough context to get it right. The cost is plain: it only works when apps cooperate. The reward is that, when they do, the result is far more humane than any central rule could manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The floor underneath the floor
&lt;/h2&gt;

&lt;p&gt;What I find quietly strange is that the speaker on a device I own runs almost entirely on an honour system. The app playing my music was never truly in control of whether it kept playing. It was just the most recent voice in a polite, system-wide conversation about who gets the floor, and it stepped back the moment it was asked.&lt;/p&gt;

&lt;p&gt;But this only explains why one sound stops when another starts. It says nothing about the moments when sounds do not stop at all: a notification chiming cleanly over the top of a song, an alarm and music sounding in the very same instant. If audio focus were the entire story, those moments should not be possible. Which means the floor I have been describing is not really one floor, and something beneath it is doing work I have not yet accounted for. That is where I want to look next.&lt;/p&gt;

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