<?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>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;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>
