<?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: Siddharth Pandalai</title>
    <description>The latest articles on DEV Community by Siddharth Pandalai (@darkpandawarrior).</description>
    <link>https://dev.to/darkpandawarrior</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%2F4036798%2Fcbc9c397-ab12-4008-91ec-0deac4a666df.jpg</url>
      <title>DEV Community: Siddharth Pandalai</title>
      <link>https://dev.to/darkpandawarrior</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darkpandawarrior"/>
    <language>en</language>
    <item>
      <title>collectAsState is quietly leaking your work</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Fri, 04 Sep 2026 04:43:46 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/collectasstate-is-quietly-leaking-your-work-4ak0</link>
      <guid>https://dev.to/darkpandawarrior/collectasstate-is-quietly-leaking-your-work-4ak0</guid>
      <description>&lt;p&gt;Here is a line you have written a hundred times:&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;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks harmless. It is the standard way to turn a &lt;code&gt;Flow&lt;/code&gt; into Compose state. And on a screen with live data, it quietly keeps working long after the user has walked away.&lt;/p&gt;

&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%2Feyxtsb0wytc3inkvbozt.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%2Feyxtsb0wytc3inkvbozt.png" alt="The Recomposer, drawn as a specimen plate. It redraws the room every time you blink. Labelled THE RECOMPOSER, @Composable." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The screen nobody is watching
&lt;/h2&gt;

&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%2F6yt3wlmslxvs53q1ufy3.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%2F6yt3wlmslxvs53q1ufy3.png" alt="The one word: collectAsState WithLifecycle" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;collectAsState&lt;/code&gt; starts collecting the flow and stops only when the composable leaves the composition. That sounds right until you remember what backgrounding an app does, which is almost nothing to the composition. Send the app to the background and the composable is still composed. So the collection keeps running. The flow keeps emitting. Your ViewModel keeps fetching, computing, and pushing new state to a screen that is not on screen.&lt;/p&gt;

&lt;p&gt;On a live price ticker or a location feed, that is real battery and mobile data spent in the dark. It is also a quiet source of bugs, because off-screen updates can pile up and then hit the user with a jump of stale changes the moment they return.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-word fix
&lt;/h2&gt;

&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%2Fx7y0joj1q6f6dvvms6gk.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%2Fx7y0joj1q6f6dvvms6gk.png" alt="The rule: Make lifecycle the default" width="800" height="1000"&gt;&lt;/a&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;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsStateWithLifecycle&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;collectAsStateWithLifecycle&lt;/code&gt; ties the collection to the lifecycle owner. When the lifecycle drops below &lt;code&gt;STARTED&lt;/code&gt;, it stops collecting. When it comes back, it resumes. If your upstream is a &lt;code&gt;stateIn&lt;/code&gt; or &lt;code&gt;shareIn&lt;/code&gt; flow with &lt;code&gt;WhileSubscribed&lt;/code&gt;, the whole pipeline can now go cold while hidden and warm back up on return. Nobody pays for a screen that is not visible.&lt;/p&gt;

&lt;p&gt;It lives in &lt;code&gt;androidx.lifecycle.runtime.compose&lt;/code&gt;. Add the dependency, change the one call, and the leak is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it the default
&lt;/h2&gt;

&lt;p&gt;Treat &lt;code&gt;collectAsStateWithLifecycle&lt;/code&gt; as the default and plain &lt;code&gt;collectAsState&lt;/code&gt; as the exception. The rare time you actually want collection to continue while the screen is hidden should be a deliberate, commented decision, not something you got by reaching for the shorter name out of habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&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%2F6wa0pgr8nzp0sq7hie0m.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%2F6wa0pgr8nzp0sq7hie0m.png" alt="The payload: Lifecycle is not a thing you bolt on later" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lifecycle awareness is not a nice-to-have you bolt on later. It is the line between work that stops when the user leaves and work that runs forever in the background. The Recomposer does not mind an empty room. It will happily keep redrawing it. Your job is to turn the lights off when everyone goes home.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 6 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **Ghosts In The Recomposition&lt;/em&gt;* · Featuring: The Recomposer*&lt;br&gt;
&lt;em&gt;← Previously in this series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/lessons/2026-07-24-lazycolumn-recomposition/article.md" rel="noopener noreferrer"&gt;Your LazyColumn recomposes on every scroll&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/ghosts-in-the-recomposition.md" rel="noopener noreferrer"&gt;Ghosts In The Recomposition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
      <category>flow</category>
      <category>lifecycle</category>
    </item>
    <item>
      <title>expect/actual is the wrong default in KMP</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Wed, 02 Sep 2026 05:41:21 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/expectactual-is-the-wrong-default-in-kmp-5g7p</link>
      <guid>https://dev.to/darkpandawarrior/expectactual-is-the-wrong-default-in-kmp-5g7p</guid>
      <description>&lt;p&gt;Every Kotlin Multiplatform tutorial teaches &lt;code&gt;expect&lt;/code&gt;/&lt;code&gt;actual&lt;/code&gt; in the first ten minutes. It is the headline feature, the thing that makes KMP feel like magic. So new teams learn it first and then reach for it every single time common code needs to touch a platform.&lt;/p&gt;

&lt;p&gt;That habit is how shared modules turn into a knot. &lt;code&gt;expect&lt;/code&gt;/&lt;code&gt;actual&lt;/code&gt; is a fine tool for a narrow job, and the wrong default for most of them.&lt;/p&gt;

&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%2Fd7509eo8hydrkk4i0gke.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%2Fd7509eo8hydrkk4i0gke.png" alt="The Understudy, drawn as a specimen plate. Cast by name at compile time. No swapping mid-run. Labelled THE UNDERSTUDY, actual." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What expect/actual actually is
&lt;/h2&gt;

&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%2Fr0tgwpyi9pxocjuk1ygu.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%2Fr0tgwpyi9pxocjuk1ygu.png" alt="The alternative: An interface in common, injected per platform" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is a compile-time binding by name. You declare something in common:&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;// commonMain&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Clipboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the compiler requires exactly one &lt;code&gt;actual&lt;/code&gt; per platform:&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;// androidMain&lt;/span&gt;
&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Clipboard&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* android impl */&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 binding is fixed at build time. There is one &lt;code&gt;actual&lt;/code&gt; for Android, one for iOS, and the compiler wires them in. That sounds convenient, and for the right case it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it hurts
&lt;/h2&gt;

&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%2Fu4yf2npcmxk8sijnod4u.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%2Fu4yf2npcmxk8sijnod4u.png" alt="How to choose: Weld, or hinge?" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The moment you want to do anything other than "one fixed body per platform," the weld gets in the way.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You cannot inject a fake.&lt;/strong&gt; There is no test &lt;code&gt;actual&lt;/code&gt;, so your common use case that
depends on &lt;code&gt;Clipboard&lt;/code&gt; cannot be tested in &lt;code&gt;commonTest&lt;/code&gt; without inventing a whole extra source set to satisfy the compiler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You cannot hold two implementations.&lt;/strong&gt; No real one and a fake one, no A/B, no decorator.
The compiler picks one per platform and that is that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your common code is coupled to platform declarations,&lt;/strong&gt; one weld per concern, spread
across storage, networking, clipboard, analytics, and everything else you reached for it with.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I lived this on PaymentsLab. Early common code used &lt;code&gt;expect&lt;/code&gt;/&lt;code&gt;actual&lt;/code&gt; for every platform seam. It compiled and shipped. Then I sat down to unit-test a payment use case in &lt;code&gt;commonTest&lt;/code&gt; and simply could not, because the seams had no fakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The better default: an interface, injected
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// commonMain&lt;/span&gt;
&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Clipboard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CopyReceiptUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Clipboard&lt;/span&gt;&lt;span class="p"&gt;)&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;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asText&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;Provide the real thing per platform, and hand it in through your DI graph (Koin, in our case):&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;// androidMain&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AndroidClipboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Clipboard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// commonTest&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeClipboard&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Clipboard&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&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;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Test&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;copies_receipt&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;fake&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeClipboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;CopyReceiptUseCase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last&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;Same platform bodies. Same behavior. But now the common code depends on an interface it owns, not on a platform declaration, and it is testable in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  So when is expect/actual right?
&lt;/h2&gt;

&lt;p&gt;For the small, fixed, one-per-platform joints where a stand-in makes no sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;typealias&lt;/code&gt; to a platform type (&lt;code&gt;expect class Uuid&lt;/code&gt;, actualized to the platform's UUID).&lt;/li&gt;
&lt;li&gt;A single top-level function with exactly one meaning per platform and nothing to test,
like reading a platform constant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are genuine welds. One joint, permanent, no reason to swap it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&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%2Fp0wccajlr66ml1x6d3nw.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%2Fp0wccajlr66ml1x6d3nw.png" alt="The payload: Reach for the tool that keeps your options open" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The KMP question is rarely "how do I reach the platform." It is "what do I hand my common code." Hand it an interface it owns, and inject the platform body. Keep &lt;code&gt;expect&lt;/code&gt;/&lt;code&gt;actual&lt;/code&gt; for the handful of places you actually want a weld.&lt;/p&gt;

&lt;p&gt;The Understudy is perfect for a small fixed role, cast by name, on stage every night. Just do not cast one for every part in the play.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 5 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **One Brain Two Bodies&lt;/em&gt;* · Featuring: The Understudy*&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/one-brain-two-bodies.md" rel="noopener noreferrer"&gt;One Brain Two Bodies&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>kmp</category>
      <category>multiplatform</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Your LazyColumn recomposes on every scroll</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Sun, 30 Aug 2026 06:26:12 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/your-lazycolumn-recomposes-on-every-scroll-3amj</link>
      <guid>https://dev.to/darkpandawarrior/your-lazycolumn-recomposes-on-every-scroll-3amj</guid>
      <description>&lt;p&gt;A feed screen felt heavy while scrolling, only on mid-range phones. Layout Inspector told the whole story: every visible row was recomposing on every scroll frame, including rows whose data had not changed at all.&lt;/p&gt;

&lt;p&gt;There was a ghost in the recomposition, and it had one cause.&lt;/p&gt;

&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%2Feh5eio50uw6vpnuiza8d.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%2Feh5eio50uw6vpnuiza8d.png" alt="The Recomposer, drawn as a specimen plate. It redraws the room every time you blink. Labelled THE RECOMPOSER, @Composable." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose can skip, if you let it
&lt;/h2&gt;

&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%2F5wxjxpbv2tkw4bpvgh13.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%2F5wxjxpbv2tkw4bpvgh13.png" alt="The cause: A List is not a stable type" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compose has a quiet superpower called skipping. When a composable's inputs have not changed, Compose skips re-running it entirely. That is most of why it is fast.&lt;/p&gt;

&lt;p&gt;Skipping depends on stability. To skip a composable, Compose has to prove its parameters did not change. Some types make that proof easy. Others make it impossible.&lt;/p&gt;

&lt;p&gt;A plain &lt;code&gt;List&lt;/code&gt; is one of the impossible ones. &lt;code&gt;List&lt;/code&gt; is an interface. The thing behind it could be a mutable list that changed under Compose's feet, and the compiler cannot rule that out. So Compose treats every &lt;code&gt;List&lt;/code&gt; parameter as unstable, assumes it might have changed, and recomposes the rows that read it. On a scrolling feed, that means redrawing everything you can see, constantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is the type
&lt;/h2&gt;

&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%2Fitlru8v72otait3qvnqp.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%2Fitlru8v72otait3qvnqp.png" alt="The fix: Let it prove nothing changed" width="800" height="1000"&gt;&lt;/a&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;// before: List is unstable, rows cannot skip&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&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="c1"&gt;// after: an immutable list is stable, skipping works&lt;/span&gt;
&lt;span class="nd"&gt;@Composable&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ImmutableList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;kotlinx.collections.immutable&lt;/code&gt; and build the list with &lt;code&gt;persistentListOf(...)&lt;/code&gt; or &lt;code&gt;.toImmutableList()&lt;/code&gt;. Now Compose knows the collection cannot change, and it skips rows whose data is unchanged.&lt;/p&gt;

&lt;p&gt;While you are there, give the items a key:&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;LazyColumn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&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="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;PostRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&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;Without keys, Compose tracks rows by position. Insert one at the top and every row below shifts identity, so more of them redraw than needed. A stable key ties a row to its data.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it for yourself
&lt;/h2&gt;

&lt;p&gt;Turn on recomposition counts in Layout Inspector and scroll. With an unstable list, the counter on every row climbs. Switch to an immutable list and add keys, and the counters go still. That stillness is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&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%2Fu93heqqj838oo09u28ha.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%2Fu93heqqj838oo09u28ha.png" alt="The payload: Compose is fast. You handed it a puzzle" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compose is not slow. It is fast by default. Almost every "Compose is janky" story is really the same story: someone handed a composable a type it could not reason about, so it stopped skipping and started repainting the house.&lt;/p&gt;

&lt;p&gt;The Recomposer only redraws the room when you give it a reason. Stop giving it reasons.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 4 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **Ghosts In The Recomposition&lt;/em&gt;* · Featuring: The Recomposer*&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/ghosts-in-the-recomposition.md" rel="noopener noreferrer"&gt;Ghosts In The Recomposition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>jetpackcompose</category>
      <category>recomposition</category>
      <category>stability</category>
    </item>
    <item>
      <title>The 5-second window that crashes your service</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Fri, 28 Aug 2026 15:00:33 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/the-5-second-window-that-crashes-your-service-5cmm</link>
      <guid>https://dev.to/darkpandawarrior/the-5-second-window-that-crashes-your-service-5cmm</guid>
      <description>&lt;p&gt;Our background service crashed for thousands of users, and the stack trace pointed at a line that does nothing wrong.&lt;/p&gt;

&lt;p&gt;The exception was &lt;code&gt;ForegroundServiceDidNotStartInTimeException&lt;/code&gt;. I had to look it up. And once I understood it, a whole class of "works on my machine" bugs made sense.&lt;/p&gt;

&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%2Fooymtx36mvf9he4zsa5u.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%2Fooymtx36mvf9he4zsa5u.png" alt="Doze The Jailer, drawn as a specimen plate. You get five seconds to explain yourself. Labelled DOZE THE JAILER, startForeground()." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The promise you make
&lt;/h2&gt;

&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%2Ff8cfkxw6qw106h50q7r8.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%2Ff8cfkxw6qw106h50q7r8.png" alt="The trap: Five seconds spent before the promise" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you start a foreground service, you are telling Android something specific: I am about to show a persistent notification and do work the user cares about, so please do not kill me the moment the screen sleeps. That is a real privilege. Foreground services get to keep running when almost nothing else can.&lt;/p&gt;

&lt;p&gt;Android grants it on one condition. You have about 5 seconds from starting the service to calling &lt;code&gt;startForeground()&lt;/code&gt; with a notification. Keep that promise and you get to stay. Miss it and the system kills the service and throws.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it only broke for some users
&lt;/h2&gt;

&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%2Ftoafct9ee5saoc4is2gn.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%2Ftoafct9ee5saoc4is2gn.png" alt="The rules: Working the night shift" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our service did 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;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStartCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;startId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Int&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;trip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tripStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restoreLastTrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// disk + db read&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;             &lt;span class="c1"&gt;// more io&lt;/span&gt;
    &lt;span class="nf"&gt;startForeground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buildNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&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;START_STICKY&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my phone, &lt;code&gt;restoreLastTrip()&lt;/code&gt; and &lt;code&gt;settings.load()&lt;/code&gt; finished in under 100 milliseconds. On a cheap device under memory pressure, with the disk busy and the CPU throttled, they sometimes took four or five seconds. By the time we reached &lt;code&gt;startForeground()&lt;/code&gt;, the window had closed. The crash only ever showed up on low-end devices, which is exactly the population least likely to file a good bug report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Post the notification first. Always. Then do the slow work.&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;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStartCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Intent&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;startId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;startForeground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;buildMinimalNotification&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;   &lt;span class="c1"&gt;// keep the promise instantly&lt;/span&gt;

    &lt;span class="n"&gt;serviceScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&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;trip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tripStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restoreLastTrip&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;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;updateNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;// enrich it later&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;START_STICKY&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The minimal notification can say something plain like "Starting trip tracking." The user sees it for a fraction of a second before the real one replaces it. That is a fine price for not crashing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Android 12 twist
&lt;/h2&gt;

&lt;p&gt;On Android 12 and up there is a second rule that catches people. In many cases you cannot start a foreground service from the background at all. If your trigger fires while the app is not visible, the launch itself throws.&lt;/p&gt;

&lt;p&gt;The answer is usually not to fight it. It is to use &lt;code&gt;WorkManager&lt;/code&gt; with an expedited request, which is the platform's sanctioned way to say "this is urgent, but let the system schedule 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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;work&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OneTimeWorkRequestBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;TripWorker&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setExpedited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OutOfQuotaPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RUN_AS_NON_EXPEDITED_WORK_REQUEST&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="nc"&gt;WorkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&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%2Fg2kvvefmjd062uh3khk6.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%2Fg2kvvefmjd062uh3khk6.png" alt="The payload: The platform is not out to get you" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The platform is not out to get you. Every one of these rules exists because some app, somewhere, tried to run forever and drained a stranger's battery. Post your notification first, respect the background-start limits, and the system leaves your work alone.&lt;/p&gt;

&lt;p&gt;Doze the Jailer runs the night shift. You get 5 seconds to explain why you belong. Explain fast.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 3 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **The Night Shift&lt;/em&gt;* · Featuring: Doze The Jailer*&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/the-night-shift.md" rel="noopener noreferrer"&gt;The Night Shift&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>foregroundservice</category>
      <category>workmanager</category>
      <category>lifecycle</category>
    </item>
    <item>
      <title>CancellationException is not an assassin</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Sun, 02 Aug 2026 07:30:50 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/cancellationexception-is-not-an-assassin-2220</link>
      <guid>https://dev.to/darkpandawarrior/cancellationexception-is-not-an-assassin-2220</guid>
      <description>&lt;p&gt;I cancelled a coroutine. It kept running for 8 more seconds. It just did not care.&lt;/p&gt;

&lt;p&gt;This was a search screen. You type, it fires a network request. Type again, it cancels the old request and fires a new one. Standard stuff. Except the old request kept running, and every so often it finished last and painted stale results right over the fresh ones. I had called cancel. I had watched it run in the debugger. The job was "cancelled." And still it went on.&lt;/p&gt;

&lt;p&gt;The culprit was one line I wrote myself.&lt;/p&gt;

&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%2Fn96fv3l1f4osytd8fhdn.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%2Fn96fv3l1f4osytd8fhdn.png" alt="The Messenger, drawn as a specimen plate: a hooded figure in an assassin's cloak, holding out a folded note stamped NOTICE. Labelled THE MESSENGER, CancellationException, exhibit 07." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation is a message, not a bullet
&lt;/h2&gt;

&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%2Fpjwib3xt4588qg4ab02k.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%2Fpjwib3xt4588qg4ab02k.png" alt="The trap: The line that ate the message" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the part people miss about Kotlin coroutines. Cancelling one does not reach in and kill it. It cannot. The coroutine has to cooperate. So the machinery throws a &lt;code&gt;CancellationException&lt;/code&gt; up through your suspend calls, and that exception is the message: we are done, unwind, release your resources, stop.&lt;/p&gt;

&lt;p&gt;Your job is to let that message travel. Most cancellation bugs are really one bug: you stopped the message from traveling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line that ate the message
&lt;/h2&gt;

&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%2Fqzxi9q4aumy2rlr8rl74.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%2Fqzxi9q4aumy2rlr8rl74.png" alt="Pick one: Four ways out" width="800" height="1000"&gt;&lt;/a&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// a suspend call&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"search failed"&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 looks careful. It looks like good defensive code. It is a trap.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CancellationException&lt;/code&gt; extends &lt;code&gt;Exception&lt;/code&gt;. So when cancellation fires mid-request, this &lt;code&gt;catch (e: Exception)&lt;/code&gt; grabs it, logs "search failed", and carries on as if a network error happened. The coroutine never unwinds. The framework thinks you handled something. The work continues.&lt;/p&gt;

&lt;p&gt;You caught the messenger, logged that he looked upset, and locked him in a cell. The message he was carrying never got delivered.&lt;/p&gt;

&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%2Fut7zrmha6p07vnlkcy9x.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%2Fut7zrmha6p07vnlkcy9x.png" alt="Diagram: a cancellation signal travelling up the suspend stack from api.search(query), through try, and dying at catch (e: Exception), which is marked " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to stop doing this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Catch what you actually expect.&lt;/strong&gt; Ninety percent of the time you do not want &lt;code&gt;Exception&lt;/code&gt;. You want the specific thing that can go wrong.&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showOfflineState&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;CancellationException&lt;/code&gt; is not an &lt;code&gt;IOException&lt;/code&gt;, so it sails right past and does its job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. If you must catch broadly, rethrow cancellation first.&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;                       &lt;span class="c1"&gt;// let the message through&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"search failed"&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;strong&gt;3. Know that &lt;code&gt;runCatching&lt;/code&gt; has the same trap.&lt;/strong&gt; It is a lovely little helper and it catches &lt;code&gt;CancellationException&lt;/code&gt; too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// also swallows cancellation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use it inside a coroutine, check for cancellation and rethrow, or do not use it there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. For cleanup that must run even while cancelling, use &lt;code&gt;NonCancellable&lt;/code&gt;.&lt;/strong&gt; A naked catch is the wrong tool for "always close 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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NonCancellable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flushAndClose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// runs even though the coroutine is cancelling&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;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%2F8v6wjfbzno4t0ol4lpx7.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%2F8v6wjfbzno4t0ol4lpx7.png" alt="Diagram: the same suspend stack, fixed. The cancellation signal now travels all the way up, past catch (e: CancellationException) which rethrows, past catch (e: IOException), and reaches " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the language made it this way
&lt;/h2&gt;

&lt;p&gt;Structured concurrency is the reason. When a parent scope cancels, every child needs to wind down cleanly and report back up. That only works if cancellation propagates. If any link in the chain swallows the exception, the parent never learns the child stopped, and your careful tree of coroutines turns into orphans that run in the dark.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;CancellationException&lt;/code&gt; is not an error to be defended against. It is the one exception in the system that is doing exactly what you asked. It is the app shutting things down cleanly, on time, on request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&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%2Fi2onz7mu16aw8tyrs5lx.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%2Fi2onz7mu16aw8tyrs5lx.png" alt="The payload: Do not shoot the messenger" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cancellation is a conversation, not a kill switch. If you swallow the message, the work does not stop. It just stops telling you it is still running.&lt;/p&gt;

&lt;p&gt;Do not shoot the messenger.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 2 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **The Coroutine Court&lt;/em&gt;* · Featuring: The Messenger*&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/the-coroutine-court.md" rel="noopener noreferrer"&gt;The Coroutine Court&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>coroutines</category>
      <category>cancellation</category>
      <category>structuredconcurrency</category>
    </item>
    <item>
      <title>Teaching a phone to disbelieve its own GPS</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:25:38 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/teaching-a-phone-to-disbelieve-its-own-gps-cip</link>
      <guid>https://dev.to/darkpandawarrior/teaching-a-phone-to-disbelieve-its-own-gps-cip</guid>
      <description>&lt;p&gt;The bug report came in on a Tuesday. One line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Your app says I hit 400 kmph. I was at a red light."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He wasn't lying. Neither was the app. The phone genuinely believed it, and that is the interesting part.&lt;/p&gt;

&lt;p&gt;I work on mileage tracking. If you have ever had an app quietly log your drives for expenses or taxes, that is the category. Trip accuracy is not a feature of the product. It is the product. When we started, ours sat around 50 percent, and almost every missing point traced back to the same thing: the phone lying to us with total confidence.&lt;/p&gt;

&lt;p&gt;Here is how we got it to 95, and the one idea underneath all of it.&lt;/p&gt;

&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%2Fhg0gbc50lvsh9x4z2hix.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%2Fhg0gbc50lvsh9x4z2hix.png" alt="The Concussed Witness, drawn as a specimen plate: a satellite with a bandaged, tilted dish and dizzy crossed-out eyes, confidently broadcasting a rejected reading of 400 km/h. Labelled THE CONCUSSED WITNESS, LocationManager, exhibit 01." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GPS is a witness with a concussion
&lt;/h2&gt;

&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%2F6puzzrcxj1hfbngw9274.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%2F6puzzrcxj1hfbngw9274.png" alt="The payload: Assume your inputs will lie" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We treat GPS like a source of truth. Out in the world, where people actually drive, it behaves more like a witness who took a hard knock to the head. Confident. Cooperative. Frequently wrong.&lt;/p&gt;

&lt;p&gt;Three places break it badly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tunnels.&lt;/strong&gt; The receiver loses its view of the sky. Instead of admitting that, it keeps reporting the last position it had, sometimes for 30 to 40 seconds. Then you come out the other end, it reacquires, and it snaps to your real location in a single jump. Take that jump, divide by the tiny time it took, and you get a parked car doing 400 kmph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Urban canyons.&lt;/strong&gt; Signals bounce off glass towers before reaching you (multipath, if you want the word). The phone averages the reflections and places you a street or two over, very sure of itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parking garages.&lt;/strong&gt; Nothing, then noise, then nothing again.&lt;/p&gt;

&lt;p&gt;None of these are edge cases. They are Tuesday. So the question stopped being "how do we get better GPS" and became "how do we stop believing bad GPS."&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: catch the liars
&lt;/h2&gt;

&lt;p&gt;The cheapest win first. For any travel mode there is a physically possible envelope. A person walking does not teleport 200 meters in a second. A car does not accelerate like a missile. So before a reading touches the trip, check whether it implies something physics would not allow.&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="nc"&gt;GpsFix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isImpossible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GpsFix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Mode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Boolean&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;meters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;haversine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;coerceAtLeast&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="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxPlausibleSpeed&lt;/span&gt;   &lt;span class="c1"&gt;// walking, cycling, driving each differ&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is impossible, drop it. This one guard killed the most embarrassing errors on its own, including the 400 kmph report. It is not clever. It just refuses to be gaslit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: dead reckoning, so a gap is not a hole
&lt;/h2&gt;

&lt;p&gt;Rejecting bad points leaves you with gaps, and a tunnel is a big one. If you freeze until GPS comes back, the trip gets a straight line cutting across three streets.&lt;/p&gt;

&lt;p&gt;So when GPS drops out, stop waiting for it and estimate. Take the last good heading and speed, read the accelerometer, and carry the position forward yourself. Old sailors did this by feel, no stars, no landmarks, just direction and time and a good guess. It drifts, so you cannot lean on it for long. But a tunnel is short, and "roughly right for 40 seconds" beats "confidently wrong" or "a hole in the map."&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;deadReckon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Imu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Position&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;speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;imu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forwardAccel&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;movedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;imu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;turnRate&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&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 trick is knowing when to trust it and when to hand control back to GPS the moment GPS is worth trusting again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: fuse, do not just pick
&lt;/h2&gt;

&lt;p&gt;Which brings us to the real fix. Most of the accuracy came from stopping the "believe whoever spoke last" habit and weighing every input by how much it deserves belief right now.&lt;/p&gt;

&lt;p&gt;A clean, consistent GPS fix pulls the estimate hard toward itself. A jittery one barely moves it. The dead-reckoned guess fills the space between. Nobody gets a veto. Everybody gets a vote, weighted by confidence.&lt;/p&gt;

&lt;p&gt;A Kalman filter is the textbook home for this, and if you reach for one, good. But the win is not the filter. The win is the mindset: hold several noisy opinions at once, and lean toward the one that looks trustworthy this second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unglamorous half
&lt;/h2&gt;

&lt;p&gt;None of this matters if the tracker is dead. On Android, the moment the screen sleeps, the system starts looking for background work to kill, and aggressive OEM skins kill harder. We ran the pipeline in a foreground service with a small floating bubble, partly so the user could see it working, mostly so the OS would leave it alone. A tracker that gets killed is 0 percent accurate no matter how good the math is. I spent nearly as long on staying alive as on the algorithm.&lt;/p&gt;

&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%2Fnaa01updei7tu9pcv5b8.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%2Fnaa01updei7tu9pcv5b8.png" alt="The three fixes as numbered cards: catch the liars, drop a reading that needs rocket acceleration; dead reckoning, estimate from the IMU and last good heading; fuse do not pick, weight each signal by how much you trust it now." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually took us from 50 to 95
&lt;/h2&gt;

&lt;p&gt;Not one silver bullet. Spike detection removed the garbage. Dead reckoning covered the gaps. Fusion made the whole thing coherent. Staying alive made it real. Each one bought a chunk, and they compounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steal this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat every external input as a witness who might be concussed, not a source of truth.&lt;/li&gt;
&lt;li&gt;Give physics a veto. The cheapest correctness check is "could this even happen."&lt;/li&gt;
&lt;li&gt;When a signal drops, degrade to a rough estimate instead of freezing or guessing wildly.&lt;/li&gt;
&lt;li&gt;Weight inputs by live confidence, not recency.&lt;/li&gt;
&lt;li&gt;Keep the thing alive first. Perfect logic in a killed process is worth nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one line I kept after all of it: good systems are not the ones with perfect inputs, because nobody gets perfect inputs. Good systems assume their inputs will lie, and plan for the day they do.&lt;/p&gt;

&lt;p&gt;Trust, but verify. Especially your own sensors.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 1 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **Sensors Who Lie&lt;/em&gt;* · Featuring: The Concussed Witness*&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/sensors-who-lie.md" rel="noopener noreferrer"&gt;Sensors Who Lie&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>location</category>
      <category>sensorfusion</category>
      <category>deadreckoning</category>
    </item>
  </channel>
</rss>
