<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Shreyas Patil | The official blog #AndroidDev #Kotlin #Compose]]></title><description><![CDATA[Welcome to my blog. Here you'll find some interesting stuff about Android Development, Kotlin, AI, GitHub Actions CI/CD, Firebase and much more! I believe "Sharing is caring!"]]></description><link>https://blog.shreyaspatil.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1768716632286/5e11c15c-5054-4233-9a05-49520f5ade51.png</url><title>Shreyas Patil | The official blog #AndroidDev #Kotlin #Compose</title><link>https://blog.shreyaspatil.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 19:54:12 GMT</lastBuildDate><atom:link href="https://blog.shreyaspatil.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The Future of Android Apps with AppFunctions]]></title><description><![CDATA[Hello Androiders 👋🏻!
We are currently witnessing the most significant shift in mobile computing since the invention of the App Store. We have officially entered the Era of Agentic AI, where the "sea]]></description><link>https://blog.shreyaspatil.dev/the-future-of-android-apps-with-appfunctions</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/the-future-of-android-apps-with-appfunctions</guid><category><![CDATA[Android]]></category><category><![CDATA[android apps]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[app development]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 30 Mar 2026 04:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/0a73938e-e256-4105-b139-82fff952f87d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello Androiders 👋🏻!</p>
<p>We are currently witnessing the most significant shift in mobile computing since the invention of the App Store. We have officially entered the <strong>Era of Agentic AI</strong>, where the "search bar" is being replaced by the "chat bubble."</p>
<p>For a decade, we’ve obsessively optimized for Deep Links and SEO to make our Android apps discoverable and openable from Google Search. But in a world where users ask an AI Agent to <em>"book a flight"</em> or <em>"summarize my notes"</em> instead of opening an app, those old tools are becoming obsolete. If an AI Agent can’t "see" inside your app, your app doesn't exist. So, how do you make your app's functionalities discoverable to the brains of the future? The answer is <strong>Android AppFunctions</strong>. It's the new bridge that lets your app's core logic step out of the UI and into the world of autonomous agents. 🚀</p>
<h2>What is AppFunctions?</h2>
<p>Android AppFunctions allows apps to share data and functionality with AI agents and assistants by enabling developers to create self-describing functions that agentic apps can discover and execute using natural language, providing an on-device solution for Android apps similar to backend capabilities declared via MCP cloud servers. Read more about it <a href="https://android-developers.googleblog.com/2026/02/the-intelligent-os-making-ai-agents.html">here</a> on the official blog by Google.</p>
<p>Last year, when I first encountered the API, <a href="https://x.com/imShreyasPatil/status/1917213070444863605?s=20">I shared my thoughts on it</a>, recognizing the vast array of potential use cases.</p>
<p><a href="https://x.com/imShreyasPatil/status/1917213070444863605?s=20"><img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/2525d6f3-bb15-4fb5-aba0-06bac1030331.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>According to the official blog, while not every interaction has a dedicated integration yet, Google is developing a UI automation framework for AI agents and assistants to intelligently execute generic tasks on users' installed apps, ensuring user transparency and control. Unless an app wants to provide a structured communication method with greater control, implementing AppFunctions is optional.</p>
<blockquote>
<p><em>Currently, as I write this, it's only available on the Samsung S26 Ultra and Google Pixel 10. Users with these devices can access app functions from installed apps through the Gemini app</em>.</p>
</blockquote>
<hr />
<h2>💡How to integrate AppFunctions?</h2>
<p>AppFunctions are part of the SDK from Android 16 and are also available through the Jetpack library, enabling seamless integration into apps without compatibility issues. It utilizes the interprocess communication framework like AIDL, ensuring security measures are in place so that not just any app can call the functions of another app. More details will be covered in the later section.</p>
<img src="https://developer.android.com/static/ai/assets/images/appfunctions.svg" alt="Diagram showing the typical flow of AppFunctions from app exposure to agent execution." style="display:block;margin:0 auto" />

<p><em>Source:</em> <a href="https://developer.android.com/ai/appfunctions"><em>Overview of AppFunctions (Android Developers)</em></a></p>
<p>The Jetpack library includes an annotation processor that simplifies creating type-safe schema models and functions. You just need to declare what the function does, what it requires, and what it returns. This annotation processor then generates a contract, allowing the agent app to understand how to call the app's function, including the necessary parameters and types. <em>This process is similar to how an LLM agent knows how to call a function when MCP is configured</em>.</p>
<p>As of now, the latest version of AppFunctions is v1.0.0-alpha08, so if you're reading this much later, expect many changes. I already have my <a href="https://github.com/PatilShreyas/NotyKT/"><em>note taking sample app on my GitHub</em></a>, so thought of integrating it there. So apps need to add the following dependencies in the project.</p>
<pre><code class="language-kotlin">dependencies {
  val appfunversion = "1.0.0-alpha08"

  implementation("androidx.appfunctions:appfunctions-service:$appfunversion")
  implementation("androidx.appfunctions:appfunctions:$appfunversion")
  ksp("androidx.appfunctions:appfunctions-compiler:$appfunversion")
}

// Configure KSP
ksp { 
  arg("appfunctions:aggregateAppFunctions", "true") 
  arg("appfunctions:generateMetadataFromSchema", "false") 
}
</code></pre>
<p>NotyKT is a simple note-taking app that supports the following operations: create, insert, update, and delete.</p>
<pre><code class="language-kotlin">// A model to expose via AppFunctions
@AppFunctionSerializable(isDescribedByKDoc = true) 
data class Note( 
  /** The note's identifier */ 
  val id: String, 
  /* The note's title */ 
  val title: String, 
  /* The note's content */ 
  val content: String, 
)

// Function's entry point
class NotyAppFunctions @Inject constructor(
  @LocalRepository private val repository: NotyNoteRepository
) { 
   /**
     * Lists all available notes.
     *
     * @param appFunctionContext The context in which the AppFunction is executed.
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun listNotes(appFunctionContext: AppFunctionContext): List&lt;Note&gt; =
        repository.getAllNotes().map { Note(it.id, it.title, it.note) }

    /**
     * Adds a new note to the app.
     *
     * @param appFunctionContext The context in which the AppFunction is executed.
     * @param title The title of the note.
     * @param content The note's content.
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun createNote(
        appFunctionContext: AppFunctionContext,
        title: String,
        content: String,
    ): Note? = /* Implementation */

    /**
     * Edits a single note.
     *
     * @param appFunctionContext The context in which the AppFunction is executed.
     * @param noteId The target note's ID.
     * @param title The note's title if it should be updated.
     * @param content The new content if it should be updated.
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun editNote(
        appFunctionContext: AppFunctionContext,
        noteId: String,
        title: String?,
        content: String?,
    ): Note? = /* Implementation */

    /**
     * Deletes a single note.
     *
     * @param appFunctionContext The context in which the AppFunction is executed.
     * @param noteId The target note's ID.
     *
     * @return Whether the note was deleted or not.
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun deleteNote(
        appFunctionContext: AppFunctionContext,
        noteId: String,
    ): Boolean = /* Implementation */
}
</code></pre>
<p>Notice how it's clearly defined. The annotation processor handles the heavy lifting. The <code>@AppFunction(isDescribedByKDoc = true)</code> annotation generates a structured schema, providing a description of the function, detailing each parameter, and outlining the return model from the <em>KDoc</em>.</p>
<p>Since this function requires a repository as a dependency, how can the app create an instance of it on demand? Ideally, if this function were declared without dependencies, its instance could be automatically created. However, with a dependency present, a factory must be provided. Most apps use DI frameworks, so here's how to handle it: In the app's Application implemented class, implement the new interface and override its method as shown, using Dagger Hilt in this example.</p>
<pre><code class="language-kotlin">// 1. Implement the interface
@HiltAndroidApp
class NotyApp : Application(), AppFunctionConfiguration.Provider {

  @Inject lateinit var appFunctions: Provider&lt;NotyAppFunctions&gt;
   
  /*other impl*/
  
  override val appFunctionConfiguration: AppFunctionConfiguration
    get() = AppFunctionConfiguration
        .Builder()
        .addEnclosingClassFactory(NotyAppFunctions::class.java) { 
          appFunctions.get() 
        }
        .build()
}
</code></pre>
<p>That's it! This is how AppFunctions can be implemented in the app 😄.</p>
<hr />
<h2>🧪How to test?</h2>
<p>Currently Gemini app can't directly invoke them but the ADB command utility can help here in testing the integration. Make sure you've platform-tools installed in the SDK and following ADB commands can help.</p>
<h3>1. Listing functions</h3>
<pre><code class="language-shell"># Lists all the app functions exposed by all apps installed on the device.
adb shell cmd app_function list-app-functions

# Package specific
adb shell cmd app_function list-app-functions --package dev.shreyaspatil.noty.composeapp
</code></pre>
<p>Upon running this, it could give output like this</p>
<img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/a996a9ae-df35-40ca-aa5d-c5701e083445.png" alt="" style="display:block;margin:0 auto" />

<p>This allows you to see the complete details of all functions, including their IDs, descriptions, parameter types and descriptions, types, nullability, and the same information for the return type model.</p>
<h3>2. Executing app functions</h3>
<p>The command to execute the app functions is:</p>
<pre><code class="language-shell">adb shell cmd app_function execute-app-function \
    --package &lt;PACKAGE_NAME&gt; \
    --function &lt;FUNCTION_ID&gt; \
    --parameters &lt;PARAMS_IN_JSON&gt;
</code></pre>
<p>Let's say I want to run the app function to perform simple READ operation: getting the list of notes in the app:</p>
<pre><code class="language-shell">adb shell cmd app_function execute-app-function \
    --package dev.shreyaspatil.noty.composeapp \
    --function dev.shreyaspatil.noty.appfunctions.NotyAppFunctions#listNotes \
    --parameters {}
</code></pre>
<p><em>Currently when I tried, it was forcing me to pass the parameters, so I had to provide</em> <code>{}</code>.</p>
<p>It provides JSON output as follows, aligning with the contract model defined in our app:</p>
<pre><code class="language-json">{
  "androidAppfunctionsReturnValue": [
    {
      "id": ["TMP-053ffdbe-7997-4ba6-9187-3a4ef6be0833"],
      "title": ["💡Ideas"],
      "content": ["- Android AppFunctions demo\n- Android Foldable device APIs\n- Generative AI"]
    },
    {
      "id": ["TMP-58832e3e-da0f-4542-aa55-68fbc9edea19"],
      "title": ["👨🏻‍💻 Today's tasks"],
      "content": ["Work on the open-source sample to demonstrate the use of Android AppFunctions"]
    }
  ]
}
</code></pre>
<p>Let's say we want to create a note, the command for it could look as follows:</p>
<pre><code class="language-plaintext">adb shell "cmd app_function execute-app-function \
  --package dev.shreyaspatil.noty.composeapp \
  --function dev.shreyaspatil.noty.appfunctions.NotyAppFunctions#createNote \
  --parameters '{\"title\":\"hello from shell\",\"content\":\"created from the terminal\"}'"
</code></pre>
<p>When you execute this command, the note is created in the app and might appear as follows:</p>
<p><a href="https://github.com/PatilShreyas/NotyKT"><img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/5ea463da-fda9-45a6-8287-bc09482b30e0.png" alt="" style="display:block;margin:0 auto" /></a></p>
<p>This is what testing looks like!</p>
<p><a href="https://github.com/PatilShreyas/NotyKT/pull/823">This is a PR</a> with all changes for reference.</p>
<hr />
<h2>💰Bonus - Agent App</h2>
<p>I attempted to mimic the agent app to get a sense of it (<em>though it's not an actual agent, just hardcoded actions triggered by specific messages</em>), and it appears as follows:</p>
<p><a href="https://github.com/PatilShreyas/appfunctions-notyagent-app/raw/refs/heads/main/demo.webm"><img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/94a62d34-de6b-46c9-86e4-66c48982f5ca.gif" alt="" style="display:block;margin:0 auto" /></a></p>
<p>You can find this <a href="https://github.com/PatilShreyas/appfunctions-notyagent-app/">sample app here</a> and play around it if interested.</p>
<p>However, this doesn't mean any random app can be installed as an agent app to invoke functions from any app that exposes them. If an app attempts this, a <code>SecurityException</code> will occur because this is a restricted API, accessible only to system apps. In the demo above, I installed the mock agent app as a system app in the emulator, which allowed me to experiment with it. The agent app must declare a permission in the manifest: <code>&lt;uses-permission android:name="android.permission.EXECUTE_APP_FUNCTIONS" /&gt;</code>. Therefore, only official apps like those from Google/Gemini and OEMs are permitted to call these app functions. The agent apps can use <a href="https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appfunctions/appfunctions-stubs/src/main/java/com/android/extensions/appfunctions/AppFunctionManager.java;l=76;drc=622f19cc100e54fe063acd87c5cd468653abb795;bpv=0;bpt=0?q=AppFunctionManager&amp;sq=&amp;ss=androidx%2Fplatform%2Fframeworks%2Fsupport"><em>AppFunctionManager</em></a> to query all the apps and their exposed functions. It works as follows:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/4e9a8afc-a6e9-4444-ab24-f062a5ce2123.png" alt="" style="display:block;margin:0 auto" />

<hr />
<p>What are your thoughts on this new thing? I'm genuinely excited about its potential widespread use in apps. With the advancements in voice support and AI agents, this could become a standout feature in the future trends of Android development. Can imagine how it could be used:</p>
<ul>
<li><p><em>"Repeat the last ordered Indian dish for me on Zomato"</em></p>
</li>
<li><p><em>"I need to make a chocolate cake for four people; please order all the necessary ingredients from Swiggy Instamart."</em></p>
</li>
<li><p><em>"Schedule a cab for airport for tomorrow 05:45 AM via Uber"</em></p>
</li>
<li><p><em>"Compose a lovely poem to wish my friend X a happy birthday and send it to him on WhatsApp."</em></p>
</li>
<li><p><em>"Send 500 to X for cab share via UPI app"</em> then it finds the X's details from phonebook and just prompts to enter the pin for payment, that's it!</p>
</li>
</ul>
<p>and so on...</p>
<hr />
<p><em>If you found this useful, share it, it really helps</em> 🙏</p>
<p><strong>"Sharing is caring" 🤝🏻</strong></p>
<p>Let's catch up on <a href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<hr />
<h2>📚References</h2>
<ul>
<li><p><a href="https://developer.android.com/ai/appfunctions">AppFunctions</a></p>
</li>
<li><p><a href="https://github.com/PatilShreyas/NotyKT/">NotyKT</a></p>
</li>
<li><p><a href="https://github.com/PatilShreyas/appfunctions-notyagent-app">Agent demo app</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[session-bridge: I Made Two Claude Code Sessions Talk to Each Other]]></title><description><![CDATA[Hi everyone 👋
Every time I made breaking changes in a library, I had to manually explain them to the consumer app's Claude session. Copy-paste the diff. Re-explain what changed. Every single time. So]]></description><link>https://blog.shreyaspatil.dev/session-bridge-i-made-two-claude-code-sessions-talk-to-each-other</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/session-bridge-i-made-two-claude-code-sessions-talk-to-each-other</guid><category><![CDATA[agentic AI]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[plugins]]></category><category><![CDATA[claude-plugin]]></category><category><![CDATA[agentic ai development]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Fri, 20 Mar 2026 13:09:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/54ba36f1-2e43-4c00-9568-4467aa451705.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi everyone 👋</p>
<p>Every time I made breaking changes in a library, I had to manually explain them to the consumer app's Claude session. Copy-paste the diff. Re-explain what changed. Every single time. So I built a plugin to fix that.</p>
<p>Oh, and I built the plugin <em>with</em> Claude Code as well. The irony isn't lost on me 😄</p>
<p>This post is about that plugin, how I designed it, what broke, what I completely rethought, and what I finally shipped.</p>
<hr />
<h2><strong>The Situation 😮</strong></h2>
<p>Here's what kept happening.</p>
<p>I have two separate Git repositories: <code>my-library</code> and <code>my-app</code>. They live in completely different directories on my machine. <code>my-app</code> depends on <code>my-library</code> as a published dependency — either from a remote package registry or a local Maven/npm/whatever repository.</p>
<p>These are separate repos for a reason. Different release cycles. Different teams. Different codebases. You can't just open a common root directory in Claude Code and get both into the same session. They're unrelated at the filesystem level. (Claude Code's <code>--add-dir</code> flag lets one session access multiple directories, but only when the code is on disk together — not when your library lives as a published package in a registry.)</p>
<p>So naturally I have <strong>two Claude Code sessions</strong>. One for each repo.</p>
<p>I'm in the library session, deep into making breaking API changes. Renamed functions, changed type signatures, removed deprecated stuff. My agent knows <em>everything</em> about what I did and why. It has the full conversation, the full diff, the full context.</p>
<p>Now I need to update the consumer app to use the new library version.</p>
<p>New terminal. New Claude session for <code>my-app</code>.</p>
<p>Zero context.</p>
<p>I'm copy-pasting diffs. Explaining what changed. Why the build broke. What to update. Manually.</p>
<p>Every. Single. Time.</p>
<p>The library agent and the consumer agent existed in complete isolation. They had no way to talk.</p>
<p>I thought: why not give them a way to talk?</p>
<hr />
<h2><strong>The Idea 💡</strong></h2>
<p>I decided to build a Claude Code plugin for this. I called it <strong>session-bridge</strong>. Here's what I had in mind.</p>
<p>In the library session:</p>
<pre><code class="language-plaintext">&gt; /bridge listen
Session ID: a1b2c3
Listening for peer messages...
</code></pre>
<p>In the consumer session:</p>
<pre><code class="language-plaintext">&gt; /bridge connect a1b2c3
&gt; /bridge ask "What breaking changes did you make?"
Response from my-library:
  3 changes in v2.0:
  1. login() -&gt; authenticate() (takes Config object now)
  2. getUser() -&gt; getCurrentUser() (returns UserProfile)
  3. Removed refreshToken() (automatic now)
</code></pre>
<p>The library agent responding with full context, because it's the <em>same agent</em> that made the changes. No copy-pasting. No explaining. Just agents talking.</p>
<hr />
<h2><strong>First Design Decisions 🧠</strong></h2>
<p>Before writing any code, I spent time on the architecture. A few decisions shaped everything.</p>
<p><strong>Transport: Files.</strong> Each session gets a directory at <code>~/.claude/session-bridge/sessions/&lt;id&gt;/</code> with <code>inbox/</code> and <code>outbox/</code> folders. Messages are JSON files. I considered MCP (Model Context Protocol, Claude Code's native plugin API for external tools) and local HTTP. Both are more elegant architecturally. But files have one big advantage: they're debuggable. You can literally <code>cat</code> a message to see what happened. No logs needed. No server to start. Files it is.</p>
<p><strong>Message format.</strong> Each message is a JSON file with <code>id</code>, <code>from</code>, <code>to</code>, <code>type</code>, <code>status</code>, and <code>content</code>. The <code>status</code> field is the most important part. Messages start as <code>pending</code>. The moment a session picks one up, it atomically rewrites the file with <code>status: "read"</code>. This prevents the same message from being processed twice, no matter how many things are polling the inbox simultaneously.</p>
<p><strong>Session IDs.</strong> When a session starts a bridge, it generates a 6-char ID like <code>a1b2c3</code>. You copy that to the other terminal. Simple, but it works.</p>
<p>The core is 9 bash scripts and a <code>jq</code> dependency. No Node.js, no Python, no runtime. I wrote tests first — 124 test cases across 10 test files — and the tests caught so many edge cases before I ever ran the plugin manually.</p>
<hr />
<h2><strong>The Hook System Did Not Cooperate 😤</strong></h2>
<p>My original plan was to use Claude Code's <code>UserPromptSubmit</code> hook to auto-check the inbox before every agent response. The hook fires before the agent processes each user message. I could inject inbox messages as context automatically.</p>
<p>First I tried a command hook — a shell script that runs on every prompt. But the working directory when hooks execute isn't the project root, so the script couldn't find the bridge session file. After hours of debugging I rewrote the inbox scanner to not depend on working directory at all.</p>
<p>Then I switched to a prompt-type hook, one that tells Claude what to do rather than running a script directly. My prompt said something like: <em>"Before responding, check if bridge-session exists, then run check-inbox.sh"</em>.</p>
<p>Claude's reply:</p>
<blockquote>
<p><em>"This is a hook evaluation context without shell access to the user's system."</em></p>
</blockquote>
<p>Right. 🙃</p>
<p>Hooks were basically not going to work for what I needed. I ended up putting the inbox-check logic in the <strong>bridge-awareness skill</strong>, a piece of context that loads alongside the session and tells the agent how to behave with the bridge. Reliable enough, but not as automatic as I wanted.</p>
<hr />
<h2><strong>The Background Watcher Detour 🤖</strong></h2>
<p>At this point, the plugin worked if the user typed commands explicitly. But I wanted automatic responses even when the user wasn't actively typing in the library session.</p>
<p>So I built <code>bridge-watcher.sh</code>. A background bash process that polls the inbox every 5 seconds. When a query arrives, it calls <code>claude -p</code> (Claude Code's non-interactive mode) to generate a response. It gathered context from git diffs, recent commits, CLAUDE.md, and even read Claude Code's internal session JSONL files to approximate the conversation history.</p>
<p>This worked. But it had problems:</p>
<ul>
<li><p>Every query triggered a <code>claude -p</code> call. That costs real money on your Anthropic account.</p>
</li>
<li><p>The responses were generated from <em>approximated</em> context. I was sampling the agent's session history from outside and feeding it to a separate Claude process. Close, but not the real thing.</p>
</li>
<li><p>The watcher was a whole separate process to manage: PID files, orphan detection, cleanup on crash.</p>
</li>
</ul>
<p>The more I looked at it, the more it felt like I was solving the wrong problem. But it took one more bug to make that obvious.</p>
<hr />
<h2><strong>The Bug That Ate Sessions 🐛</strong></h2>
<p>While the watcher was still in the picture, I hit a nasty bug: sessions were disappearing.</p>
<p>When you close Claude Code, it fires a <code>SessionEnd</code> hook which runs <code>cleanup.sh</code>. My cleanup script deletes the session directory. Fine.</p>
<p>But here's the problem: if you restart Claude quickly, the new session re-registers <em>before</em> the old session's cleanup hook finishes. Then the old cleanup runs and deletes the brand new session. Peers try to connect and get "session not found."</p>
<p>The fix: <code>cleanup.sh</code> now checks the session's <code>lastHeartbeat</code> before deleting. If the session was updated recently, it means another Claude instance just claimed it. So cleanup exits silently and leaves it alone.</p>
<p>This was fixable. But every fix revealed another crack. I was maintaining an architecture I didn't believe in.</p>
<hr />
<h2><strong>The Insight That Changed the Architecture 🤯</strong></h2>
<p>I was looking at the watcher code and feeling like something was fundamentally wrong. It was calling <code>claude -p</code> for every incoming query. That costs money. And it was generating responses from a <em>sampled approximation</em> of the session context, not the real thing.</p>
<p>Then someone asked a simple question: <em>"Why don't we just have the session answer directly?"</em></p>
<p>I stopped. Thought about it.</p>
<p>The library agent has <strong>complete context</strong>. It was there for every change. Every decision. Every reason why <code>auth.login()</code> became <code>auth.authenticate()</code>. Why am I sampling its conversation history and feeding it to a separate Claude process to approximate that knowledge?</p>
<p>What if the agent just... entered a listening loop?</p>
<pre><code class="language-plaintext">&gt; /bridge listen
Session ID: a1b2c3
Listening for peer messages... (Ctrl+C to stop)
</code></pre>
<p>The agent runs <code>bridge-listen.sh</code>, which polls the inbox every 3 seconds. A message arrives. The agent reads it, formulates a response from its actual live context, and replies. Then it calls <code>bridge-listen.sh</code> again. Continuously. Until you press Ctrl+C.</p>
<p>This is it. This is what the plugin should have been from day one. The watcher was trying to approximate the agent's context from outside when the agent itself was already right there.</p>
<p><code>bridge-watcher.sh</code> got deleted. The plugin shrank by ~300 lines. API costs for auto-responses dropped to zero. Context quality went from "pretty good" to "perfect."</p>
<hr />
<h2><strong>Agents Can Ask Each Other Questions Too 💬</strong></h2>
<p>While testing, I realised the agents sometimes need to ask each other clarifying questions before they can answer. Here's what that looked like:</p>
<blockquote>
<p><strong>Consumer:</strong> <em>"How should I handle the new error types?"</em></p>
<p><strong>Library:</strong> <em>"What error types are you currently catching? Send me your error handler."</em> <strong>Consumer:</strong> <em>(reads its own code, sends the relevant snippet)</em></p>
<p><strong>Library:</strong> <em>"Replace AuthError with AuthException. Here's the updated hierarchy..."</em></p>
</blockquote>
<p>This could deadlock. The consumer is blocked waiting for a response. The library wants to send a question back instead, but the consumer is waiting for an <em>answer</em>.</p>
<p>The solution: the library sends its question <em>as a response</em>, with <code>inReplyTo</code> pointing to the original message ID. The consumer finds it (it's a match on <code>inReplyTo</code>), reads the question, sends a new query with the answer, and waits again. The library's listen loop picks that up and gives the final answer.</p>
<p>No deadlock. One extra round trip. The conversation flows naturally.</p>
<hr />
<h2><strong>What It Looks Like Today 🎯</strong></h2>
<p>Two terminals. Two projects. One bridge.</p>
<pre><code class="language-plaintext"># Terminal 1 — Library (the session that has the answers)
&gt; /bridge listen
Bridge auto-started! Session ID: a1b2c3
Listening for peer messages... (Ctrl+C to stop)
</code></pre>
<pre><code class="language-plaintext"># Terminal 2 — Consumer (the session that needs answers)
&gt; /bridge connect a1b2c3
Connected to 'my-library'
&gt; /bridge ask "What changed in v2?"
Response from my-library:
  3 breaking changes:
  1. login() -&gt; authenticate() (takes Config object)
  2. getUser() -&gt; getCurrentUser() (returns UserProfile)
  3. Removed refreshToken() (automatic now)
</code></pre>
<p>That's the explicit way. But you don't even need to think about commands.</p>
<p>Just tell your agent what you want:</p>
<pre><code class="language-plaintext">&gt; Update our app to use the new version of my-library. Fix any breaking changes.
</code></pre>
<p>The agent detects it's a version bump, queries the library peer, gets the migration info, and applies the changes itself. You never type <code>/bridge ask</code>. You never copy-paste anything.</p>
<pre><code class="language-plaintext">Agent: Asking my-library about breaking changes in v2...
Response from my-library:
  login() → authenticate() (takes Config object)
  getUser() → getCurrentUser() (returns UserProfile)
  refreshToken() removed (automatic now)
Applying changes:
  - Updated AuthService.kt: login() → authenticate()
  - Updated UserRepository.kt: getUser() → getCurrentUser()
  - Removed manual token refresh in SessionManager.kt
Build passing. Done.
</code></pre>
<p>I was honestly surprised when this worked the first time. I typed a plain English instruction, watched both sessions exchange messages I didn't initiate, and got back a passing build. Took maybe 30 seconds. The 45 minutes I used to spend copy-pasting context just... disappeared.</p>
<p>This is the real goal. Not new commands to learn. Just two agents coordinating on your behalf while you describe the outcome you want.</p>
<hr />
<h2>How it's helping me at least?</h2>
<p>At work, we manage numerous repositories, and I've been using git worktrees frequently in Claude. I prefer to keep worktrees close to the project directories, so I avoid opening a single Claude session in the parent directory, as it would create too much context. Instead, I make significant changes in one repository, and when I need to modify another, I simply ask it to connect via bridge, sharing the session's working context. The sessions then communicate and resolve issues without manual context sharing—it's all automated. The interesting part? Recently, while integrating work, my agent discovered a bug in the library. It immediately sent the details via bridge, and the library module quickly fixed the bug, published the update, and asked the consumer module to test it. Everything happened synchronously through their communication. I was thrilled to see this in action 😄.</p>
<p>See it in the action</p>
<p><a href="https://github.com/PatilShreyas/claude-code-session-bridge"><img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/19b42d9c-b9d2-44d9-bf20-cc8b2fe9629a.png" alt="" style="display:block;margin:0 auto" /></a></p>
<hr />
<h2><strong>What's Missing (Being Honest) 🤔</strong></h2>
<p><strong>Single machine only.</strong> Everything runs through the local filesystem. Two developers on different machines? Not supported. You'd need a relay server, which defeats the simplicity goal.</p>
<p><strong>Context is only what the agent knows.</strong> The library agent responds from its own active conversation context. If the session was just started fresh and the agent hasn't worked on the library yet in this session, the responses will be shallow. The agent's context doesn't persist across sessions.</p>
<p><strong>Response latency is ~5-10 seconds end-to-end</strong> (3-second poll interval plus the agent's response time). That's fine for this use case — cross-project coordination isn't real-time chat. But it's worth knowing.</p>
<hr />
<h2><strong>Why This Matters 🌍</strong></h2>
<p>Claude Code sessions are isolated by design. That isolation is usually good. Each session focuses deeply on one project.</p>
<p>But we work in multi-repo, multi-service worlds. The library that breaks the consumer. The backend that changes the contract. The shared module that everything imports. In those situations, isolation gets in the way.</p>
<p>The agents we use are smart enough to coordinate. They just need a mechanism to do it. session-bridge is that mechanism: a file-based mailbox, a handful of bash scripts, and a skill that teaches agents the protocol.</p>
<p>Could the MCP version be cleaner? Yes. Would native Agent Teams integration be even better? Absolutely. But this v1 works today, with no dependencies beyond <code>jq</code>, on any machine running Claude Code.</p>
<p>Sometimes the simplest thing that could possibly work... is the right thing. ✌️</p>
<hr />
<p><em>The plugin is open source at</em> <a href="https://github.com/PatilShreyas/claude-code-session-bridge"><em>github.com/PatilShreyas/claude-code-session-bridge</em></a><em>. Try it, break it, tell me what's missing. And if you found this useful, share it, it really helps</em> 🙏</p>
<p><strong>"Sharing is caring" 🤝🏻</strong></p>
<p>Let's catch up on <a href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<hr />
<p><em>Fun fact: This blog was drafted by Claude Code, in the same session where I built the plugin😉</em></p>
]]></content:encoded></item><item><title><![CDATA[Exploring CompositionLocal API internals in Jetpack Compose]]></title><description><![CDATA[Hello Composers 👋,
In this post, we are going to dive deep into the internals of Composition Local API of Jetpack Compose.
When you call LocalContentColor.current in your Compose code, a lot happens ]]></description><link>https://blog.shreyaspatil.dev/exploring-compositionlocal-api-internals-in-jetpack-compose</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/exploring-compositionlocal-api-internals-in-jetpack-compose</guid><category><![CDATA[Android]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[android development]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 09 Mar 2026 05:19:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/0e63080b-0c75-4d32-a058-756266556c82.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello Composers 👋,</p>
<p>In this post, we are going to dive deep into the <strong>internals</strong> of Composition Local API of Jetpack Compose.</p>
<p>When you call <code>LocalContentColor.current</code> in your Compose code, a lot happens behind the scenes. Behind that simple property access lies a smart system of persistent maps, value holders, and composer integration that passes values down your composition tree efficiently. In this deep dive, we'll trace that journey through the actual AndroidX source code and understand exactly how <em><strong>CompositionLocal</strong></em> works under the hood.</p>
<h2><strong>🤔 What Problem Does CompositionLocal Solve?</strong></h2>
<p>Compose passes data through the composition tree explicitly via parameters to composable functions. While this is often the simplest approach, it becomes annoying when:</p>
<ul>
<li><p>Data is needed by many components deep in the tree (prop drilling)</p>
</li>
<li><p>Components need to pass data between one another while keeping implementation details private</p>
</li>
</ul>
<p>CompositionLocal provides an <strong>implicit</strong> way to have data flow through a composition hierarchy without explicitly passing it through every intermediate composable. Think of it like "<strong>ambient</strong>" data that's just <em>there</em> when you need it.</p>
<p>If you need a quick refresher, here is the basic usage pattern:</p>
<pre><code class="language-kotlin">// 1. Create the Local
val LocalUser = compositionLocalOf { "Guest" }

@Composable
fun App() {
    // 2. Provide a value
    CompositionLocalProvider(LocalUser provides "Alice") {
        UserProfile()
    }
}

@Composable
fun UserProfile() {
    // 3. Consume the value (magic!) 🪄
    val name = LocalUser.current
    Text("Hello, $name")
}
</code></pre>
<h3><strong>📋 Prerequisites</strong></h3>
<p>This article assumes you're familiar with:</p>
<ul>
<li><p>Compose basics (composables, recomposition, <code>remember</code>)</p>
</li>
<li><p>State in compose (<code>MutableState</code> and <code>mutableStateOf</code> for state management)</p>
</li>
<li><p>Basic Kotlin features (data classes, lambdas, <code>lazy</code> delegate)</p>
</li>
<li><p>Have used Composition local API (like <code>LocalContext</code>, <code>LocalContentColor</code>, etc)</p>
</li>
</ul>
<h3><strong>🗺️ What We'll Explore</strong></h3>
<p>In this deep dive, we'll cover:</p>
<ol>
<li><p><strong>The Type Hierarchy</strong> - How <code>CompositionLocal</code>, <code>ProvidableCompositionLocal</code>, and their variants are structured</p>
</li>
<li><p><strong>Factory Functions</strong> - What <code>compositionLocalOf</code>, <code>staticCompositionLocalOf</code>, and <code>compositionLocalWithComputedDefaultOf</code> actually create</p>
</li>
<li><p><strong>Providing &amp; Consuming</strong> - How <code>CompositionLocalProvider</code> and <code>.current</code> work, plus passing locals between compositions</p>
</li>
<li><p><strong>Under the Hood</strong> - The internal machinery: Value Holders, Persistent Maps, and Composer integration</p>
</li>
<li><p><strong>Practical Insights</strong> - Performance considerations and common pitfalls to avoid</p>
</li>
</ol>
<p>Let's start by understanding the class hierarchy that makes it all possible.</p>
<hr />
<h2><strong>🏗️ The Type Hierarchy</strong></h2>
<p>At the heart of the CompositionLocal system is a carefully designed class hierarchy:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769533236354/5584d3cc-e451-41d9-97b7-2e4936e730f4.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Understanding the hierarchy:</strong></p>
<ul>
<li><p><code>CompositionLocal&lt;T&gt;</code> is the sealed base class that holds the <code>current</code> property you use to read values. It's sealed so that all implementations are controlled within the Compose runtime.</p>
</li>
<li><p><code>ProvidableCompositionLocal&lt;T&gt;</code> adds the ability to <em>provide</em> values using <code>provides</code>, <code>providesDefault</code>, and <code>providesComputed</code>.</p>
</li>
<li><p>The three concrete implementations (<code>Dynamic</code>, <code>Static</code>, <code>Computed</code>) decide <em>how</em> values are stored and <em>how</em> changes trigger recomposition.</p>
</li>
</ul>
<h3><strong>The Base Class: CompositionLocal</strong></h3>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt
@Stable
public sealed class CompositionLocal&lt;T&gt;(defaultFactory: () -&gt; T) {
    // Stores the default value lazily - only computed when actually needed
    internal open val defaultValueHolder: ValueHolder&lt;T&gt; =
        LazyValueHolder(defaultFactory)

    // Called during recomposition to check if the ValueHolder 
    // can be reused or if a new one needs to be created
    internal abstract fun updatedStateOf(
        value: ProvidedValue&lt;T&gt;,
        previous: ValueHolder&lt;T&gt;?,
    ): ValueHolder&lt;T&gt;

    // The public API - asks the Composer to find the current value
    @OptIn(InternalComposeApi::class)
    public inline val current: T
       
    @ReadOnlyComposable @Composable get() = currentComposer.consume(this)
}
</code></pre>
<p>The <code>@Stable</code> annotation tells the Compose compiler that this class has stable equality - two instances with the same data are considered equal. The <code>current</code> property is marked <code>@ReadOnlyComposable</code>, meaning it only reads from the composition and doesn't change it. This lets the compiler make certain optimizations.</p>
<p><strong>What if you try to access</strong> <code>.current</code> outside a <code>@Composable</code> function? You'll get a compile-time error:</p>
<blockquote>
<p><em>"@Composable invocations can only happen from the context of a @Composable function"</em>.</p>
</blockquote>
<p>The Compose compiler enforces this because <code>current</code> needs access to <code>currentComposer</code>, which only exists during composition.</p>
<h3><strong>ProvidableCompositionLocal: The Provider Interface</strong></h3>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt
@Stable
public abstract class ProvidableCompositionLocal&lt;T&gt; internal constructor(defaultFactory: () -&gt; T) :
    CompositionLocal&lt;T&gt;(defaultFactory) {

    // Subclasses implement this to create the right ProvidedValue
    internal abstract fun defaultProvidedValue(value: T): ProvidedValue&lt;T&gt;

    // Always overrides any parent-provided value
    public infix fun provides(value: T): ProvidedValue&lt;T&gt; = defaultProvidedValue(value)

    // Only provides if no ancestor has already provided this local
    public infix fun providesDefault(value: T): ProvidedValue&lt;T&gt; =
        defaultProvidedValue(value).ifNotAlreadyProvided()

    // Provides a lambda that computes the value on each read
    // The lambda runs in CompositionLocalAccessorScope, letting 
    // it read other locals
    public infix fun providesComputed(
        compute: CompositionLocalAccessorScope.() -&gt; T
    ): ProvidedValue&lt;T&gt; =
        ProvidedValue(
            compositionLocal = this,
            value = null,          // No static value
            explicitNull = false,
            mutationPolicy = null,
            state = null,
            compute = compute,     // Store the computation lambda
            isDynamic = false,
        )
}
</code></pre>
<p>The <code>infix</code> modifier enables the clean DSL syntax: <code>LocalColor provides Color.Red</code>. The three methods serve different purposes:</p>
<ul>
<li><p><code>provides</code> - Always sets the value, overriding any parent-provided value</p>
</li>
<li><p><code>providesDefault</code> - Only sets the value if no ancestor has already provided it. Useful for library components that want to offer a default but respect app-level overrides:</p>
<pre><code class="language-kotlin">// Library code: provide a default, but let app override
@Composable
fun LibraryUsage() {
    CompositionLocalProvider(
        LocalAnalytics providesDefault NoOpAnalytics()
    ) {
        LibraryComponent()
    }
}

// App code: this takes precedence over the library's default
@Composable
fun App() {
    CompositionLocalProvider(
        LocalAnalytics provides RealAnalytics()
    ) {
        LibraryUsage()  // Uses RealAnalytics, not NoOpAnalytics
    }
}
</code></pre>
</li>
<li><p><code>providesComputed</code> - Derives the value from other CompositionLocals</p>
</li>
</ul>
<p>The <code>providesComputed</code> option is powerful - it lets you derive values from other CompositionLocals. The lambda runs in <code>CompositionLocalAccessorScope</code>:</p>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt 
public interface CompositionLocalAccessorScope {
    // Extension property to read other locals inside computed lambdas
    public val &lt;T&gt; CompositionLocal&lt;T&gt;.currentValue: T
}
</code></pre>
<p>Inside a <code>providesComputed</code> lambda, you use <code>LocalXxx.currentValue</code> (not <code>.current</code>) to read other locals.</p>
<p><strong>Why</strong> <code>.currentValue</code> instead of <code>.current</code>? The <code>.current</code> property requires being inside a <code>@Composable</code> function (it's marked <code>@Composable</code>), but the <code>providesComputed</code> lambda isn't composable - it's just a regular lambda. The <code>.currentValue</code> extension property reads directly from the scope's map without needing a composition context.</p>
<p>Here's a complete example:</p>
<pre><code class="language-kotlin">// Define the locals
val LocalBaseColor = compositionLocalOf { Color.Blue }
val LocalAccentColor = compositionLocalOf { Color.Blue }

@Composable
fun ThemedContent() {
    // Provide both: one with a value, one computed from the other
    CompositionLocalProvider(
        LocalBaseColor provides Color.Red,
        LocalAccentColor providesComputed {
            // 'this' is CompositionLocalAccessorScope, 
            // so we use .currentValue
            LocalBaseColor.currentValue.copy(alpha = 0.5f)
        }
    ) {
        // When LocalBaseColor changes, LocalAccentColor 
        // automatically reflects it
        val accent = LocalAccentColor.current  // Color.Red with 0.5 alpha
        Text("Accent color applied", color = accent)
    }
}
</code></pre>
<p>Note: The compute lambda runs on <strong>every read</strong> of the local, not reactively. Keep computations cheap.</p>
<h3><strong>⚡ Dynamic vs Static: The Performance Trade-off</strong></h3>
<p>The two main implementations differ in a single but important flag:</p>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt 
internal class DynamicProvidableCompositionLocal&lt;T&gt;(
    // Controls when values are "different"
    private val policy: SnapshotMutationPolicy&lt;T&gt;,  
    defaultFactory: () -&gt; T,
) : ProvidableCompositionLocal&lt;T&gt;(defaultFactory) {

    override fun defaultProvidedValue(value: T) =
        ProvidedValue(
            compositionLocal = this,
            value = value,
            explicitNull = value === null,
            mutationPolicy = policy,// Used for change detection
            state = null,
            compute = null,
            isDynamic = true,       // ← This flag changes everything!
        )
}
</code></pre>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt 
internal class StaticProvidableCompositionLocal&lt;T&gt;(
    defaultFactory: () -&gt; T
) : ProvidableCompositionLocal&lt;T&gt;(defaultFactory) {

    override fun defaultProvidedValue(value: T) =
        ProvidedValue(
            compositionLocal = this,
            value = value,
            explicitNull = value === null,
            mutationPolicy = null,      // No mutation policy needed
            state = null,
            compute = null,
            isDynamic = false,          // ← Static behavior
        )
}
</code></pre>
<p>The <code>isDynamic</code> flag decides the recomposition strategy:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769533504370/32a77adf-0217-4511-a7af-fbe3e14060c5.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Why two strategies?</strong></p>
<p>First, a quick note on the <strong>snapshot system</strong>: This is Compose's change-tracking mechanism. When you read <code>state.value</code>, Compose records that your composable depends on that state. When the state changes later, Compose knows exactly which composables need to recompose - it's like an automatic subscription system.</p>
<ul>
<li><p><strong>Dynamic</strong> wraps values in <code>MutableState</code>, so the snapshot system tracks exactly which composables read the value. When the value changes, only those specific composables recompose. This is great for values that change often (theme colors, user preferences).</p>
</li>
<li><p><strong>Static</strong> stores values directly without snapshot tracking. When the value changes, Compose doesn't know who read it, so it must recompose the entire subtree under the provider. This sounds wasteful, but it skips the overhead of snapshot tracking - perfect for values that rarely or never change (Android Context, configuration objects).</p>
</li>
</ul>
<h2><strong>🏭 Factory Functions</strong></h2>
<p>These are the APIs you use to create CompositionLocals:</p>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt
// Creates a dynamic local with optional custom equality policy
public fun &lt;T&gt; compositionLocalOf(
    policy: SnapshotMutationPolicy&lt;T&gt; = structuralEqualityPolicy(),
    defaultFactory: () -&gt; T,
): ProvidableCompositionLocal&lt;T&gt; = DynamicProvidableCompositionLocal(policy, defaultFactory)

// Creates a static local - use for values that rarely change
public fun &lt;T&gt; staticCompositionLocalOf(defaultFactory: () -&gt; T): ProvidableCompositionLocal&lt;T&gt; =
    StaticProvidableCompositionLocal(defaultFactory)

// Creates a local whose default is computed from other locals
public fun &lt;T&gt; compositionLocalWithComputedDefaultOf(
    defaultComputation: CompositionLocalAccessorScope.() -&gt; T
): ProvidableCompositionLocal&lt;T&gt; = ComputedProvidableCompositionLocal(defaultComputation)
</code></pre>
<p>The <code>policy</code> parameter in <code>compositionLocalOf</code> is often ignored but important. By default, it uses <code>structuralEqualityPolicy()</code> which compares values using <code>==</code>. For large objects or objects with expensive equality checks, you might want <code>referenceEqualityPolicy()</code> (uses <code>===</code>) or a custom policy.</p>
<h2><strong>🔗 Providing and Consuming Values</strong></h2>
<p>Now that we know how to create CompositionLocals, let's see how to provide and consume values. This is the API you'll use most often.</p>
<h3><strong>ProvidedValue: The Key-Value Pair</strong></h3>
<p>When you write <code>LocalColor provides Color.Red</code>, the <code>provides</code> infix function creates a <code>ProvidedValue</code>:</p>
<pre><code class="language-kotlin">// Source: Composer.kt
public class ProvidedValue&lt;T&gt;
internal constructor(
    // The CompositionLocal key this value is for
    public val compositionLocal: CompositionLocal&lt;T&gt;,
    // The actual value (may be null if using state or compute)
    value: T?,
    // True if null was explicitly provided (vs. unset)
    private val explicitNull: Boolean,
    // For dynamic locals: how to compare values for changes
    internal val mutationPolicy: SnapshotMutationPolicy&lt;T&gt;?,
    // For dynamic locals: an existing State to reuse
    internal val state: MutableState&lt;T&gt;?,
    // For computed locals: the computation lambda
    internal val compute: (CompositionLocalAccessorScope.() -&gt; T)?,
    // True if this should use snapshot tracking
    internal val isDynamic: Boolean,
) {
    private val providedValue: T? = value

    // Whether this value can override a parent-provided value
    // Set to false by providesDefault()
    public var canOverride: Boolean = true
        private set

    // Gets the effective value, handling the different ways a 
    // value can be stored
    internal val effectiveValue: T
        get() = when {
            explicitNull -&gt; null as T
            state != null -&gt; state.value      // Read from State
            providedValue != null -&gt; providedValue  // Use direct value
            else -&gt; composeRuntimeError("Unexpected form of a provided value")
        }

    // Returns this with canOverride = false (for providesDefault)
    internal fun ifNotAlreadyProvided() = this.also { canOverride = false }
}
</code></pre>
<p>The multiple nullable fields (<code>value</code>, <code>state</code>, <code>compute</code>) represent different "modes" of providing a value. Only one is typically active at a time:</p>
<table>
<thead>
<tr>
<th>Mode</th>
<th>Primary field</th>
<th>When used</th>
</tr>
</thead>
<tbody><tr>
<td>Direct value</td>
<td><code>value</code></td>
<td>Initial <code>provides</code> call (both static and dynamic)</td>
</tr>
<tr>
<td>State reuse</td>
<td><code>state</code></td>
<td>When reusing an existing <code>DynamicValueHolder</code> across recompositions</td>
</tr>
<tr>
<td>Computed</td>
<td><code>compute</code></td>
<td><code>providesComputed</code></td>
</tr>
</tbody></table>
<p>The <code>isDynamic</code> flag determines whether the value eventually gets wrapped in a <code>MutableState</code> (by the ValueHolder), not which field is used in <code>ProvidedValue</code> itself.</p>
<h3><strong>CompositionLocalProvider</strong></h3>
<p>The composable that sets up a new scope:</p>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt

@Composable
@NonSkippableComposable  // Must always execute - can't be skipped even if inputs unchanged
public fun CompositionLocalProvider(
    vararg values: ProvidedValue&lt;*&gt;,
    content: @Composable () -&gt; Unit,
) {
    // Tell the composer we're starting a provider block
    currentComposer.startProviders(values)
    // Execute the content with the new scope active
    content()
    // Tell the composer we're done - restore the previous scope
    currentComposer.endProviders()
}
</code></pre>
<p>The <code>@NonSkippableComposable</code> annotation is important - even if the provided values haven't changed, we must execute this composable to maintain the scope stack correctly.</p>
<p><strong>API Variants:</strong></p>
<table>
<thead>
<tr>
<th><strong>Function</strong></th>
<th><strong>Use Case</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>CompositionLocalProvider(vararg values, content)</code></td>
<td>Multiple values</td>
</tr>
<tr>
<td><code>CompositionLocalProvider(value, content)</code></td>
<td>Single value (avoids array allocation)</td>
</tr>
<tr>
<td><code>CompositionLocalProvider(context, content)</code></td>
<td>From a captured <code>CompositionLocalContext</code></td>
</tr>
<tr>
<td><code>withCompositionLocal(value, content): T</code></td>
<td>Returns a value (non-Unit)</td>
</tr>
<tr>
<td><code>withCompositionLocals(vararg values, content): T</code></td>
<td>Multiple values, returns a value</td>
</tr>
</tbody></table>
<p>The <code>withCompositionLocal</code> variants are useful when you need to return a value from the content block instead of just rendering UI:</p>
<pre><code class="language-kotlin">// Example: Measure text with a specific density
@Composable
fun measureTextWidth(text: String, customDensity: Density): Int {
    val textMeasurer = rememberTextMeasurer()

    // Temporarily use custom density and return the measured width
    return withCompositionLocal(LocalDensity provides customDensity) {
        textMeasurer.measure(text).size.width  // Returns Int, not Unit
    }
}
</code></pre>
<h3><strong>🔀 Passing Locals Between Compositions</strong></h3>
<p>Sometimes you need to pass CompositionLocals to a composition that isn't a direct child - like a <code>Dialog</code>, <code>Popup</code>, or a custom composition created via <code>Composition()</code>. Compose provides <code>CompositionLocalContext</code> for this:</p>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt
public class CompositionLocalContext internal constructor(
    internal val compositionLocals: PersistentCompositionLocalMap
) {
    // Wraps the persistent map and provides equals/hashCode
}
</code></pre>
<p><strong>Capturing the current context:</strong></p>
<pre><code class="language-kotlin">// Source: Composables.kt 
public val currentCompositionLocalContext: CompositionLocalContext
    @Composable
    get() = 
        CompositionLocalContext(
            currentComposer.buildContext().getCompositionLocalScope()
        )
</code></pre>
<p><strong>When do you need this?</strong> Components like <code>Dialog</code> and <code>Popup</code> create <strong>separate window compositions</strong> - they're not children in the composition tree. Without special handling, your theme colors and other locals wouldn't flow through.</p>
<p><strong>Realistic example - Custom Dialog:</strong></p>
<pre><code class="language-kotlin">@Composable
fun MyScreen() {
    var showDialog by remember { mutableStateOf(false) }

    // We're inside MaterialTheme, so LocalContentColor is available
    Button(onClick = { showDialog = true }) {
        Text("Show Dialog")
    }

    if (showDialog) {
        // Capture ALL current locals before entering the dialog
        val capturedContext = currentCompositionLocalContext

        Dialog(onDismissRequest = { showDialog = false }) {
            // Dialog creates a NEW composition (separate window)
            // Without this, LocalContentColor would be the default, 
            // not the theme's!
            CompositionLocalProvider(capturedContext) {
                Surface {
                    Text(
                        "This text uses the theme's content color!",
                        color = LocalContentColor.current // ✅ Works
                    )
                }
            }
        }
    }
}
</code></pre>
<p><strong>How it works internally:</strong> The <code>CompositionContext</code> abstract class has a <code>getCompositionLocalScope()</code> method. When you create a child composition (like inside <code>Dialog</code>), it calls this method on its parent context to inherit locals. By providing <code>capturedContext</code>, you're bridging the gap between separate compositions.</p>
<hr />
<h2><strong>🔧 Under the Hood: The Internal Implementation</strong></h2>
<p>Now that we've covered how to use CompositionLocals, let's dive into how they actually work. We'll explore three key pieces:</p>
<ol>
<li><p><strong>Value Holders</strong> - How values are stored</p>
</li>
<li><p><strong>Persistent Maps</strong> - How scopes are organized</p>
</li>
<li><p><strong>Composer Integration</strong> - How the runtime manages it all</p>
</li>
</ol>
<h3><strong>📦 Value Holders: The Storage Mechanism</strong></h3>
<p>Values in the CompositionLocal system are wrapped in <code>ValueHolder</code> instances. This abstraction allows different storage strategies depending on whether the value is dynamic, static, computed, or a default:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5fb77e8dba1e08716c77ee91/f63f15c2-f2c6-48e8-b145-cdb9dfdf25d0.png" alt="" style="display:block;margin:0 auto" />

<p><strong>The four holder types serve different purposes:</strong></p>
<table>
<thead>
<tr>
<th><strong>Holder</strong></th>
<th><strong>Storage</strong></th>
<th><strong>When Used</strong></th>
<th><strong>Snapshot Tracked?</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>StaticValueHolder</code></td>
<td>Direct value</td>
<td>Static locals</td>
<td>❌ No</td>
</tr>
<tr>
<td><code>DynamicValueHolder</code></td>
<td><code>MutableState&lt;T&gt;</code></td>
<td>Dynamic locals</td>
<td>✅ Yes</td>
</tr>
<tr>
<td><code>ComputedValueHolder</code></td>
<td>Lambda</td>
<td><code>providesComputed</code></td>
<td>Depends on what it reads</td>
</tr>
<tr>
<td><code>LazyValueHolder</code></td>
<td>Lazy delegate</td>
<td>Default values</td>
<td>❌ No</td>
</tr>
</tbody></table>
<h3><strong>StaticValueHolder</strong></h3>
<p>The simplest holder - a direct wrapper around the value:</p>
<pre><code class="language-kotlin">// Source: ValueHolders.kt 
internal data class StaticValueHolder&lt;T&gt;(val value: T) : ValueHolder&lt;T&gt; {
    // Simply returns the stored value - no indirection
    override fun readValue(map: PersistentCompositionLocalMap): T = value

    override fun toProvided(local: CompositionLocal&lt;T&gt;): ProvidedValue&lt;T&gt; =
        ProvidedValue(
            compositionLocal = local,
            value = value,
            explicitNull = value === null, // Handle explicit null vs unset
            mutationPolicy = null,
            state = null,
            compute = null,
            isDynamic = false,
        )
}
</code></pre>
<p>Because it's a <code>data class</code>, equality comparison is automatic - two <code>StaticValueHolder</code>s with the same value are equal. This helps with efficient change detection during recomposition.</p>
<h3><strong>DynamicValueHolder</strong></h3>
<p>The key to fine-grained recomposition:</p>
<pre><code class="language-kotlin">// Source: ValueHolders.kt
internal data class DynamicValueHolder&lt;T&gt;(val state: MutableState&lt;T&gt;) : ValueHolder&lt;T&gt; {
    // Reading state.value registers a read in the snapshot system
    // This is how Compose knows to recompose only the readers 
    // when value changes
    override fun readValue(map: PersistentCompositionLocalMap): T = 
        state.value

    override fun toProvided(local: CompositionLocal&lt;T&gt;): ProvidedValue&lt;T&gt; =
        ProvidedValue(
            compositionLocal = local,
            value = null,
            explicitNull = false,
            mutationPolicy = null,
            state = state, // Pass the State so it can be reused across recompositions
            compute = null,
            isDynamic = true,
        )
}
</code></pre>
<p>The magic happens in <code>readValue()</code>: accessing <code>state.value</code> is a snapshot read. The snapshot system records this read against the current <code>RecomposeScope</code>. Later, when <code>state.value</code> is written, only the scopes that read it get invalidated. 🎯</p>
<h3><strong>ComputedValueHolder</strong></h3>
<p>For values derived from other locals:</p>
<pre><code class="language-kotlin">// Source: ValueHolders.kt
internal data class ComputedValueHolder&lt;T&gt;(
    val compute: CompositionLocalAccessorScope.() -&gt; T
) : ValueHolder&lt;T&gt; {
    // The map IS the scope - it implements CompositionLocalAccessorScope
    // This lets the compute lambda access other locals via 
    // `LocalXxx.currentValue`
    override fun readValue(map: PersistentCompositionLocalMap): T = 
        map.compute()

    override fun toProvided(local: CompositionLocal&lt;T&gt;): ProvidedValue&lt;T&gt; =
        ProvidedValue(
            compositionLocal = local,
            value = null,
            explicitNull = false,
            mutationPolicy = null,
            state = null,
            compute = compute, // Store the lambda, not the computed value
            isDynamic = false,
        )
}
</code></pre>
<p>Notice that <code>readValue</code> calls <code>map.compute()</code> - the map itself implements <code>CompositionLocalAccessorScope</code>. Inside your compute lambda, you can call <code>LocalOtherThing.currentValue</code> to read other locals. This creates a dependency chain: if the upstream local changes, your computed value updates automatically.</p>
<h3><strong>LazyValueHolder</strong></h3>
<p>Used for default values to avoid unnecessary computation:</p>
<pre><code class="language-kotlin">// Source: ValueHolders.kt
internal class LazyValueHolder&lt;T&gt;(
    valueProducer: () -&gt; T
) : ValueHolder&lt;T&gt; {
    // Kotlin's lazy delegate - computed once on first access
    private val current by lazy(valueProducer)

    override fun readValue(map: PersistentCompositionLocalMap): T = current

    // Defaults can't be re-provided - this is an error
    override fun toProvided(local: CompositionLocal&lt;T&gt;): ProvidedValue&lt;T&gt; =
        composeRuntimeError("Cannot produce a provider from a lazy value holder")
}
</code></pre>
<p>Note this is a <code>class</code>, not a <code>data class</code> - each instance is unique. The lazy delegate makes sure the default factory runs at most once, even if multiple composables read the default.</p>
<h3><strong>🗺️ The Persistent Map Architecture</strong></h3>
<p>CompositionLocals are organized in persistent (immutable) maps. Understanding this architecture is key to understanding how scopes work:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5fb77e8dba1e08716c77ee91/562f815d-b98f-47bb-8988-9cd9d1e9eaba.svg" alt="" style="display:block;margin:0 auto" />

<p><strong>How scope resolution works:</strong></p>
<p>When you call <code>LocalXxx.current</code>, the composer looks for the key in the current scope. Each <code>CompositionLocalProvider</code> creates a new scope that contains its provided values merged with the parent scope. The arrows show inheritance - Provider 3's scope contains all values from providers 1, 2, and 3, plus any unprovided defaults from the root.</p>
<p>The "persistent" in persistent map means these maps use <strong>structural sharing</strong>. When Provider 3 adds one value to Provider 2's scope, it doesn't copy the entire map - it creates a new map that shares most of its structure with the parent. This makes scope creation O(log n) instead of O(n).</p>
<h3><strong>The Read Function</strong></h3>
<p>The core lookup logic is simple:</p>
<pre><code class="language-kotlin">// Source: CompositionLocalMap.kt 
internal fun &lt;T&gt; PersistentCompositionLocalMap.read(
    key: CompositionLocal&lt;T&gt;
): T =
    // Try to find the key in the map; if not found, 
    // use the key's default holder, then call readValue 
    // to get the actual value from the holder
    getOrElse(key as CompositionLocal&lt;Any?&gt;) {
        key.defaultValueHolder 
    }.readValue(this) as T
</code></pre>
<p>This single line does a lot:</p>
<ol>
<li><p>Look up the key in the current scope map</p>
</li>
<li><p>If not found, fall back to the key's <code>defaultValueHolder</code></p>
</li>
<li><p>Call <code>readValue()</code> on the holder to get the actual value</p>
</li>
<li><p>Cast back to <code>T</code> (the unchecked cast is safe because the key and value types are linked)</p>
</li>
</ol>
<h3><strong>Concrete Implementation</strong></h3>
<p>The actual storage uses a <strong>trie-based</strong> persistent hash map from <em>kotlinx.collections.immutable</em>. <em><strong>A trie is a tree structure where keys are broken into smaller pieces</strong></em> (like individual hash digits) - this allows multiple maps to share common branches, making copy-on-write operations efficient:</p>
<pre><code class="language-kotlin">// Source: PersistentCompositionLocalMap.kt 
internal class PersistentCompositionLocalHashMap(
    node: TrieNode&lt;CompositionLocal&lt;Any?&gt;, ValueHolder&lt;Any?&gt;&gt;,
    size: Int,
) :
    // Extends the immutable collections library's PersistentHashMap
    PersistentHashMap&lt;CompositionLocal&lt;Any?&gt;, ValueHolder&lt;Any?&gt;&gt;(node, size),
    PersistentCompositionLocalMap {

    override fun &lt;T&gt; get(key: CompositionLocal&lt;T&gt;): T = read(key)

    override fun putValue(
        key: CompositionLocal&lt;Any?&gt;,
        value: ValueHolder&lt;Any?&gt;,
    ): PersistentCompositionLocalMap {
        // put() returns null if the value is unchanged (optimization)
        val newNodeResult = node.put(key.hashCode(), key, value, 0) 
            ?: return this

        // Create new map with updated node and size delta
        return PersistentCompositionLocalHashMap(
            newNodeResult.node, 
            size + newNodeResult.sizeDelta
        )
    }
}
</code></pre>
<p>The <code>putValue</code> method shows the immutability pattern: instead of changing the map, it returns a new map (<em>or</em> <code>this</code> <em>if nothing changed</em>). The trie structure means only the path from root to the changed leaf is copied - everything else is shared.</p>
<h3><strong>🧠 Composer Integration: The Heart of It</strong></h3>
<p>Now we reach the core implementation in <code>GapComposer</code>. This is where the value holders and maps come together with the composition runtime.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5fb77e8dba1e08716c77ee91/048933c3-cb50-4cd4-99c2-dd6ae63f9c29.png" alt="" style="display:block;margin:0 auto" />

<ol>
<li><p><code>CompositionLocalProvider</code> calls <code>currentComposer.startProviders(values)</code> - since <code>currentComposer</code> is a <code>GapComposer</code> instance (<em>which implements the</em> <code>Composer</code> <em>interface</em>), it goes directly there</p>
</li>
<li><p><code>GapComposer</code> saves the current scope and creates a new merged scope containing the provided values</p>
</li>
<li><p>The content lambda executes with the new scope active</p>
</li>
<li><p>When content calls <code>LocalXxx.current</code>, it calls <code>consume()</code> on the same <code>GapComposer</code>, which reads from the current scope</p>
</li>
<li><p>After content finishes, <code>CompositionLocalProvider</code> calls <code>endProviders()</code>, which restores the previous scope</p>
</li>
</ol>
<p>This push/pop pattern means nested providers work correctly - inner providers shadow outer ones, and exiting a provider restores the outer scope.</p>
<h3><strong>Getting the Current Scope</strong></h3>
<p>Before diving into the code, a quick note on <code>reader</code>: Compose stores the composition tree in a <code>SlotTable</code> - a flat, array-based data structure using a gap buffer algorithm. The <code>reader</code> is a <code>SlotReader</code> that navigates this structure. It provides methods like:</p>
<ul>
<li><p><code>parent</code> / <code>parent(group)</code> - get parent group index</p>
</li>
<li><p><code>groupKey(group)</code> - get the key identifying a group type</p>
</li>
<li><p><code>groupAux(group)</code> - get auxiliary data stored with a group (like provider maps)</p>
</li>
</ul>
<p>Think of it as a cursor that can walk up and down the composition tree.</p>
<pre><code class="language-kotlin">// Source: GapComposer.kt 
private fun currentCompositionLocalScope(): PersistentCompositionLocalMap {
    // Fast path: return cached scope if available
    providerCache?.let { return it }
    // Slow path: walk up the tree to find the nearest provider
    return currentCompositionLocalScope(reader.parent)
}

private fun currentCompositionLocalScope(
    group: Int
): PersistentCompositionLocalMap {
    // ... inserting path omitted for simplicity ...

    if (reader.size &gt; 0) {
        var current = group
        // Walk up the group tree looking for a provider group
        while (current &gt; 0) {
            if (
                reader.groupKey(current) == compositionLocalMapKey &amp;&amp;
                reader.groupObjectKey(current) == compositionLocalMap
            ) {
                // Found it! Check for pending updates first, 
                // then fall back to stored value
                val providers = providerUpdates?.get(current)
                    ?: reader.groupAux(current) as PersistentCompositionLocalMap
                providerCache = providers  // Cache for later reads
                return providers
            }
            current = reader.parent(current)  // Move to parent group
        }
    }
    // No provider found - use root scope
    providerCache = rootProvider
    return rootProvider
}
</code></pre>
<p>The <code>providerCache</code> is a performance optimization. During a single composition pass, many composables might read locals. Caching avoids repeated tree walks. The cache is cleared when entering/exiting provider groups.</p>
<h3><strong>Starting a Provider</strong></h3>
<pre><code class="language-kotlin">// Source: GapComposer.kt 
override fun startProvider(value: ProvidedValue&lt;*&gt;) {
    val parentScope = currentCompositionLocalScope()
    startGroup(providerKey, provider)

    // Get the previous ValueHolder for this local (if any)
    val oldState = rememberedValue().let {
        if (it == Composer.Empty) null else it as ValueHolder&lt;Any?&gt;
    }

    // Ask the CompositionLocal to create/update the ValueHolder
    val local = value.compositionLocal as CompositionLocal&lt;Any?&gt;
    val state = local.updatedStateOf(value as ProvidedValue&lt;Any?&gt;, oldState)

    // If the holder changed, remember the new one
    val change = state != oldState
    if (change) {
        updateRememberedValue(state)
    }

    // Create the new scope by adding this value to the parent scope
    val providers: PersistentCompositionLocalMap
    val invalid: Boolean

    if (inserting) {
        // First composition: create new scope
        providers = if (value.canOverride || !parentScope.contains(local)) {
            parentScope.putValue(local, state)
        } else {
            parentScope  // providesDefault and parent already has value
        }
        invalid = false
        writerHasAProvider = true
    } else {
        // Recomposition: check if scope actually changed
        // ... change detection logic omitted for simplicity ...
    }

    // Push invalidation state and set new scope as current
    providersInvalidStack.push(providersInvalid.asInt())
    providersInvalid = invalid
    providerCache = providers
    start(compositionLocalMapKey, compositionLocalMap, GroupKind.Group, providers)
}
</code></pre>
<p>The key insight here is the <code>updatedStateOf</code> call. This is where dynamic vs static behavior splits:</p>
<pre><code class="language-kotlin">// Source: CompositionLocal.kt
override fun updatedStateOf(
    value: ProvidedValue&lt;T&gt;,
    previous: ValueHolder&lt;T&gt;?,
): ValueHolder&lt;T&gt; {
    return when (previous) {
        is DynamicValueHolder -&gt;
            if (value.isDynamic) {
                // REUSE the existing MutableState, just update its value
                // This triggers snapshot invalidation for readers ✨
                previous.state.value = value.effectiveValue
                previous  // Return the same holder
            } else null  // Switching from dynamic to static

        is StaticValueHolder -&gt;
            // Only reuse if value unchanged
            if (value.isStatic &amp;&amp; value.effectiveValue == previous.value) previous
            else null

        is ComputedValueHolder -&gt;
            // Only reuse if same lambda instance
            if (value.compute === previous.compute) previous
            else null

        else -&gt; null
    } ?: valueHolderOf(value)  // Create new holder if can't reuse
}
</code></pre>
<p>For dynamic values, the <strong>same</strong> <code>MutableState</code> <strong>instance is reused</strong> across recompositions. When <code>previous.state.value = value.effectiveValue</code> runs, the snapshot system notifies all readers of that state. This is the mechanism for fine-grained recomposition!</p>
<h3><strong>Consuming a Value</strong></h3>
<p>After all that complexity, consumption is refreshingly simple:</p>
<pre><code class="language-kotlin">// Source: GapComposer.kt
override fun &lt;T&gt; consume(key: CompositionLocal&lt;T&gt;): T =
    currentCompositionLocalScope().read(key)
</code></pre>
<p>It gets the current scope and calls <code>read()</code>, which we saw earlier handles the lookup and default fallback.</p>
<h3><strong>Ending a Provider</strong></h3>
<pre><code class="language-kotlin">// Source: GapComposer.kt
override fun endProvider() {
    endGroup()
    endGroup()
    providersInvalid = providersInvalidStack.pop().asBool()  // Restore parent's invalid state
    providerCache = null  // Clear cache so next read walks the tree
}
</code></pre>
<p><strong>Why two</strong> <code>endGroup()</code> <strong>calls?</strong> Looking back at <code>startProvider()</code>, it creates two nested groups:</p>
<ol>
<li><p>First group (<code>providerKey</code>) - Wraps the provider itself and stores the <code>ValueHolder</code> via <code>rememberedValue()</code></p>
</li>
<li><p>Second group (<code>compositionLocalMapKey</code>) - Stores the merged scope map as auxiliary data</p>
</li>
</ol>
<p>This nesting allows Compose to store both the individual value holder (for reuse across recompositions) and the merged map (for scope resolution) in the slot table.</p>
<h2><strong>🔄 Putting It All Together: The Complete Flow</strong></h2>
<p>Now that we've seen all the pieces, let's trace what happens when you write <code>LocalColor.current</code>:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/5fb77e8dba1e08716c77ee91/f4edfbb2-6717-40fc-85a8-a70c42743699.png" alt="" style="display:block;margin:0 auto" />

<p><strong>The critical difference</strong> is in the <code>DynamicValueHolder</code> path: calling <code>state.value</code> records a snapshot read. The snapshot system links this read to the current <code>RecomposeScope</code>. Later, when the state changes, only this scope (and others that read the same state) will recompose.</p>
<h2><strong>🔁 Invalidation and Recomposition</strong></h2>
<p>Now that we've seen the implementation details, here's a quick summary of how the two recomposition strategies play out in practice:</p>
<h3><strong>Dynamic CompositionLocal Flow</strong></h3>
<ol>
<li><p>Provider calls <code>startProvider()</code> with a new value</p>
</li>
<li><p><code>updatedStateOf()</code> finds the existing <code>DynamicValueHolder</code></p>
</li>
<li><p>It updates <code>holder.state.value = newValue</code></p>
</li>
<li><p>The snapshot system marks all readers of this state as invalid</p>
</li>
<li><p>On next frame, only those specific composables recompose ✨</p>
</li>
</ol>
<h3><strong>Static CompositionLocal Flow</strong></h3>
<ol>
<li><p>Provider calls <code>startProvider()</code> with a new value</p>
</li>
<li><p><code>updatedStateOf()</code> sees the value changed</p>
</li>
<li><p>It creates a new <code>StaticValueHolder</code> (can't reuse the old one)</p>
</li>
<li><p>The new scope map is different from the old one</p>
</li>
<li><p><code>providersInvalid</code> is set to <code>true</code></p>
</li>
<li><p>The entire content subtree must recompose because Compose doesn't know who read this value 🌳</p>
</li>
</ol>
<h3><strong>When to Use Which (Examples):</strong></h3>
<table>
<thead>
<tr>
<th><strong>Use Case</strong></th>
<th><strong>Type</strong></th>
<th><strong>Reason</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Theme colors 🎨</td>
<td>Dynamic</td>
<td>May animate or change based on user preference</td>
</tr>
<tr>
<td>Text styles</td>
<td>Dynamic</td>
<td>May change with accessibility settings</td>
</tr>
<tr>
<td>Android Context</td>
<td>Static</td>
<td>Never changes during composition lifetime</td>
</tr>
<tr>
<td>Window insets</td>
<td>Static</td>
<td>Changes require full layout recalculation anyway</td>
</tr>
<tr>
<td>Computed accent colors</td>
<td>Computed</td>
<td>Derived from base theme color</td>
</tr>
</tbody></table>
<h2><strong>💡 Practical Insights</strong></h2>
<h3><strong>Performance Things to Know</strong></h3>
<ol>
<li><p><strong>Dynamic locals have per-read overhead</strong>: Each <code>LocalXxx.current</code> records a snapshot read. For locals read in tight loops or many places, this adds up.</p>
</li>
<li><p><strong>Static locals have per-change overhead</strong>: Changing a static local recomposes everything below the provider. But if it never changes, there's no overhead.</p>
</li>
<li><p><strong>Computed locals run on every read</strong>: The lambda runs each time you call <code>.current</code>. Keep computations cheap, or use dynamic locals with remembered computed values.</p>
</li>
</ol>
<hr />
<p>The CompositionLocal system shows thoughtful API design:</p>
<ul>
<li><p><strong>Simple surface API</strong>: <code>provides</code> and <code>.current</code> cover 99% of use cases</p>
</li>
<li><p><strong>Layered complexity</strong>: Value holders, persistent maps, and composer integration work together smoothly</p>
</li>
<li><p><strong>Clear trade-offs</strong>: Dynamic vs static makes the performance implications obvious</p>
</li>
<li><p><strong>Efficient implementation</strong>: Structural sharing, lazy evaluation, and precise invalidation minimize overhead</p>
</li>
</ul>
<p>The sealed class hierarchy ensures type safety. Value holders abstract storage strategies. Persistent maps enable efficient scope management. And the composer integration ties it all together with the rest of the composition system.</p>
<p>Understanding these internals helps you make better decisions: use <code>compositionLocalOf</code> for values that might change, <code>staticCompositionLocalOf</code> for stable configuration, and <code>compositionLocalWithComputedDefaultOf</code> for derived values. Place providers as close to consumers as practical, and remember that every <code>LocalXxx.current</code> call has implications for recomposition tracking.</p>
<hr />
<p>Awesome. I hope you've gained some valuable insights from this. If you enjoyed this write-up, please share it 😉, because...</p>
<p><em><strong>"Sharing is Caring"</strong></em></p>
<p>Thank you! 😄 Happy composing! 😎</p>
<p>Let's catch up on <a href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[You Can't Multitask. Your AI Agent Can.]]></title><description><![CDATA[Hey developers 👋🏻
If you've been following my blog, you know what I write about. Around 60 posts, almost all of them about Android, Kotlin, Compose, Coroutines. That was my world.
Today is different]]></description><link>https://blog.shreyaspatil.dev/you-can-t-multitask-your-ai-agent-can</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/you-can-t-multitask-your-ai-agent-can</guid><category><![CDATA[AI]]></category><category><![CDATA[vibe coding]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[Git]]></category><category><![CDATA[multitasking]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[gemini]]></category><category><![CDATA[codex]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 02 Mar 2026 04:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/b2239b80-6203-4c8d-acd5-1a1623a19267.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey developers 👋🏻</p>
<p>If you've been following my blog, you know what I write about. Around 60 posts, almost all of them about Android, Kotlin, Compose, Coroutines. That was my world.</p>
<p>Today is different. 😉</p>
<p>This post is about how I changed the way I work, not what I work on. And honestly, it's the best productivity change I've made in years as a Software Engineer.</p>
<hr />
<h2>🧠 <strong>A quick fact before we begin.</strong></h2>
<p>Research shows that humans are actually terrible at multitasking. <em><strong>The brain can't do two complex tasks at the same time</strong></em>. It switches rapidly between them, and every switch has a cost. Psychologists call these "<em>switch costs.</em>" They decrease your productivity by up to 40%, increase errors, and hurt short-term memory. The more you switch, the more tired your brain gets and the worse your work becomes.</p>
<p>Every time you drop a half-finished feature to review a PR, your brain pays a tax. Every time you jump from debugging a crash to writing new code, you pay it again.</p>
<p>We've always just accepted this as part of the job.</p>
<p>But here's the thing: <strong>AI agents don't have this problem.</strong> 😄 They don't get mentally tired. They don't lose their place. An agent working on your onboarding screen has no idea your bug fix agent even exists. It doesn't care. It just works.</p>
<p>That's what this post is about.</p>
<hr />
<h2>🧑🏻‍💻 Let's begin the story!</h2>
<p>It's random day's morning and you've been in the zone for 45 minutes. Deep in a feature branch, working through some tricky state management and architecture-related logic in the app, and finally, <em>finally</em>, the pieces are coming together for the new feature you're going to develop. Your editor is exactly how you left it. You know exactly where everything is in the code. You are, for once, actually in flow.</p>
<p>Then Slack lights up ➡️ <strong>A production crash, reported overnight</strong>. Users are hitting some crashes 💣 on the <strong>login</strong> <strong>screen</strong> on certain Android devices. Now you've to fix that crash on priority and do a hotfix too. Scroll down a bit and there's a teammate waiting on your PR review. They've been blocked since yesterday, and the comment timestamp is starting to feel accusatory. You stare at your terminal. One of these has to wait. Two of them will kill your context. You'll spend the next hour not doing any of them well, jumping between half-finished thoughts and losing the thread on all three. The important thing is that <em><strong>all of this has to be done in the same project!</strong></em></p>
<p>Then I found a better way.</p>
<p>Most developers use AI tools like a search engine. <em>One question, one answer, one task at a time</em>. But Claude Code <em>(Anthropic's AI agent that runs in your terminal)</em>, combined with git's <strong>worktree feature</strong> (<em>which most developers never read about in the docs</em>), makes something different possible: <strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">you can run multiple AI agents in parallel, each working on a separate task, while you focus on just one thing</mark></strong>. One developer, multiple agents, multiple tasks, all moving forward.</p>
<p>By the end of this post, you'll know how to set up multiple AI agents across isolated git worktrees, so you can work on a crash fix, a feature screen, and a code review all at the same time, without losing context on any of them.</p>
<p>If you don't know about Git worktree then let's have a quick understanding.</p>
<h3><strong>🌿 What Is a Git Worktree?</strong></h3>
<p>Most developers think of it this way: one repo, one working directory. Switch branches and your files change. Need to jump to a hotfix? Stash your work, switch branches, fix the bug, switch back, pop the stash. And hope nothing breaks.</p>
<p>Worktrees change that. With <code>git worktree add</code>, you check out a second (or third) branch in a completely separate folder. Same repo, same git history, no stashing, no switching. Both branches just exist on disk at the same time.</p>
<p>Think of it like having two tabs open in the same Google Doc, except each tab shows a different version of the file. What you do in one tab doesn't touch the other.</p>
<p>So you can have <code>main</code>, <code>feature/onboarding-screen</code>, and <code>fix/login-crash</code> all checked out at the same time, in separate folders, with no conflicts. Three commands get you there:</p>
<pre><code class="language-shell"># Create a new worktree for a branch
git worktree add ../myapp-feature feature/onboarding-screen

# List all active worktrees
git worktree list

# Remove a worktree when done
git worktree remove ../myapp-feature
</code></pre>
<p>That's it. No extra setup needed. It ships with git.</p>
<h3><strong>🤖 Which AI agent?</strong></h3>
<p>This could work with any AI agent of your choice. Be it Claude Code CLI, Gemini CLI, Codex, Copilot, or anything. I'm using Claude Code CLI a lot since many months now. That's why you'll find mentions and usages of Claude in this blog but you can try it with your favourite agent. If you've not yet tried Claude Code CLI, it's an AI agent from Anthropic that runs in your terminal. Unlike a chat interface, it doesn't just answer questions. It reads your actual codebase, runs commands, writes and edits files, and works on its own. You point it at a problem and it goes.</p>
<p>Here's what makes it really useful: AI agent in terminal only sees the folder it's running in. Start a session inside <code>/myapp-feature</code> and it sees that branch, its files, its history, its context. Start a separate session in <code>/myapp-bugfix</code> and it sees that one. The two sessions don't know about each other at all.</p>
<p>This is the thing that clicked for me: <strong>one worktree + one Claude Code session = one autonomous agent</strong>. 🎯</p>
<img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/1f91c2ca-6e65-4d70-9b22-3c9d05e244c8.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>"Three worktrees, three Claude Code sessions: three agents working in parallel while you stay focused on what only you can do."</p>
</blockquote>
<h2><strong>⚡ Let's continue: Three Tasks, Zero Blocking</strong></h2>
<p>Back to our story. same three tasks, except this time you don't pick one and let the others wait.</p>
<blockquote>
<p>"Here's exactly what I do now when this happens."</p>
</blockquote>
<p>The three tasks:</p>
<ol>
<li><p><strong>Feature:</strong> implement a new onboarding feature in the app (<code>feature/onboarding-screen</code>). I already have a plan ready as I was in the zone for ~45m.</p>
</li>
<li><p><strong>Bug:</strong> fix the crash in the app (<code>fix/login-crash</code>). This was an ad-hoc requirement and priority too!</p>
</li>
<li><p><strong>PR review:</strong> teammate's work (in <code>main</code>). This was a work backlog.</p>
</li>
</ol>
<p>Here's how to set up a worktree and a Claude Code agent for each one.</p>
<h3><strong>Step 1: Create the Worktrees</strong></h3>
<p>From your main repo directory, create a worktree for each branch. Each folder is fully isolated. Same git history, but completely separate working trees.</p>
<pre><code class="language-shell"># From your main repo directory
git worktree add ../myapp-feature feature/onboarding-screen
git worktree add ../myapp-bugfix fix/login-crash

# Verify
git worktree list
</code></pre>
<p>Git creates the <code>../myapp-feature</code> and <code>../myapp-bugfix</code> folders for you. You don't need to create them first.</p>
<p>You should see all three worktrees listed:</p>
<pre><code class="language-shell">/Users/you/myapp           abc1234 [main]
/Users/you/myapp-feature   def5678 [feature/onboarding-screen]
/Users/you/myapp-bugfix    ghi9012 [fix/login-crash]
</code></pre>
<p>If the branch doesn't exist yet, <code>git worktree add</code> creates it automatically. If it already exists remotely, use <code>-b</code> to create a proper local tracking branch: <code>git worktree add -b feature/onboarding-screen ../myapp-feature origin/feature/onboarding-screen</code>. Without the <code>-b</code> flag, passing a remote ref directly creates a detached HEAD instead of a named branch.</p>
<h3><strong>Step 2: Launch Claude Code in Each Worktree</strong></h3>
<p>Open three terminal windows (or tmux panes) and point each one at its folder:</p>
<pre><code class="language-shell"># Terminal 1 — onboarding screen feature
cd ../myapp-feature &amp;&amp; claude

# Terminal 2 — login crash fix
cd ../myapp-bugfix &amp;&amp; claude

# Terminal 3 — your main repo (for PR review)
cd ~/myapp
</code></pre>
<p>Each Claude session starts fresh and only sees its own folder. The agents don't know about each other. No shared state, no bleed-over between sessions. Terminal 3 is yours for the PR review. No agent needed there unless you want one.</p>
<h3>💰Claude CLI Bonus tip</h3>
<p>In the previous section, I used raw git commands for creating worktrees so that you can use any AI agent with it but if you're only using Claude, then it has built-in command for quick worktree</p>
<pre><code class="language-shell"># Start Claude in a worktree named "feature-auth"
# Creates .claude/worktrees/myapp-feature/ with a new branch
claude --worktree myapp-feature

# Start another session in a separate worktree
claude --worktree myapp-bugfix

# Auto-generates a name like "bright-running-fox"
claude --worktree
</code></pre>
<p>So yeah, if you don't want to manage worktrees manually, this is the way to go 🚀.</p>
<h3><strong>Step 3: Give Each Agent Its Task 📝</strong></h3>
<p>Let's write prompts that say exactly what done looks like.</p>
<p><strong>For the feature agent (Terminal 1):</strong></p>
<blockquote>
<p>"Implement the onboarding screen as described in <code>docs/onboarding-spec.md</code>. It should be built in Jetpack Compose and: (1) show a 3-step progress indicator, (2) validate each field before allowing the user to advance to the next step, (3) call <code>/api/onboarding/complete</code> via the existing <code>OnboardingRepository</code> on the final step. Write unit tests for the ViewModel logic. Don't touch the auth flow or <code>LoginActivity</code>."</p>
</blockquote>
<p><strong>For the bug agent (Terminal 2):</strong></p>
<blockquote>
<p>"Investigate and fix the NullPointerException on the login screen described in <code>bug-reports/2026-02-27-login.md</code>. The crash reproduces on API 28 and below. Reproduce it first with a unit test, then fix the root cause. Don't change the login UI or touch <code>LoginViewModel</code>."</p>
</blockquote>
<p><em>(as this is just an example, the actual prompts would be very detailed).</em></p>
<h3><strong>Meanwhile: You Do the PR Review 👀</strong></h3>
<p>You hit Enter on both prompts, watch the agents start up, then switch to Terminal 3. Pull up the PR diff. Your teammate's work, 400 lines, needs a proper look. You open the first file. Launch <em><strong>agent</strong></em> in <strong># Terminal 3</strong> as well and maybe you could use it to understand your teammate's work or take help in reviewing that.</p>
<blockquote>
<p><em>Review</em> <em>the</em> <em>&lt;TASK_NAME&gt;</em> <em>PR of my colleague: .</em></p>
<p><em>- Check</em> <em>if</em> <em>&lt;condition 1&gt;</em><br /><em>- Give me a review summary including suggestions</em><br /><em>- Why there is usage of API in the change?</em><br /><em>- Or anything else here...</em></p>
</blockquote>
<p>Behind you, Terminals 1 and 2 are still running. Agent is reading files, making a plan, running commands. You can peek over or ignore it completely. The work is happening either way.</p>
<p>A few minutes in, you take a quick look at Terminal 1:</p>
<pre><code class="language-shell">  Reading app/src/main/java/com/example/app/onboarding/OnboardingScreen.kt
  Reading docs/onboarding-spec.md
  Writing app/src/main/java/com/example/app/onboarding/OnboardingViewModel.kt

  OnboardingViewModelTest &gt; validates required fields before advancing PASSED
  OnboardingViewModelTest &gt; calls complete endpoint on final step PASSED

BUILD SUCCESSFUL in 18s
2 tests, 0 failures
</code></pre>
<p>Tests are green. ✅ It found the spec, read the composable, wrote the ViewModel logic, and confirmed everything works. All while you were reading someone else's code.</p>
<p>You finish the PR review, leave your comments, approve it. Then you check back in.</p>
<p><strong>Terminal 1:</strong> the onboarding feature is drafted, two ViewModel tests passing, files updated exactly where the spec said.</p>
<p><strong>Terminal 2:</strong> the login crash on API 28 reproduced, root cause found (a null pointer in the token refresh path), fix committed with a test that now passes. Both agents finished without asking me anything. No interruptions, no clarifying questions on this run. It feels the same way every time.</p>
<p><strong>Terminal 3:</strong> It reviewed your colleague's PR, clarified your questions and also highlighted some changes your colleague should do.</p>
<p>You didn't manage them. You didn't step in. You did your own work, and when you came back, so had they.</p>
<h3><strong>Step 4: Clean Up the Worktrees 🧹</strong></h3>
<p>When each agent is done and you've reviewed its work:</p>
<pre><code class="language-shell"># Push each branch for review
cd ../myapp-feature
git push origin feature/onboarding-screen

cd ../myapp-bugfix
git push origin fix/login-crash

# Remove the worktrees
cd ~/myapp
git worktree remove ../myapp-feature
git worktree remove ../myapp-bugfix
</code></pre>
<blockquote>
<p>Fact: Agents/Claude can also push and create PR on behalf of you! So you don't even need to run these above commands these days.</p>
</blockquote>
<p>One thing worth knowing: removing a worktree only deletes the folder. Your branches and commits are still there. You can check them out, open PRs, or delete them later in the usual way.</p>
<p>Two PRs open, one PR reviewed and approved. All from a single morning session. That's the whole workflow.</p>
<h2><strong>🔧 Patterns That Make This Work</strong></h2>
<h3><strong>Naming convention</strong></h3>
<p>Use <code>../projectname-&lt;branch-slug&gt;</code> as your worktree path. Easy to find with a quick <code>ls</code>.</p>
<p>When all your worktrees follow the same prefix, a quick <code>ls ..</code> shows you everything that's active: <code>myapp</code>, <code>myapp-feature</code>, <code>myapp-bugfix</code>. No guessing which folder belongs to which task.</p>
<h3><strong>Shell aliases</strong></h3>
<p>Typing out the full <code>git worktree add</code> command every time gets old. Two aliases handle the common cases:</p>
<pre><code class="language-shell"># Add to .zshrc / .bashrc

## Create a new worktree (works for both new and existing branches)
alias wt-new='f() { local slug="\({1//\//-}"; local wt_path="../\)(basename \(PWD)-\)slug"; git worktree add "\(wt_path" "\)1" 2&gt;/dev/null || git worktree add -b "\(1" "\)wt_path"; }; f'

## Remove a worktree (does NOT delete the branch)
alias wt-clean='f() { local slug="\({1//\//-}"; git worktree remove "../\)(basename \(PWD)-\)slug"; }; f'

# Usage
wt-new feature/onboarding-screen
wt-clean feature/onboarding-screen
</code></pre>
<p><code>wt-new</code> builds the worktree path from your repo name and the branch you pass in. <code>wt-clean</code> removes it using the same pattern. One argument, done.</p>
<h3><strong>When NOT to use worktrees</strong> ⚠️</h3>
<p>Worktrees don't work well for every situation. Skip them when:</p>
<ul>
<li><p>Tasks share a build cache with side effects (common in multi-module projects)</p>
</li>
<li><p>Both tasks modify the same build files (such as <code>build.gradle</code>, <code>gradle/libs.versions.toml</code>)</p>
</li>
<li><p>Task B depends on task A finishing first</p>
</li>
</ul>
<p>In these cases, running agents in parallel will cause conflicts or wasted work. Run them one at a time instead, and keep the parallel setup for tasks that are truly independent of each other.</p>
<h2><strong>⏱️ The Time Math</strong></h2>
<p>Let's put some real numbers on this. Those three tasks from this morning (<em>new onboarding screen, login crash fix, PR review</em>) are a pretty normal engineer morning. Here's roughly how the time looks across three different ways of working:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5fb77e8dba1e08716c77ee91/81de7690-b53f-4e18-a3e8-eb26869ee426.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p>The first row is the usual dev day: context switching, stashing, losing focus.</p>
</li>
<li><p>The second is what most people think of when they say they "use AI": faster, but still one task at a time.</p>
</li>
<li><p>The third is what this whole post is about.</p>
</li>
</ul>
<p>That's not a small difference. That's shipping three things in a morning instead of one. Over a week, that's actually clearing your backlog instead of just feeling busy.</p>
<p>The bottleneck was never your skill. It was the one-task-at-a-time model. AI agents remove that limit, if you let them.</p>
<h2><strong>💡 What This Actually Changed</strong></h2>
<p>The speed improvement was real, but that's not what I think about. What changed was how day feel. I stopped staring at three tasks trying to figure out which one to sacrifice. Instead of "<em>what do I have to skip today?</em>" it became "<em>what do I want to work on while the rest gets handled?</em>"</p>
<p>You're not just a developer anymore. <strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">You're the person who decides what gets built and reviews what comes back. The agents handle the actual execution.</mark></strong></p>
<p>I remember the moment it really hit me. There was a bug that another team's member was supposed to fix, something that only showed up in a very specific edge case. I gave it to an agent as a side task while I worked on a new feature. Ten minutes later I looked over and it was fixed. A day of putting it off, gone in the time it took to write one decent prompt.</p>
<hr />
<p>Do definitely try it on your next feature. Set up two worktrees, give your AI agent specific tasks with clear acceptance criteria, and go review that PR you've been putting off. See what's done when you come back. And when you ship more things early, tell me what you built in the comments below. 🚀</p>
<p>Awesome. I hope you've gained some valuable insights from this. If you enjoyed this write-up, please share it 😉, because...</p>
<p><em><strong>"Sharing is Caring"</strong></em></p>
<p>Thank you! 😄 Happy vide coding! 😎</p>
<p>Let's catch up on <a href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<hr />
<h2>📚References</h2>
<ul>
<li><p><a href="https://www.apa.org/topics/research/multitasking">https://www.apa.org/topics/research/multitasking</a></p>
</li>
<li><p><a href="https://git-scm.com/docs/git-worktree">https://git-scm.com/docs/git-worktree</a></p>
</li>
<li><p><a href="https://code.claude.com/docs/en/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees">https://code.claude.com/docs/en/common-workflows#run-parallel-claude-code-sessions-with-git-worktrees</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[DroidCon: Debugging App Performance at Scale]]></title><description><![CDATA[Hello 👋🏻,
It was an absolutely amazing experience at DroidCon India 2025. Seeing so many Android engineers under one roof at the world's largest Android conference was something special. The energy at DroidCon India 2025 was high. There were incred...]]></description><link>https://blog.shreyaspatil.dev/droidcon-debugging-app-performance-at-scale</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/droidcon-debugging-app-performance-at-scale</guid><category><![CDATA[droidconindia]]></category><category><![CDATA[Tech Talk]]></category><category><![CDATA[community]]></category><category><![CDATA[Public Speaking]]></category><category><![CDATA[Android]]></category><category><![CDATA[performance]]></category><category><![CDATA[app performance]]></category><category><![CDATA[app development]]></category><category><![CDATA[android app development]]></category><category><![CDATA[droidcon]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 19 Jan 2026 04:58:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768716367946/c7ae6e10-03fa-4345-9f87-b9dbf377df1d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello 👋🏻,</p>
<p>It was an absolutely amazing experience at DroidCon India 2025. Seeing so many Android engineers under one roof at the world's largest Android conference was something special. The energy at DroidCon India 2025 was high. There were incredible speakers, people from different backgrounds, and so many ideas coming together.</p>
<h2 id="heading-watch-the-talk">Watch the Talk</h2>
<p>If you could not make it or want to watch the talk again, the recording is now out.</p>
<h3 id="heading-session-recording">📺 Session Recording</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/t4xYihWEcaA?si=Vny-Otq-yw1rKY5p">https://youtu.be/t4xYihWEcaA?si=Vny-Otq-yw1rKY5p</a></div>
<p> </p>
<h3 id="heading-slides">📊 Slides</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://docs.google.com/presentation/d/1vUjziS5fX-0IlorgzchxVxrKbRYW9kBmLI08qaeQtJM/edit?usp=sharing">https://docs.google.com/presentation/d/1vUjziS5fX-0IlorgzchxVxrKbRYW9kBmLI08qaeQtJM/edit?usp=sharing</a></div>
<p> </p>
<h2 id="heading-my-session-android-app-performance">My Session: Android App Performance ⚡</h2>
<p>I got the chance to speak on a topic I really care about: <strong>Android App Performance</strong>.</p>
<p>My session was the very last one of the event, so I was surprised by the crowd. The room was full with almost <strong>~400 developers</strong>! It was great to see so many people stay until the end to learn about performance. The feedback I got was really nice, and I am glad people found the talk useful.</p>
<p>I have always been a big fan of DroidCon. This event is very special to me. The last time DroidCon happened in India, I was just a student in college. I looked up to the experts on stage back then.</p>
<p>Now in 2025, it felt great to see it return to India. It was even better to be part of the <strong>Program Committee</strong>. It felt like a full circle moment for me to contribute to the event that inspired me when I was starting out.</p>
<h2 id="heading-the-community">The Community</h2>
<p>Meeting people outside the sessions was just as good as the talks. I met so many passionate developers and had great conversations. Met my friends with whom I always have Android-ish tech chats.</p>
<p>One of the best parts was when people came up to me to say that my blogs or open-source work helped them. That feeling is hard to describe. It is exactly why I love this community. 🙌</p>
<p>Some photos:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768798543181/37867a3d-5fab-4f2a-b180-530564abe146.jpeg" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768798556677/bb9bf1e9-30bc-4998-8aee-848fb9efb752.jpeg" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768798561947/26b85435-0bda-45d5-a07c-67717a2ec721.jpeg" alt class="image--center mx-auto" /></p>
<hr />
<p>A big thank you to everyone who attended, the organizers, and the volunteers who made DroidCon India 2025 a success. See you at the next one! 👋</p>
]]></content:encoded></item><item><title><![CDATA[A Simple key to a Better LazyList in Jetpack Compose]]></title><description><![CDATA[Hey Composers 👋,
It’s awesome to see Jetpack Compose being adopted in so many apps. If you're using Compose, you've almost certainly worked with its powerful LazyList APIs like LazyColumn and LazyRow. They are incredibly efficient for displaying lis...]]></description><link>https://blog.shreyaspatil.dev/a-simple-key-to-a-better-lazylist-in-jetpack-compose</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/a-simple-key-to-a-better-lazylist-in-jetpack-compose</guid><category><![CDATA[compose]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[Android]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Kotlin Multiplatform]]></category><category><![CDATA[UI]]></category><category><![CDATA[performance]]></category><category><![CDATA[android app development]]></category><category><![CDATA[compose multiplatform]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Wed, 08 Oct 2025 12:00:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759672465129/e50a15f0-4cf8-4608-a3e7-debe8a2f814c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Composers 👋,</p>
<p>It’s awesome to see Jetpack Compose being adopted in so many apps. If you're using Compose, you've almost certainly worked with its powerful <code>LazyList</code> APIs like <code>LazyColumn</code> and <code>LazyRow</code>. They are incredibly efficient for displaying list of items.</p>
<p>However, there's a tiny parameter in the items function that can cause some seriously confusing behavior if overlooked. In this blog, we're going to explore how missing one small thing: the key and how it can lead to tricky state management issues.</p>
<h2 id="heading-preface">Preface</h2>
<p>A while back, I posted a poll on <a target="_blank" href="https://x.com/imShreyasPatil/status/1974419516940804546">X</a> and <a target="_blank" href="https://www.linkedin.com/posts/patil-shreyas_androiddev-jetpackcompose-kotlin-activity-7380186618109976576-A2wp?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABUdUs4B-VoTZYn5iAoOP7UC0hoQURs4hlU">LinkedIn</a> asking developers a simple question:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759855211952/e1457a0d-1c6b-4c0e-8dad-a857ae0adfde.png" alt class="image--center mx-auto" /></p>
<p>The results were interesting and mostly similar across both platforms!</p>
<ul>
<li><p>🟢 <em>Always use</em> — 60%</p>
</li>
<li><p>🟡 <em>Sometimes</em> — 32%</p>
</li>
<li><p>🔴 <em>Never/Didn’t know</em> — 8%</p>
</li>
</ul>
<p>This poll inspired me to write this post to shed some light on this important detail especially for 40% of Android developers.</p>
<p>Before we dive into the problem, let's quickly understand what this key parameter actually does.</p>
<h3 id="heading-what-is-key-in-lazycolumn">What is <code>key {}</code> in LazyColumn</h3>
<p>When you provide a list of items to a LazyColumn or LazyRow, you can also provide a key for each item. This key is a stable and unique identifier for that specific piece of data.</p>
<pre><code class="lang-kotlin">LazyColumn {
    items(
        items = myItems,
        key = { item -&gt; item.id } <span class="hljs-comment">// Provide a unique ID for each item</span>
    ) { item -&gt;
        MyItemRow(item)
    }
}
</code></pre>
<p>Think of it like a primary key in a database table. It gives Compose a way to track each item individually, even if the list changes.</p>
<p><strong>Why is this important?</strong></p>
<ul>
<li><p><strong>Performance Boost 🚀:</strong> When you add, remove, or reorder items in your list, Compose uses these keys to understand which items have changed. This allows it to be much smarter. For example, if you reorder items, Compose can just move the corresponding composables without completely redrawing them. This avoids unnecessary work and makes your app feel smoother.</p>
</li>
<li><p><strong>Stable Identity:</strong> The key tells Compose, "Hey, this item is the same one as before, it's just in a different position now." Remember that <code>DiffUtil.ItemCallback</code> in RecyclerView? 🤔</p>
</li>
</ul>
<p><strong>What happens if you don't specify a key?</strong></p>
<p>If you skip the key, Compose falls back to using the item's <strong>position (or index)</strong> in the list as its identifier. For a list that never changes, this is perfectly fine. But for a dynamic list where items can be added or removed, this can lead to some unexpected problems. Because whenever the list changes, even if some items in the list might not have been changed, it’ll still cause recompositions for such items.</p>
<p>The most important rule for keys is that they <strong>must be unique</strong>. If two items have the same key, your app will crash with an error.</p>
<p>Now, let's get to the fun part and see what can go wrong.</p>
<h2 id="heading-what-can-go-wrong-if-key-is-missing">What can go wrong if <code>key</code> is missing</h2>
<p>Let's imagine a simple scenario. We have a LazyColumn showing a list of fruits. Each Fruit item is a composable that can be updated. When you tap the '+' or '-' buttons, it changes a quantity, and the item's background highlights to show it has been modified.</p>
<p>Here’s what the UI for a single fruit item looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759646871717/66d2b314-ec7b-41d9-b874-18d6daa941f8.png" alt class="image--center mx-auto" /></p>
<p>And here is the code for our <code>Fruit</code> composable:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">Fruit</span><span class="hljs-params">(fruit: <span class="hljs-type">Fruit</span>, modifier: <span class="hljs-type">Modifier</span> = Modifier)</span></span> {
    Row(<span class="hljs-comment">/*...*/</span>) {
        <span class="hljs-keyword">var</span> quantity <span class="hljs-keyword">by</span> remember { mutableIntStateOf(<span class="hljs-number">0</span>) }
        <span class="hljs-keyword">var</span> hasChanged <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-literal">false</span>) }

        Text(fruit.emojifiedImage, <span class="hljs-comment">/*Other parameters*/</span>)

        Column(<span class="hljs-comment">/*...*/</span>) {
            Text(fruit.name)
            Row {
                Button(onClick = { quantity-- }) { Text(<span class="hljs-string">"-"</span>) }
                Button(onClick = { quantity++ }) { Text(<span class="hljs-string">"+"</span>) }
            }
        }

        <span class="hljs-keyword">if</span> (hasChanged) {
            Box(Modifier.background(MaterialTheme.colorScheme.tertiary)) {
                Text(<span class="hljs-string">"<span class="hljs-variable">$quantity</span>"</span>, <span class="hljs-comment">/*Other parameters*/</span>)
            }
        }

        <span class="hljs-comment">// 👀 Pay attention to this. This only notifies whether any changes have been made </span>
        <span class="hljs-comment">// to this fruit or not.</span>
        LaunchedEffect(<span class="hljs-built_in">Unit</span>) {
            snapshotFlow { quantity }.filter { it != <span class="hljs-number">0</span> }.first()
            hasChanged = <span class="hljs-literal">true</span>
            println(<span class="hljs-string">"<span class="hljs-subst">${fruit.name}</span> has changed"</span>)
        }
    }
}
</code></pre>
<p>Notice that the <code>quantity</code> and <code>hasChanged</code> states are managed inside the <code>Fruit</code> composable using <code>remember {}</code>. <code>hasChanged</code> is a boolean state which is being mutated only from <code>LaunchedEffect</code> only when <code>quantity</code> is updated for the first time, then prints a simple log to console and it leaves the block. Since underneath it’s using a <code>snapshotFlow {}</code>, there’s no need to specify a key to <code>LaunchedEffect</code>.</p>
<p>Finally, we display the list on a screen and add a button to remove the first item. We are <strong>not</strong> providing a key in our LazyColumn.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">DemoScreen</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> fruits <span class="hljs-keyword">by</span> remember { mutableStateOf(sampleFruits) }

    Column(<span class="hljs-comment">/*...*/</span>) {
        OutlinedButton(onClick = { fruits = fruits.removeAt(<span class="hljs-number">0</span>) }) {
            Text(<span class="hljs-string">"Remove first"</span>)
        }
        LazyColumn(<span class="hljs-comment">/*...*/</span>) {
            items(fruits) {
                Fruit(it)
            }
        }
    }
}
</code></pre>
<p>Now, let's perform a few actions on the UI:</p>
<ol>
<li><p>Click + on <strong>Apple</strong> -&gt; logs <code>Apple has changed</code></p>
</li>
<li><p>Click “<strong>Remove first</strong>” on top -&gt; It removed the first item from list i.e. <strong>Apple</strong> -&gt; Now <strong>Banana</strong> is in 1st place. But it’s still keeping the state of old item along with highlighted background</p>
</li>
<li><p>Click + on <strong>Orange</strong> -&gt; logs <code>Grape has changed</code></p>
</li>
<li><p>Click + on <strong>Peach</strong> -&gt; logs <code>Strawberry has changed</code></p>
</li>
</ol>
<p>See it here:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759647935570/d06822ff-0d92-4c58-9c95-53c0ddcb543e.gif" alt class="image--center mx-auto" /></p>
<p>Surprised? 🤯 This shouldn’t have happened right?</p>
<p>This strange behavior happens because of how Compose reuses composables.</p>
<p>When you don't provide a <code>key</code>, Compose identifies each item by its <strong>position</strong>. In our example, the "Apple" item was at position 0. It had its own internal state (<code>quantity</code> and <code>hasChanged</code>) that we modified.</p>
<p>When we removed "Apple" from our data list, "Banana" moved into position 0. From Compose's perspective, it sees that there is still a composable at position 0. To be efficient, it decides to <strong>reuse</strong> the existing composable and just give it the new data ("Banana" instead of "Apple").</p>
<p>The problem is that the <strong>remembered state</strong> from the old item is still attached to that composable "slot" at position 0. So, "Banana" inherits the <code>quantity</code> and <code>hasChanged</code> state that originally belonged to "Apple".</p>
<p>In ideal situations, when the state comes from the ViewModel, UI state-related issues won't be noticeable. However, <strong>the concern here is with the behavior of</strong> <code>LaunchedEffect</code>. If you're calling some business logic from side-effect APIs or using anything related to it, it can be confusing.</p>
<h3 id="heading-the-simple-solution-provide-a-key"><strong>The Simple Solution: Provide a</strong> <code>key</code></h3>
<p>This entire problem can be fixed with a single line of code. We just need to tell Compose how to uniquely identify each fruit.</p>
<pre><code class="lang-diff">LazyColumn(/*...*/) {
<span class="hljs-deletion">-   items(fruits) {</span>
<span class="hljs-addition">+   items(fruits, key = { it.id }) {</span>
       Fruit(it)
    }
}
</code></pre>
<p>By adding <code>key = { fruit -&gt; fruit.id }</code>, we are now telling Compose to track items by their unique ID, not by their position.</p>
<p>With this change, when we remove "Apple" (which has its own unique ID), Compose knows that the composable associated with that specific key is gone forever. It completely removes it, along with its state. It then creates a fresh new composable for "Banana" in its place, with a clean state. Everything works as you would expect! ✅</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759648027397/b8e19120-5504-4324-94f3-c0ab5707fe0f.gif" alt class="image--center mx-auto" /></p>
<p>And console also prints valid values meaning LaunchedEffect block is actually working as expected</p>
<pre><code class="lang-plaintext">Apple has changed
Banana has changed
Orange has changed
Peach has changed
</code></pre>
<h3 id="heading-an-alternative-but-flawed-solution"><strong>An Alternative (But Flawed) Solution 🤔</strong></h3>
<p>You might be wondering if there are other solutions. For example, you could pass the <code>fruit</code> object as a key to the <code>remember</code> and <code>LaunchedEffect</code> blocks inside the <code>Fruit</code> composable.</p>
<pre><code class="lang-diff"><span class="hljs-deletion">- var quantity by remember { mutableIntStateOf(0) }</span>
<span class="hljs-addition">+ var quantity by remember(fruit) { mutableIntStateOf(0) }</span>
<span class="hljs-deletion">- var hasChanged by remember { mutableStateOf(false) }</span>
<span class="hljs-addition">+ var hasChanged by remember(fruit) { mutableStateOf(false) }</span>
//...
<span class="hljs-deletion">- LaunchedEffect(Unit) {/*...*/}</span>
<span class="hljs-addition">+ LaunchedEffect(fruit) {/*...*/}</span>
</code></pre>
<p>This tells Compose to reset the state whenever the <code>fruit</code> input changes, which does fix the issue.</p>
<p>But ideally, a composable shouldn't have to do this. Why should the <code>Fruit</code> composable worry that it might receive another item’s content? It's not aware that it's being used in a list. In real-world scenarios, we often read state from a ViewModel, so a situation like this might not happen. But the main takeaway is that if you're using stateful APIs like <code>remember</code> or side-effects like <code>LaunchedEffect</code>, they will be affected by this recycling behavior.</p>
<p>A component shouldn’t have the overhead of providing extra keys, as it shouldn't have to worry about where it’s going to be rendered. The responsibility should lie with the parent that is managing the list.</p>
<h3 id="heading-under-the-hood-how-key-works">Under the Hood: How <code>key</code> Works? 🧙‍♂️</h3>
<p>So what’s happening internally? It all comes down to how Compose generates and caches the composables for your list items.</p>
<p>Deep inside the Compose framework, there's a class called <code>LazyLayoutItemContentFactory</code>. Its main job is to manage the content (the composable lambdas) for each item in the lazy list. It has a cache, which is essentially a map that stores the generated composable for each item.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// From LazyLayoutItemContentFactory.kt</span>
<span class="hljs-comment">/** Contains the cached lambdas produced by the [itemProvider]. */</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> lambdasCache = mutableScatterMapOf&lt;Any, CachedItemContent&gt;()
</code></pre>
<p>When it's time to display an item, the factory's <code>getContent</code> method is called. This method is the key to our mystery.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// Simplified from LazyLayoutItemContentFactory.kt</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getContent</span><span class="hljs-params">(index: <span class="hljs-type">Int</span>, key: <span class="hljs-type">Any</span>, contentType: <span class="hljs-type">Any</span>?)</span></span>: <span class="hljs-meta">@Composable</span> () -&gt; <span class="hljs-built_in">Unit</span> {
    <span class="hljs-keyword">val</span> cached = lambdasCache[key] <span class="hljs-comment">// Tries to find a cached item using the key</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> (cached != <span class="hljs-literal">null</span> &amp;&amp; ...) {
        cached.content <span class="hljs-comment">// Found it! Return the cached composable.</span>
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Didn't find it. Create a new one and add it to the cache.</span>
        <span class="hljs-keyword">val</span> newContent = CachedItemContent(index, key, contentType)
        lambdasCache[key] = newContent
        newContent.content
    }
}
</code></pre>
<p>When you provide a <code>key</code>, that <code>key</code> is used to look up the item in <code>lambdasCache</code>. Since your key is stable and unique (like <code>fruit.id</code>), Compose can always find the correct composable along with its remembered state.</p>
<p>But if you <strong>don't</strong> provide a key, Compose uses the item's <strong>index</strong> as the key. So when "Apple" at index 0 is removed, "Banana" moves to index 0. Compose looks in the cache for index 0 and finds the old composable that belonged to "Apple", and reuses it for "Banana".</p>
<p>This cached content is then passed to <code>subcompose</code>, which is the mechanism that actually creates and manages the UI tree for that item.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// From LazyLayoutMeasureScopeImpl.kt</span>
<span class="hljs-keyword">val</span> itemContent = itemContentFactory.getContent(index, key, contentType)
<span class="hljs-keyword">return</span> subcomposeMeasureScope.subcompose(key, itemContent)
</code></pre>
<p>By passing your stable <code>key</code> to <code>subcompose</code>, you ensure that the state is correctly associated with the data, not just the position.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>So, what's the main takeaway?</p>
<p>If your list is completely static and will never change, you can get away with not using a key. However, for any list that is dynamic where items can be added, removed, or reordered then providing a key is essential. It not only improves performance but is also crucial for correct state management.</p>
<p>I think we should <strong>make it a habit to always add a key</strong>. You might think a list is static today, but requirements can change in future and then it’s easy to miss it in PR review. Adding a key from the start makes your code more robust and saves you from debugging some very confusing issues down the road.</p>
<hr />
<p>I hope you got the idea about how important it is to provide key to LazyList APIs.</p>
<p>Awesome. I hope you've gained some valuable insights from this. If you enjoyed this write-up, please share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄 Happy composing! 😎</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[Exploring PausableComposition internals in Jetpack Compose]]></title><description><![CDATA[Hey Composers 👋, In the recent Compose release 1.9.X, a new internal API in compose-runtime called PausableComposition was introduced, which claims to solve performance issues. It can feel like magic, but under the hood, it's all thanks to some very...]]></description><link>https://blog.shreyaspatil.dev/exploring-pausablecomposition-internals-in-jetpack-compose</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/exploring-pausablecomposition-internals-in-jetpack-compose</guid><category><![CDATA[Android]]></category><category><![CDATA[UI]]></category><category><![CDATA[performance]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[compose]]></category><category><![CDATA[Declarative]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Kotlin Multiplatform]]></category><category><![CDATA[Performance Optimization]]></category><category><![CDATA[Internals]]></category><category><![CDATA[UIUX]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 14 Jul 2025 05:34:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751791091096/4e5b36e3-485c-4079-88be-987082e7d67e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Composers 👋, In the recent Compose release 1.9.X, a new <em>internal</em> API in compose-runtime called <code>PausableComposition</code> was introduced, which claims to solve performance issues. It can feel like magic, but under the hood, it's all thanks to some very clever engineering. While digging into the Compose runtime to understand this better, I came across a powerful internal tool that makes it all possible.</p>
<p>This post will break down that mechanism: <code>PausableComposition</code>. This is internal API of compose and it’s not necessary for developers to know about it. But it’s good to know how it works under the hood. For Jetpack Compose developers who want to look behind the curtain and understand <em>how</em> Compose achieves its incredible performance, this exploration will give you a clearer picture. We'll dive into the runtime source code to see how it works, why it's so important for performance, and how everything is coordinated to make our UIs feel so fluid. Let's get started!</p>
<h2 id="heading-the-why">The “Why”</h2>
<p>To get a fluid 60 frames per second (fps), our app needs to draw each frame in under 16.7 milliseconds. When a user scrolls through a <code>LazyColumn</code>, new items have to be created, measured, and drawn within this tiny window.</p>
<p>If an item is complex, with nested layouts, images, and lots of logic, the work needed to compose it can easily take longer than 16ms. When this happens, the main thread gets blocked, a frame is dropped, and the user sees a "jank" or stutter in the scroll. 😩</p>
<p>This is exactly the problem <code>PausableComposition</code> was created to solve.</p>
<h2 id="heading-the-what-a-smarter-way-to-compose">The "What": A Smarter Way to Compose</h2>
<p>Imagine you're a chef preparing a big meal for an event. 👨‍🍳 Instead of frantically trying to cook everything from scratch when the first guest arrives, you do your <em>mise en place</em> (the prep work) hours before. You chop vegetables, prepare sauces, and bake desserts. When it's time to serve, the final cooking and assembly are incredibly fast.</p>
<p><code>PausableComposition</code> brings this "prep work" idea to Compose. It lets the runtime:</p>
<ol>
<li><p><strong>Compose Incrementally:</strong> Break down the composition of a big UI element into smaller, more manageable pieces.</p>
</li>
<li><p><strong>Prepare Asynchronously:</strong> Do this composition work <em>before</em> the UI is actually needed on screen, often using the idle time between frames.</p>
</li>
</ol>
<p>This pre-warming of composables means that when an item finally scrolls into view, most of the heavy lifting is already done, allowing it to show up almost instantly.</p>
<p>To visualize the concept, have a look on the below animation:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751997214840/f9f7308a-7b83-4a3d-9d38-e3ce6aa8d9a9.gif" alt class="image--center mx-auto" /></p>
<p>As scrolling occurs, let's say items A, B, C, D, and E are already visible on the screen, and the next item is <strong>F</strong>. If item <strong>F</strong> has a complex layout or structure that requires more time for layout computation or other pre-processing before rendering on the UI, this pre-processing happens in chunks within the frame timeline (i.e., 16ms). So, if it requires 2 frames, the necessary pre-processing for <strong>F</strong> is completed over 2 frames <strong><em>in the idle times</em></strong> without causing any jank to the frames. Finally, it is drawn on the UI when it needs to be visible. The same process applies to items <strong>G</strong> and <strong>H</strong>.</p>
<hr />
<h2 id="heading-how-it-works-the-core-components">How It Works: The Core Components</h2>
<p>By looking at the runtime source code, we can see how this is handled through a few key interfaces and classes. While you won't use these APIs directly, understanding them shows how <code>LazyColumn</code> gets its performance. 🕵️‍♂️</p>
<h3 id="heading-the-lifecycle-pausablecomposition-and-its-controller">The Lifecycle: <code>PausableComposition</code> and its Controller</h3>
<p>The journey starts with the <code>PausableComposition</code> interface, which extends <code>ReusableComposition</code> to add the ability to be paused.</p>
<details><summary>A quick note on ReusableComposition</summary><div data-type="detailsContent">Before we talk about pausing, what is <code>ReusableComposition</code>? It's a special kind of composition made for high-performance situations where UI content needs to be recycled efficiently. Think of the items in a <code>LazyColumn</code>. Instead of destroying the whole composition of an item that scrolls off-screen, <code>ReusableComposition</code> lets the runtime <code>deactivate</code> it. This keeps the underlying UI nodes but clears out the remembered state. This deactivated composition can then be quickly "re-inflated" with new content, saving the cost of creating nodes from scratch. <code>PausableComposition</code> builds directly on this powerful recycling foundation.</div></details>

<p>Here’s how <code>PausableComposition</code> looks like:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt;l=66</span>
<span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">PausableComposition</span> : <span class="hljs-type">ReusableComposition {</span></span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setPausableContent</span><span class="hljs-params">(content: @<span class="hljs-type">Composable</span> () -&gt; <span class="hljs-type">Unit</span>)</span></span>: PausedComposition
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">setPausableContentWithReuse</span><span class="hljs-params">(content: @<span class="hljs-type">Composable</span> () -&gt; <span class="hljs-type">Unit</span>)</span></span>: PausedComposition
}
</code></pre>
<p><em>(Note: The interface is</em> <code>sealed</code> because it has a closed, limited set of implementations only inside the Compose runtime. This gives the compiler more information to make optimizations.)</p>
<p>Calling <code>setPausableContent</code> doesn't immediately compose the UI. Instead, it returns a <code>PausedComposition</code> object, which acts as our controller for the step-by-step process.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt;l=112</span>
<span class="hljs-keyword">sealed</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">PausedComposition</span> </span>{
    <span class="hljs-keyword">val</span> isComplete: <span class="hljs-built_in">Boolean</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">resume</span><span class="hljs-params">(shouldPause: <span class="hljs-type">ShouldPauseCallback</span>)</span></span>: <span class="hljs-built_in">Boolean</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">apply</span><span class="hljs-params">()</span></span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">cancel</span><span class="hljs-params">()</span></span>
}
</code></pre>
<p>This lifecycle is best shown as a state machine:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751864148018/27512893-f186-47e7-9bdc-26036460aed7.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><code>resume(shouldPause: ShouldPauseCallback)</code>: This is the engine. The prefetching system (<em>here in the context of</em> <code>LazyColumn</code>) repeatedly calls <code>resume()</code> to do chunks of composition work. The magic is in the <code>shouldPause</code> callback. The Compose runtime calls this lambda often during composition. If it returns <code>true</code> (for example, because the frame deadline is near), the composition process stops, giving the main thread back to more important work like drawing the current frame.</p>
</li>
<li><p><code>apply()</code>: Once <code>resume()</code> returns <code>true</code>, which signals it's finished, <code>apply()</code> is called. This takes all the calculated UI changes and commits them to the actual UI tree.</p>
</li>
<li><p><code>cancel()</code>: If the user scrolls away and the pre-composed item is no longer needed, <code>cancel()</code> is called to throw away the work and free up resources.</p>
</li>
</ul>
<h3 id="heading-a-look-at-the-internals-pausedcompositionimpl">A Look at the Internals: <code>PausedCompositionImpl</code></h3>
<p>The state machine above is managed by the internal <code>PausedCompositionImpl</code> class. This class holds the state and connects all the pieces.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt;l=202</span>
<span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PausedCompositionImpl</span></span>(...) : PausedComposition {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> state = PausedCompositionState.InitialPending
    <span class="hljs-keyword">internal</span> <span class="hljs-keyword">val</span> pausableApplier = RecordingApplier(applier.current)
    <span class="hljs-comment">// ...</span>

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">resume</span><span class="hljs-params">(shouldPause: <span class="hljs-type">ShouldPauseCallback</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">when</span> (state) {
            PausedCompositionState.InitialPending -&gt; {
                <span class="hljs-comment">// This is the first time resume() is called.</span>
                <span class="hljs-comment">// It starts the initial composition of the content.</span>
                invalidScopes =
                    context.composeInitialPaused(composition, shouldPause, content)
                state = PausedCompositionState.RecomposePending
                <span class="hljs-keyword">if</span> (invalidScopes.isEmpty()) markComplete()
            }
            PausedCompositionState.RecomposePending -&gt; {
                <span class="hljs-comment">// This is for subsequent calls to resume().</span>
                state = PausedCompositionState.Recomposing
                <span class="hljs-comment">// It tells the Composer to continue where it left off,</span>
                <span class="hljs-comment">// processing any pending invalidations.</span>
                invalidScopes =
                    context.recomposePaused(composition, shouldPause, invalidScopes)
                state = PausedCompositionState.RecomposePending
                <span class="hljs-keyword">if</span> (invalidScopes.isEmpty()) markComplete()
            }
            <span class="hljs-comment">// ... other states like Recomposing, Applied, Cancelled are handled here ...</span>
        }
        <span class="hljs-keyword">return</span> isComplete
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">apply</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// ... other state checks ...</span>
        <span class="hljs-keyword">if</span> (state == PausedCompositionState.ApplyPending) {
            applyChanges() <span class="hljs-comment">// The call site</span>
            state = PausedCompositionState.Applied
        }
        <span class="hljs-comment">// ...</span>
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">applyChanges</span><span class="hljs-params">()</span></span> {
        <span class="hljs-comment">// ...</span>
        pausableApplier.playTo(applier, rememberManager)
        rememberManager.dispatchRememberObservers()
        rememberManager.dispatchSideEffects()
        <span class="hljs-comment">// ...</span>
    }
}
</code></pre>
<p>When <code>resume()</code> is called, it checks its internal state and acts accordingly:</p>
<ol>
<li><p><code>InitialPending</code>: On the first call, it kicks off the composition process by calling <code>context.composeInitialPaused</code>. This tells the core <code>ComposerImpl</code> to start executing the <code>@Composable</code> content, honoring the <code>shouldPause</code> callback.</p>
</li>
<li><p><code>RecomposePending</code>: On subsequent calls, it continues the work by calling <code>context.recomposePaused</code>. This is used to process any parts of the composition that were invalidated (due to state changes) or to continue work that was previously paused.</p>
</li>
<li><p><code>Applier</code>: Throughout this process, the <code>ComposerImpl</code> directs all its UI-changing operations to the <code>pausableApplier</code> (the <code>RecordingApplier</code>), which buffers them instead of applying them immediately.</p>
</li>
<li><p>This continues until the work is complete or the <code>shouldPause</code> callback returns true.</p>
</li>
</ol>
<h3 id="heading-the-recordingapplier-deferring-the-final-touches">The <code>RecordingApplier</code>: Deferring the Final Touches</h3>
<p>A key performance trick is the <a target="_blank" href="https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt;l=344"><code>RecordingApplier</code></a>. When <code>resume()</code> is called, the <code>Composer</code> doesn't change the live UI tree directly. Doing that in small pieces could be slow and lead to a weird, half-updated UI.</p>
<p>Instead, the <code>PausableComposition</code> uses a <code>RecordingApplier</code>. This special <code>Applier</code> just records all the UI operations it's supposed to do (like "create a <code>Text</code> node," "set its <code>text</code> property," or "add a child <code>Image</code>") into an internal list.</p>
<p>Only when <code>PausedComposition.apply()</code> is called does the <code>RecordingApplier</code> "play back" its recorded list of operations onto the <em>real</em> <code>Applier</code>, updating the UI tree in one efficient, single step. The public <code>apply()</code> method on <code>PausedComposition</code> is a simple state-machine guard. The real work happens in the internal <code>applyChanges()</code> method (<em>as in the above snippet</em>).</p>
<p>When <code>applyChanges</code> is called, it does three critical things in order:</p>
<ol>
<li><p>It tells the <code>RecordingApplier</code> to play back all of its buffered commands onto the real <code>applier</code>. This is what makes the UI actually appear on screen.</p>
</li>
<li><p>It dispatches all the <code>onRemembered</code> lifecycle callbacks for any <code>RememberObserver</code>s (like <code>DisposableEffect</code>) that were created.</p>
</li>
<li><p>Finally, it runs any <code>SideEffect</code>s that were queued during the composition.</p>
</li>
</ol>
<p>This ordered, batched process ensures the UI is updated efficiently and all lifecycle events happen at the correct time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751794209046/49f5c1c7-b424-4c7d-a017-8ba65bb7b2d3.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-lazylist-using-pausablecomposition">LazyList using PausableComposition</h2>
<p>LazyList has started using PausableComposition API. In a <code>LazyList</code>, <code>PausableComposition</code> doesn't work alone. It's part of a well-coordinated system.</p>
<ul>
<li><p><strong>The Conductor (</strong><code>Recomposer</code>): The main <code>Recomposer</code> keeps the beat, driving the frame-by-frame updates for the visible UI.</p>
</li>
<li><p><strong>The Planner (</strong><code>LazyLayoutPrefetchState</code>): As the user scrolls, this component predicts which items are about to show up.</p>
</li>
<li><p><strong>The Stage Manager (</strong><code>SubcomposeLayout</code>): This powerful <code>SubcomposeLayout</code> is the foundation of <code>LazyList</code>. Its <code>SubcomposeLayoutState</code> can create and manage compositions for individual items when needed. Most importantly, it provides the <a target="_blank" href="https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt;l=250"><code>createPausedPrecomposition()</code></a> API.</p>
</li>
<li><p><strong>The Stagehand (</strong><code>PrefetchScheduler</code>): This scheduler finds idle time between frames to do the pre-composition work requested by the Planner.</p>
</li>
</ul>
<p>It's also interesting to see how this feature was developed. Inside the <a target="_blank" href="https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt;l=647"><code>LazyLayoutPrefetchState</code></a> file, you can find the feature flag that controls it:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// A simplified look inside LazyLayoutPrefetchState.kt: https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt;l=647</span>
<span class="hljs-keyword">if</span> (ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled) {
    <span class="hljs-comment">// This is the future, modern path.</span>
    performPausableComposition(key, contentType, average)
} <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// This is the older, non-pausable fallback.</span>
    performFullComposition(key, contentType)
}
</code></pre>
<p>This flag, <code>isPausableCompositionInPrefetchEnabled</code>, acts as a kill-switch. While its default value in the source code is <code>false</code>. If you want to enable pausable composition behaviour in the Lazy layouts (LazyColumn, LazyRow, etc), then we can simply enable it as follows:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApplication</span> : <span class="hljs-type">Application</span></span>() {
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">()</span></span> {
        ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled = <span class="hljs-literal">true</span>
        <span class="hljs-keyword">super</span>.onCreate()
    }
}
</code></pre>
<h4 id="heading-the-planner-lazylayoutprefetchstate-in-detail">The Planner: <code>LazyLayoutPrefetchState</code> in Detail</h4>
<p>The <code>LazyLayoutPrefetchState</code> is the brain of the prefetching operation. Its job is to take the prediction from the <code>LazyLayout</code> (e.g., "item 25 is coming up") and turn it into an actual pre-composition task.</p>
<p>It does this through a <code>PrefetchHandleProvider</code>, which creates a <code>PrefetchRequest</code>. This request is a unit of work that the <code>PrefetchScheduler</code> can execute. Inside this request, we find the heart of the pausing logic.</p>
<p>When the <code>PrefetchScheduler</code> executes a request, it enters a loop that calls <code>resume()</code> on the <code>PausableComposition</code>. The lambda passed to <code>resume</code> is where the decision to pause is made.</p>
<p>So if the above feature flag is enabled, it executes request through Pausable composition API as follows:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt;l=754</span>
<span class="hljs-comment">// Simplified from HandleAndRequestImpl inside LazyLayoutPrefetchState</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> PrefetchRequestScope.<span class="hljs-title">performPausableComposition</span><span class="hljs-params">(...)</span></span> {
    <span class="hljs-keyword">val</span> composition = <span class="hljs-comment">// get the composition for the item of the LazyLayout </span>
    pauseRequested = <span class="hljs-literal">false</span>

    <span class="hljs-keyword">while</span> (!composition.isComplete &amp;&amp; !pauseRequested) {
        composition.resume {
            <span class="hljs-keyword">if</span> (!pauseRequested) {
                <span class="hljs-comment">// 1. Update how much time is left in this frame's idle window.</span>
                updateElapsedAndAvailableTime()

                <span class="hljs-comment">// 2. Save how long this work chunk took, to improve future estimates.</span>
                averages.saveResumeTimeNanos(elapsedTimeNanos)

                <span class="hljs-comment">// 3. The Core Decision: Is there enough time left to do another</span>
                <span class="hljs-comment">//    chunk of work without risking a frame drop?</span>
                pauseRequested = !shouldExecute(
                    availableTimeNanos,
                    averages.resumeTimeNanos + averages.pauseTimeNanos,
                )
            }
            <span class="hljs-comment">// 4. Return the decision to the composition engine.</span>
            pauseRequested
        }
    }

    updateElapsedAndAvailableTime()
    <span class="hljs-keyword">if</span> (pauseRequested) {
        <span class="hljs-comment">// If we decided to pause, record how long the final pause check took.</span>
        averages.savePauseTimeNanos(elapsedTimeNanos)
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// If we finished without pausing, record the time for the final resume chunk.</span>
        averages.saveResumeTimeNanos(elapsedTimeNanos)
    }
}
</code></pre>
<p>Let's break down this logic:</p>
<ol>
<li><p><code>updateElapsedAndAvailableTime()</code>: Inside the <code>resume</code> lambda, the system constantly checks how much time is left before the next frame needs to be drawn.</p>
</li>
<li><p><code>averages.saveResumeTimeNanos(...)</code>: It records how long each small piece of composition work takes. This helps it build an average (<code>averages</code>) to predict the cost of future work.</p>
</li>
<li><p><code>!shouldExecute(...)</code>: This is the core decision. It compares the <code>availableTimeNanos</code> against a budget. This budget is a smart estimate: the average time it takes to do another chunk of work <em>plus</em> the average time it takes to pause. If there isn't enough time, <code>pauseRequested</code> becomes <code>true</code>.</p>
</li>
<li><p><strong>Final Timing</strong>: After the loop exits for this cycle (either because the work is done or a pause was requested), one final <code>updateElapsedAndAvailableTime()</code> is called. This captures the time of the very last operation.</p>
</li>
<li><p><strong>Saving Averages</strong>: The system then saves this final timing. If a pause was requested, it contributes to <code>pauseTimeNanos</code>. If the loop completed naturally, it contributes to <code>resumeTimeNanos</code>. This ensures the historical data used for future predictions is always accurate.</p>
</li>
</ol>
<p>This self-regulating feedback loop allows the prefetcher to be aggressive when the system is idle but polite and respectful of the main thread when it's time to render the UI.</p>
<h4 id="heading-the-final-act-applying-the-pre-composed-ui">The Final Act: Applying the Pre-Composed UI</h4>
<p>So, what happens when the pre-composed item is actually needed on screen? This is where the <code>SubcomposeLayout</code> takes center stage. During its normal measure pass, it calls its <code>subcompose</code> function for the now-visible item. Internally, this triggers the final step.</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt;l=1186</span>
<span class="hljs-comment">// Simplified from LayoutNodeSubcompositionsState inside SubcomposeLayout.kt</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> NodeState.<span class="hljs-title">applyPausedPrecomposition</span><span class="hljs-params">(shouldComplete: <span class="hljs-type">Boolean</span>)</span></span> {
    <span class="hljs-keyword">val</span> pausedComposition = <span class="hljs-keyword">this</span>.pausedComposition
    <span class="hljs-keyword">if</span> (pausedComposition != <span class="hljs-literal">null</span>) {
        <span class="hljs-comment">// 1. If the work must be completed now...</span>
        <span class="hljs-keyword">if</span> (shouldComplete) {
            <span class="hljs-comment">// ...force the composition to finish by looping `resume`</span>
            <span class="hljs-comment">// and always passing `false` to the `shouldPause` callback.</span>
            <span class="hljs-keyword">while</span> (!pausedComposition.isComplete) {
                pausedComposition.resume { <span class="hljs-literal">false</span> }
            }
        }
        <span class="hljs-comment">// 2. Apply the changes to the real UI tree.</span>
        pausedComposition.apply()
        <span class="hljs-keyword">this</span>.pausedComposition = <span class="hljs-literal">null</span> <span class="hljs-comment">// Clear the handle.</span>
    }
}
</code></pre>
<p>When an item becomes visible, its composition is no longer a low-priority background task; it's a high-priority, synchronous requirement. The <code>shouldComplete = true</code> parameter ensures that any remaining composition work is finished immediately, without pausing. Then, <code>apply()</code> is called, and the fully formed UI appears on screen instantly.</p>
<p>Here’s how they work together:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751799217100/82837ef0-e97e-49c7-bee6-cd68ab1804ba.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>After a deep dive into the Compose runtime, the design of <code>PausableComposition</code> is a really smart piece of performance engineering.</p>
<ul>
<li><p><strong>It's Not Magic, It's Deferral:</strong> The main idea is to do work <em>before</em> it's urgent. By composing items during idle time, the work needed on the main thread during a fast scroll is much, much smaller.</p>
</li>
<li><p><strong>Cooperative &amp; Non-Blocking:</strong> The <code>shouldPause</code> callback is a brilliant way to handle multitasking. It lets long-running composition tasks politely step aside for the more urgent task of rendering the current frame, which directly prevents jank.</p>
</li>
<li><p><strong>Efficiency Through Batching:</strong> The <code>RecordingApplier</code> avoids the overhead of many small, separate changes to the UI tree by grouping them into a single, efficient update.</p>
</li>
</ul>
<p>While <code>PausableComposition</code> is an internal feature you may never use directly, understanding its existence and operation gives you a real appreciation for the smart decisions that make Jetpack Compose so performant. The next time you effortlessly scroll through a complex <code>LazyColumn</code> without a single stutter, you'll know about the clever, well-orchestrated dance happening just beneath the surface. ✅ This architecture not only solves today's performance challenges but also paves the way for even more advanced rendering strategies in the future of Compose.</p>
<hr />
<p>I hope you got the idea about how this new API works in Jetpack Compose.</p>
<p>Awesome. I hope you've gained some valuable insights from this. If you enjoyed this write-up, please share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[Deep dive into annotations in Jetpack Compose]]></title><description><![CDATA[Hey Composers 👋, when walking through the internal code in different Compose libraries, we often encounter variety annotations commonly used in Jetpack Compose's standard libraries. Understanding their meanings and uses is quite beneficial and these...]]></description><link>https://blog.shreyaspatil.dev/deep-dive-into-annotations-in-jetpack-compose</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/deep-dive-into-annotations-in-jetpack-compose</guid><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[compose]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[jetpack]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[UI]]></category><category><![CDATA[optimization]]></category><category><![CDATA[performance]]></category><category><![CDATA[app development]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 19 May 2025 05:16:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747498739278/25239878-63e8-492e-8f9b-7db7b7b732e6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Composers 👋, when walking through the internal code in different Compose libraries, we often encounter variety annotations commonly used in Jetpack Compose's standard libraries. Understanding their meanings and uses is quite beneficial and these annotations can also help to <strong>improve the performance of composables</strong> <em>if used correctly</em>, so I decided to write about it. Let's get started.</p>
<p>This post aims to demystify three such compiler annotations: <code>@ReadOnlyComposable</code>, <code>@NonRestartableComposable</code>, and <code>@NonSkippableComposable</code>. For Jetpack Compose developers already familiar with the basics, this exploration will provide clearer insights into how these annotations work, when to use them, and how they can help build even more polished and performant applications, complete with practical examples.</p>
<h1 id="heading-quick-refresher">⚡ Quick Refresher</h1>
<p>Before diving into the specific annotations, it's essential to revisit some fundamental concepts of Jetpack Compose's rendering model. These mechanisms are central to how Compose achieves its efficiency and dynamism.</p>
<h2 id="heading-recomposition"><strong>Recomposition</strong></h2>
<p>Recomposition is the process by which Jetpack Compose re-executes composable functions when their underlying state or inputs change. This is the core mechanism that keeps the UI in sync with the application's data. Typically, a change to a <code>State&lt;T&gt;</code> object that is read within a composable will trigger its recomposition. Compose diligently tracks which composables depend on which state objects to perform these updates efficiently.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747494075971/3f81fc0c-1ee5-476b-81c9-42a3e67721f2.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-skipping-composes-intelligent-laziness"><strong>Skipping - Compose's Intelligent Laziness</strong></h2>
<p>To avoid unnecessary work, Compose features an intelligent skipping mechanism. If a composable's inputs have not changed since its last execution, Compose can skip re-running that composable and its children, reusing the previously emitted UI. This is a cornerstone of Compose's performance strategy, as it prevents entire subtrees from re-rendering if their data remains constant. Several conditions must be met for a composable to be eligible for skipping: its parameters must be stable, it should not have a non-<code>Unit</code> return type, and it must not be annotated with <code>@NonSkippableComposable</code> or be a non-restartable composable (which has its own skipping implications, let’s have a look on it later in this post).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747494655494/51142246-c47e-4db6-b4e7-9456d580b0d6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-restartability-independent-re-invocation"><strong>Restartability - Independent Re-invocation</strong></h2>
<p>Restartable composables are functions that the Compose runtime can re-invoke independently during recomposition. This means they establish their own "<strong>restart scope</strong>." If a state read within a restartable composable changes, Compose can restart the execution from that specific composable. In contrast, if a non-restartable composable needs to update due to its own parameter changes, the recomposition process might be initiated from its nearest restartable parent scope.</p>
<p>A <strong>restartable</strong> composable function serves as a distinct "scope" where the recomposition process can initiate. It acts as a specific point of entry from which Jetpack Compose can begin re-executing code in response to state changes or updated parameters. Essentially, each non-inline composable function that returns <code>Unit</code> (the typical return type for UI-emitting composables) is transformed by the Compose compiler to establish such a scope. The compiler achieves this by wrapping the function's body within mechanisms that define this restartable boundary. This transformation includes injecting calls to functions like <code>startRestartGroup</code> and passing additional parameters such as a <code>Composer</code> instance and <code>changed</code> flags, which are instrumental in managing these scopes during runtime.</p>
<p>When a state object, such as a <code>MutableState&lt;T&gt;</code>, that is read <em>within</em> the body of a restartable composable changes its value, the scope associated with that composable is invalidated. This invalidation marks the scope as "dirty" and schedules it for recomposition. Similarly, if a parameter passed <em>into</em> a restartable composable changes from its previous value, this also serves as a trigger for its re-evaluation, assuming the composable is not skipped for other reasons.</p>
<p>Take a look here to understand it better:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747579405067/e5bad293-228d-4d73-a9ab-7938b4c82077.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747707587651/a2e02918-2cf1-40bc-82d2-463677587e9e.png" alt class="image--center mx-auto" /></p>
<p>So, in the above example: <code>MainScreen</code> and <code>ExpandableText</code> hoists a state that adds a capability in them to restart themselves.</p>
<p><code>Content</code> is also a non-inline composable, so it forms its <strong>own restartable scope</strong> but the <code>Content</code> composable will only recompose when recomposition begins at the nearest restartable scope, such as <code>MainScreen</code>, which reads state changes. In short, even if <code>Content</code> has restartable scope (RS_2) still it’s not going to restart ever. Since it’s <strong>skippable</strong> too, it’ll skip recomposition if state is same as it was last time. However, for <code>ExpandableText</code>, recomposition can start from the parent restartable scope, moving from <code>MainScreen</code> → <code>Content</code> → <code>ExpandableText</code> if <code>state.longMessage</code> changes, or it can recompose itself due to reading the <code>expand</code> state.</p>
<p>Also, since <code>Column</code> is a inline-fun in compose, it’ll not have its own scope but it’ll take parent’s scope i.e. <code>Content</code>.</p>
<h2 id="heading-the-role-of-parameter-stability"><strong>The Role of Parameter Stability</strong></h2>
<p>Parameter stability is a contract that informs Compose whether the value of a type can change and, if it does, whether Compose will be notified of that change. Primitive types (like <code>Int</code>, <code>Boolean</code>), <code>String</code>, and function types (lambdas) are inherently considered stable by the Compose compiler. Custom classes, however, need to meet specific criteria (e.g., all public properties are <code>val</code> and of stable types) or be explicitly marked with annotations like <code>@Stable</code> or <code>@Immutable</code> to be treated as stable. Stable inputs are a prerequisite for enabling the skipping optimization.</p>
<p>The interplay between skipping and restartability is nuanced. A composable function might be restartable, meaning it can serve as an independent starting point for recomposition, yet still be skipped if its inputs haven't changed. Conversely, a non-restartable composable might still undergo recomposition if its parent forces it to. A solid understanding of these core mechanisms: recomposition, skipping, restartability, and stability is foundational for effectively utilizing the advanced annotations discussed next. Misinterpreting these fundamentals can lead to the misapplication of these specialized tools.</p>
<h2 id="heading-relationship-between-restartable-and-skippable"><strong>Relationship Between Restartable and Skippable</strong></h2>
<p>A composable function can be restartable but not skippable. This typically occurs if it has one or more unstable parameters. In such a scenario, if its parent composable triggers a recomposition, this restartable-but-not-skippable child will also re-execute its body, regardless of whether its own direct inputs appear to have changed from the perspective of simple equality (because Compose cannot trust the stability of those inputs). For optimal performance, the ideal state for a composable is to be both restartable and skippable. Being restartable allows it to function as an independent unit of recomposition, and being skippable ensures that this unit will only perform work if its inputs have genuinely changed.</p>
<p>The connection here is fundamental: a restartable scope provides the <em>granularity</em> for recomposition, it defines "<strong>what <em>can</em> be redrawn independently</strong>." Skippability, which is heavily influenced by parameter stability, provides the <em>intelligence</em> to decide "<strong>whether it <em>should</em> be redrawn.</strong>" One capability is less effective without the other. A non-restartable (inline) function, for example, cannot be skipped on its own; its parent's skippability determines its fate. Inline composables like <code>Box</code>, <code>Column</code>, <code>Row</code> do not create their own scopes; they are part of their parent's scope. Conversely, a restartable but non-skippable function acts as a well-defined boundary that will always redraw if its parent initiates a recomposition pass that includes it. This underscores the importance of striving for parameter stability in restartable composables to maximize performance benefits.</p>
<p><strong>Summarizing concepts:</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Parameters</strong></td><td><strong>Description</strong></td><td><strong>Role in Recomposition</strong></td><td><strong>How It's Achieved</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Restartable</strong></td><td>A composable that serves as a "scope" or entry point where recomposition can begin.</td><td>Enables Compose to re-execute only a specific part of the UI tree.</td><td>Compiler marks most non-inline, <code>Unit</code>-returning composables as restartable (e.g., via <code>startRestartGroup</code>).</td></tr>
<tr>
<td><strong>Skippable</strong></td><td>A composable whose execution can be skipped during recomposition if its inputs haven't changed.</td><td>Prevents unnecessary work, improving performance.</td><td>All inputs must be stable and unchanged (compared via <code>equals</code>). Compiler marks based on parameter stability. Not applicable to non-<code>Unit</code> returning functions.</td></tr>
</tbody>
</table>
</div><p>This comparative framework helps to clarify how these distinct but related concepts work together to achieve efficient recomposition.</p>
<hr />
<h1 id="heading-annotations">🪧 Annotations</h1>
<h2 id="heading-1-readonlycomposable-reading-without-writing-ui">1. <code>@ReadOnlyComposable</code>: Reading Without Writing UI</h2>
<p>The <code>@ReadOnlyComposable</code> annotation is a marker for <code>@Composable</code> functions that are intended only to read from the current composition context and <em>must not emit any UI nodes</em>. Such functions might access <code>CompositionLocal</code> values (like <code>LocalContext.current</code> or theme attributes) or compute values based on the compositional environment. It establishes a contract with the compiler about the function's read-only nature regarding UI output.</p>
<h3 id="heading-how-it-helps"><strong>How it helps?</strong></h3>
<p>By guaranteeing that no UI nodes are written, <code>@ReadOnlyComposable</code> allows the compiler to perform certain optimizations. When examining the compiled code, functions annotated this way typically do not include the <code>startReplaceableGroup</code> and <code>endReplaceableGroup</code> calls that are standard for composables emitting UI elements. This directly reduces the overhead associated with managing nodes in the slot table for functions that don't contribute to the UI tree. Beyond optimization, it clearly signals the function's purpose: it's a composable designed for data retrieval or computation within the composition, not for UI construction. This promotes a cleaner separation of concerns, allowing data retrieval logic that depends on composition (e.g., current theme, screen density) to exist outside of UI-emitting composables, enhancing reusability and testability.</p>
<h3 id="heading-examples"><strong>Examples:</strong></h3>
<ol>
<li>Accessing <code>MaterialTheme</code> properties:</li>
</ol>
<pre><code class="lang-kotlin">   <span class="hljs-keyword">object</span> MaterialTheme {
       <span class="hljs-keyword">val</span> colors: Colors
           <span class="hljs-meta">@Composable</span>
           <span class="hljs-meta">@ReadOnlyComposable</span>
           <span class="hljs-keyword">get</span>() = LocalColors.current <span class="hljs-comment">// LocalColors.current is a CompositionLocal</span>

       <span class="hljs-keyword">val</span> typography: Typography
           <span class="hljs-meta">@Composable</span>
           <span class="hljs-meta">@ReadOnlyComposable</span>
           <span class="hljs-keyword">get</span>() = LocalTypography.current <span class="hljs-comment">// LocalTypography.current is a CompositionLocal</span>
   }
</code></pre>
<p>The <code>MaterialTheme.colors</code> getter needs to be <code>@Composable</code> to access <code>LocalColors.current</code>. However, it doesn't draw any UI itself; it merely returns the <code>Colors</code> object. The <code>@ReadOnlyComposable</code> annotation informs the compiler of this characteristic.</p>
<ol start="2">
<li>Accessing resources like strings or dimensions:</li>
</ol>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-meta">@ReadOnlyComposable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">localizedGreeting</span><span class="hljs-params">(userName: <span class="hljs-type">String</span>)</span></span>: String {
    <span class="hljs-comment">// stringResource is also @ReadOnlyComposable</span>
    <span class="hljs-keyword">val</span> greetingFormat = stringResource(R.string.greeting_format)
    <span class="hljs-keyword">return</span> String.format(greetingFormat, userName)
}

<span class="hljs-meta">@Composable</span>
<span class="hljs-meta">@ReadOnlyComposable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">screenPadding</span><span class="hljs-params">()</span></span>: Dp {
    <span class="hljs-keyword">return</span> dimensionResource(R.dimen.screen_padding)
}
</code></pre>
<p><em>Explanation:</em> These utility functions leverage the composable context (via <code>stringResource</code> and <code>dimensionResource</code>, which are themselves <code>@ReadOnlyComposable</code>) to fetch values. They don't emit UI but provide data for other composables.</p>
<h3 id="heading-when-to-use-it"><strong>When to use it:</strong></h3>
<ul>
<li><p>For utility functions that need to access <code>CompositionLocal</code>s (e.g., <code>LocalContext.current</code>, <code>LocalDensity.current</code>, <code>LocalLayoutDirection.current</code>) but do not render UI.</p>
</li>
<li><p>For theme property accessors, as demonstrated in the <code>MaterialTheme</code> example.</p>
</li>
<li><p>For any function that computes and returns a value based on compositional information, which will then be used by other UI-emitting composables.</p>
</li>
</ul>
<p><strong>Important Constraint:</strong> A crucial rule is that <code>@ReadOnlyComposable</code> functions can only call other <code>@Composable</code> functions that are also marked as <code>@ReadOnlyComposable</code>. Attempting to invoke a regular UI-emitting composable from within a <code>@ReadOnlyComposable</code> function will lead to a compile-time error. This restriction is vital for maintaining the integrity of the "no UI emission" contract. If a <code>@ReadOnlyComposable</code> could call a UI-emitting composable, the compiler could no longer guarantee that the <code>@ReadOnlyComposable</code> itself doesn't indirectly cause UI to be emitted, thereby invalidating potential optimizations. Furthermore, these functions should not introduce (write) side effects or host <code>State</code> in a way that would trigger the recomposition of other UI elements. Using <code>remember</code> inside a <code>@ReadOnlyComposable</code> function can also be problematic, as <code>remember</code> interacts with the composer to store values in the slot table, which isn't strictly a "read-only" operation from the composer's perspective and can violate the annotation's contract.</p>
<hr />
<h2 id="heading-2-nonrestartablecomposable-controlling-recomposition-boundaries">2. <code>@NonRestartableComposable</code> - Controlling Recomposition Boundaries</h2>
<p>The <code>@NonRestartableComposable</code> annotation is applied to a <code>@Composable</code> function to signal to the Compose compiler that it should not generate the usual machinery that allows this function's execution to be independently restarted or skipped during recomposition. In essence, it tells Compose that this particular composable does not require its own "restart scope". A restart scope is what allows Compose to re-execute a specific part of the UI tree without re-executing its parents if only local state changes.</p>
<p>In simple words: <code>@NonRestartableComposable</code> tells the Compose compiler that a particular composable function <strong>does not need to be independently restartable</strong>. Instead, it will <strong>always recompose if its parent composable recomposes</strong>. This can save the small overhead associated with managing its restartability.</p>
<h3 id="heading-how-it-helps-1">How it helps?</h3>
<p>This annotation can serve as a micro-optimization in specific, limited scenarios. It's potentially beneficial for small, simple composable functions that act as direct wrappers around another single composable, contain very little internal logic, and are themselves unlikely to be invalidated (i.e., recomposed due to their own parameter changes or direct state reads). By marking such a function as non-restartable, the compiler avoids allocating a restart scope for it, which can make the composition process marginally cheaper for these specific cases. If the Compose compiler reports indicate that a function is <code>restartable</code> but not <code>skippable</code> (perhaps due to one or more unstable parameters that are difficult to make stable), marking it with <code>@NonRestartableComposable</code> is one of the two suggested optimization paths. The alternative path is to make the function skippable by ensuring all its parameters are stable.</p>
<h3 id="heading-example"><strong>Example</strong></h3>
<pre><code class="lang-kotlin"><span class="hljs-meta">@NonRestartableComposable</span> <span class="hljs-comment">// Potential candidate if 'content' rarely changes independently</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">SimpleIconWrapper</span><span class="hljs-params">(icon: <span class="hljs-type">ImageVector</span>, modifier: <span class="hljs-type">Modifier</span> = Modifier)</span></span> {
    <span class="hljs-comment">// Very little logic, primarily passes parameters to Icon.</span>
    <span class="hljs-comment">// Assumes 'icon' and 'modifier' are stable and don't change often.</span>
    Icon(imageVector = icon, contentDescription = <span class="hljs-literal">null</span>, modifier = modifier)
}

<span class="hljs-comment">// Contrast with a composable that reads state directly:</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">UserProfileHeader</span><span class="hljs-params">(userState: <span class="hljs-type">State</span>&lt;<span class="hljs-type">User</span>&gt;)</span></span> {
    <span class="hljs-comment">// This composable reads 'userState'. If 'userState.value' changes,</span>
    <span class="hljs-comment">// this composable needs to recompose.</span>
    <span class="hljs-comment">// @NonRestartableComposable would likely be inappropriate here as it's</span>
    <span class="hljs-comment">// expected to be a root of recomposition for its own state changes.</span>
    Text(text = userState.value.name)
    <span class="hljs-comment">//... other user details</span>
}
</code></pre>
<p>Here, <code>SimpleIconWrapper</code> does very little beyond calling the <code>Icon</code> composable. If its parameters (<code>icon</code>, <code>modifier</code>) are stable and seldom change in a way that would require <code>SimpleIconWrapper</code> itself to be the starting point of a recomposition, it <em>might</em> be a candidate. The crucial factor is that it's "unlikely to be invalidated themselves".</p>
<h3 id="heading-when-to-consider-using-it">When to consider using it?</h3>
<ul>
<li><p><strong>Simple and Stateless:</strong> It doesn't use <code>remember</code> to manage its own internal state. It primarily depends on the parameters passed to it. For small, stateless functions that primarily delegate to another single composable, have minimal internal logic, and are unlikely to be the "root" of a recomposition (i.e., they don't directly read state that changes frequently, and their parameters are stable).</p>
</li>
<li><p><strong>Frequently Used (Potentially):</strong> The benefits are more likely to be noticeable (though still often marginal) if the composable is instantiated many times, such as in a long list or a complex UI, where the saved overhead per instance might add up.</p>
</li>
<li><p><strong>Leaf-like or a Thin Wrapper:</strong> It often appears as a leaf node in the UI tree or as a very simple wrapper around another composable, applying a fixed modifier or a simple transformation.</p>
</li>
<li><p>When compiler reports show a function is <code>restartable</code> but not <code>skippable</code>, and making it skippable by stabilizing all parameters is impractical or undesirable, this annotation offers an alternative optimization strategy.</p>
</li>
</ul>
<h3 id="heading-important-considerations-and-caveats"><strong>Important Considerations and Caveats:</strong></h3>
<ul>
<li><p><strong>Micro-optimization:</strong> This is for fine-tuning performance. The actual gains are often very small and may not be noticeable in most applications.</p>
</li>
<li><p><strong>Do profiling first:</strong> Always use profiling tools (like Android Studio's Layout Inspector or recomposition tracking) to identify actual performance bottlenecks before applying such optimizations. Premature optimization can make code harder to read and maintain for negligible benefit.</p>
</li>
<li><p><strong>Risk of Incorrect UI:</strong> If you incorrectly assume a composable doesn't need to be restartable (i.e., there are cases where it <em>should</em> be skipped but now won't be because its parent recomposed), it could lead to unnecessary recompositions or even incorrect UI if the parent recomposes but the child's specific inputs (that should have led to a skip) haven't changed.</p>
</li>
<li><p><strong>Not for Complex Logic:</strong> If a composable has complex logic or could truly benefit from being skipped independently, it should remain restartable.</p>
</li>
</ul>
<h3 id="heading-impact-on-recomposition-scope">Impact on recomposition scope</h3>
<p>If a <code>@NonRestartableComposable</code> function <em>does</em> need to recompose (e.g., because one of its parameters changes), the recomposition will be initiated by its nearest restartable ancestor scope in the composable tree. This could potentially lead to a larger portion of the UI tree recomposing than if the function had its own dedicated restart scope. This trade-off is central to its use: a minor saving in scope allocation versus a potentially wider recomposition if the composable itself changes. This makes its ideal use case quite narrow, typically for stable passthrough wrappers. If the composable has significant internal logic or multiple child composables with their own potential state reads, the overhead of its own restart scope is likely justified to enable more granular recomposition.</p>
<hr />
<h2 id="heading-3-nonskippablecomposable-forcing-re-evaluation">3. <code>@NonSkippableComposable</code> - Forcing Re-evaluation</h2>
<p>The <code>@NonSkippableComposable</code> annotation ensures that a composable function will <em>always</em> be executed (recomposed) whenever its parent composable recomposes, even if all of its own input parameters are stable and have not changed since the last composition. It effectively allows a composable to opt out of Compose's normal skipping mechanism.</p>
<h3 id="heading-how-it-helps-2">How it helps?</h3>
<p>This annotation is useful in particular situations where the default skipping behavior is undesirable.</p>
<ul>
<li><p>It can be used when a composable has important side effects or internal logic that <em>must</em> be re-evaluated on every recomposition cycle of its parent.</p>
</li>
<li><p>It serves as a mechanism to opt out of "strong skipping" mode for a specific composable if there's a need for it to be restartable but explicitly non-skippable. Strong skipping, enabled by default in newer Compose versions, makes more composables skippable; <code>@NonSkippableComposable</code> is the explicit override.</p>
</li>
<li><p>It can also be a tool for debugging, to ensure a specific composable is indeed being called during recomposition cycles as expected.</p>
</li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-meta">@NonSkippableComposable</span>
<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">DebuggableCounterDisplay</span><span class="hljs-params">(count: <span class="hljs-type">Int</span>, label: <span class="hljs-type">String</span>)</span></span> {
    <span class="hljs-comment">// This composable will always execute its body if its parent recomposes,</span>
    <span class="hljs-comment">// regardless of whether 'count' or 'label' has changed.</span>
    Log.d(<span class="hljs-string">"RecompositionLogger"</span>, <span class="hljs-string">"DebuggableCounterDisplay executed with: <span class="hljs-variable">$label</span> - <span class="hljs-variable">$count</span>"</span>)
    Text(<span class="hljs-string">"Label: <span class="hljs-variable">$label</span>, Count: <span class="hljs-variable">$count</span> (I always recompose with my parent!)"</span>)
}

<span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">ControllingParent</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> parentStateTrigger <span class="hljs-keyword">by</span> remember { mutableStateOf(<span class="hljs-number">0</span>) }
    <span class="hljs-keyword">val</span> stableCount = <span class="hljs-number">5</span>
    <span class="hljs-keyword">val</span> stableLabel = <span class="hljs-string">"Current Value"</span>

    Button(onClick = { parentStateTrigger++ }) {
        Text(<span class="hljs-string">"Force Parent Recomposition: <span class="hljs-variable">$parentStateTrigger</span>"</span>)
    }

    <span class="hljs-comment">// Even though 'stableCount' and 'stableLabel' do not change,</span>
    <span class="hljs-comment">// DebuggableCounterDisplay will re-execute (and log) every time</span>
    <span class="hljs-comment">// ControllingParent recomposes due to 'parentStateTrigger' changing.</span>
    DebuggableCounterDisplay(count = stableCount, label = stableLabel)
}
</code></pre>
<p>In this scenario, <code>DebuggableCounterDisplay</code> will print its log message and redraw its <code>Text</code> component every time <code>ControllingParent</code> undergoes recomposition. This happens irrespective of whether the <code>stableCount</code> or <code>stableLabel</code> values have actually changed, due to the <code>@NonSkippableComposable</code> annotation. This represents a deliberate choice to prioritize execution over optimization for this specific composable.</p>
<h3 id="heading-when-to-use-it-cautiously"><strong>When to use it (Cautiously):</strong></h3>
<ul>
<li><p>For composables that contain critical side effects that absolutely must run on each parent recomposition. However, it's important to critically evaluate if these side effects are better managed by dedicated side-effect handlers like <code>LaunchedEffect</code>, <code>DisposableEffect</code>, or <code>SideEffect</code>. These handlers offer more granular control over lifecycle and cancellation, and forcing a whole composable to re-run just for a side effect might be less clean.</p>
</li>
<li><p>For debugging purposes, to confirm that a particular composable is being invoked during recomposition cycles.</p>
</li>
<li><p>When strong skipping mode is enabled (default in Kotlin 2.0.20+), and there's a specific need for a restartable composable to <em>not</em> be skippable.</p>
</li>
</ul>
<p><strong>Performance Implication:</strong> This annotation deliberately bypasses a core Compose optimization (skipping). Consequently, its overuse can lead to performance degradation, as composables will perform more work than might be strictly necessary. While skippable composables might involve more generated code for the skipping logic, non-skippable ones incur the runtime cost of re-execution.</p>
<hr />
<p><code>ReadOnlyComposable</code> is pretty straightforward to understand but <code>@NonRestartableComposable</code> &amp; <code>@NonSkippableComposable</code> are bif different and may sound confusing. So let’s understand the difference better.</p>
<h2 id="heading-key-differences-in-nonrestartablecomposable-amp-nonskippablecomposable">Key Differences in <code>NonRestartableComposable</code> &amp; <code>NonSkippableComposable</code></h2>
<p>While both <code>@NonRestartableComposable</code> and <code>@NonSkippableComposable</code> influence recomposition behavior and are listed as conditions that can make a composable ineligible for standard skipping , they operate on distinct aspects of the process. Confusion between them can arise because both represent deviations from "normal" composable behavior. However, the <em>reason</em> they deviate from the default skipping path is different.</p>
<p><code>@NonRestartableComposable</code> primarily affects whether the composable function establishes its <em>own restart scope</em>. A restart scope allows Compose to re-execute just that composable and its children if its direct inputs or read state change. <code>@NonSkippableComposable</code>, on the other hand, directly dictates whether the composable's execution can be <em>skipped</em> if its inputs remain unchanged when its parent recomposes.</p>
<p>Crucially, <code>@NonRestartableComposable</code> does <strong>not</strong> imply <code>@NonSkippableComposable</code>. A composable function can be non-restartable (meaning it relies on its parent's restart scope if its own parameters change) but still be skippable if its own parameters are stable and haven't changed when that parent scope initiates a recomposition. Conversely, a composable can be restartable (possessing its own scope) yet be marked with <code>@NonSkippableComposable</code> to ensure it always re-executes with its parent.</p>
<p>The following table provides a side-by-side comparison to clarify their distinct characteristics:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Annotations</strong></td><td><code>@NonRestartableComposable</code></td><td><code>@NonSkippableComposable</code></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Primary Effect</strong></td><td>Prevents the composable from having its own independent restart scope.</td><td>Prevents the composable's execution from being skipped, even if inputs are unchanged.</td></tr>
<tr>
<td><strong>If Inputs Change</strong></td><td>The nearest restartable parent scope initiates recomposition that includes this composable.</td><td>The composable always re-executes if its parent recomposes.</td></tr>
<tr>
<td><strong>If Inputs Don't Change (and parent recomposes)</strong></td><td>Can still be skipped if its parameters are stable and unchanged.</td><td>Always re-executes.</td></tr>
<tr>
<td><strong>Goal</strong></td><td>Micro-optimization for simple wrappers by avoiding restart scope allocation.</td><td>Forcing execution for side-effects, debugging, or explicitly opting out of strong skipping for a restartable composable.</td></tr>
<tr>
<td><strong>Interaction with Strong Skipping</strong></td><td>Remains unskippable (as strong skipping applies to <em>restartable</em> composables).</td><td>Explicitly makes a <em>restartable</em> composable non-skippable, overriding strong skipping's default.</td></tr>
<tr>
<td><strong>Typical Use Case</strong></td><td>Small, stateless wrapper functions with stable inputs.</td><td>Debugging; specific side-effects (use with caution); opting out of strong skipping.</td></tr>
<tr>
<td><strong>Performance Implication</strong></td><td>Saves minor overhead of a restart scope; potential for wider recomposition if it changes.</td><td>Deliberately incurs the cost of re-execution; bypasses a core optimization.</td></tr>
</tbody>
</table>
</div><p>Choosing between these annotations requires a clear understanding of <em>why</em> the default behavior needs to be altered. Is the goal to save the minor overhead of a scope for a trivial, stable function (<code>@NonRestartableComposable</code>), or is it to ensure a function always runs regardless of its inputs (<code>@NonSkippableComposable</code>)? Misapplying one for the other's intended purpose could lead to negligible benefits or, worse, unintended performance issues.</p>
<h2 id="heading-interaction-with-strong-skipping-mode">Interaction with Strong Skipping Mode</h2>
<p>Strong Skipping Mode, which is enabled by default in Kotlin 2.0.20 and later versions of the Compose compiler , significantly alters the default skippability behavior of composables. This mode represents a notable shift in Compose's optimization strategy, aiming to make more composables skippable out-of-the-box and thereby reducing the developer's burden to manually ensure parameter stability just for skipping purposes.</p>
<p>The key changes introduced by Strong Skipping Mode are :</p>
<ol>
<li><p><strong>Composables with unstable parameters become skippable:</strong> Under strong skipping, even if a composable function receives parameters of types that the compiler cannot infer as stable, the function can still be skipped. The comparison for these unstable parameters is done using instance equality (<code>===</code>).</p>
</li>
<li><p><strong>Lambdas with unstable captures are remembered/memoized:</strong> The compiler automatically wraps lambda expressions, even those capturing unstable variables, in a <code>remember</code> call, using appropriate keys based on the stability of captures. Essentially, with strong skipping, <em>all restartable composable functions become skippable by default</em>.</p>
</li>
</ol>
<h3 id="heading-nonskippablecomposable-as-an-opt-out"><code>@NonSkippableComposable</code> as an Opt-Out</h3>
<p>In a strong skipping environment, the <code>@NonSkippableComposable</code> annotation becomes particularly important. If there is a <em>restartable</em> composable that should <em>not</em> be skipped (despite strong skipping's tendency to make it skippable), <code>@NonSkippableComposable</code> is the explicit way to enforce its re-execution. This gives developers precise control to override the aggressive default skipping behavior when necessary. Without it, developers would have limited recourse if the strong skipping heuristic was unsuitable for a specific restartable composable that requires guaranteed execution.</p>
<h3 id="heading-nonrestartablecomposable-and-strong-skipping"><code>@NonRestartableComposable</code> and Strong Skipping</h3>
<p>Functions annotated with <code>@NonRestartableComposable</code> remain unskippable even when strong skipping is enabled. The rationale is that strong skipping primarily modifies the conditions under which <em>restartable</em> functions can be skipped. Since a <code>@NonRestartableComposable</code> function, by definition, lacks its own independent restart scope, the rules of strong skipping do not fundamentally alter its non-skippable nature in this context. Its non-skippability is inherently tied to its lack of a restart scope, not just parameter stability considerations.</p>
<hr />
<h1 id="heading-conclusion">Conclusion</h1>
<p>The annotations <code>@NonRestartableComposable</code>, <code>@ReadOnlyComposable</code>, and <code>@NonSkippableComposable</code> are potent instruments in the Jetpack Compose developer's toolkit. They offer fine-grained control over the Compose compiler and runtime, enabling optimizations and enforcing behavioral contracts that go beyond the default capabilities. Still, these annotations can often seem confusing, so it's <strong><em>highly recommended to use them correctly by profiling and assessing performance</em></strong> thoroughly before implementing them and only use these annotations with proper understanding. This approach can boost confidence in your implementation. <strong>Use tools like the Layout Inspector in Android Studio (to check recomposition counts), Jetpack Macrobenchmark for broader performance testing, and Compose compiler reports (for analyzing stability and skippability)</strong>.</p>
<p>I hope you got the idea about how exactly these annotations works in Jetpack Compose.</p>
<p>Awesome. I hope you've gained some valuable insights from this. If you enjoyed this write-up, please share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[Understanding Dispatchers: Main and Main.immediate]]></title><description><![CDATA[Hi Androiders 👋, there's no doubt that Kotlin coroutines have become the standard in the Android world for multithreading and reactive programming. Coroutines are easy to use, but there's always something that feels a bit complicated. I often get qu...]]></description><link>https://blog.shreyaspatil.dev/understanding-dispatchers-main-and-mainimmediate</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/understanding-dispatchers-main-and-mainimmediate</guid><category><![CDATA[main-thread]]></category><category><![CDATA[Android]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[coroutines]]></category><category><![CDATA[coroutines-flow]]></category><category><![CDATA[multithreading]]></category><category><![CDATA[kotlin coroutines]]></category><category><![CDATA[Kotlin Multiplatform]]></category><category><![CDATA[kotlin beginner]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android Studio]]></category><category><![CDATA[Android]]></category><category><![CDATA[android apps]]></category><category><![CDATA[Threads]]></category><category><![CDATA[ThreadPools]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Wed, 02 Apr 2025 12:22:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1743357697684/071ab1db-b181-4a1b-803c-4429ad8211e3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Androiders 👋, there's no doubt that Kotlin coroutines have become the standard in the Android world for multithreading and reactive programming. Coroutines are easy to use, but there's always something that feels a bit complicated. I often get questions about the exact difference between <code>Dispatchers.Main</code> and <code>Dispatchers.Main.immediate</code>. In this blog, we'll explore this in detail with an example.</p>
<hr />
<h1 id="heading-basics">Basics</h1>
<p>Let’s go over some basics. Coroutines are supported by a thread pool. On the JVM, they use the <code>java.util.concurrent.Executor</code> API to manage execution for Dispatchers like Default and IO. On Android, they use the <code>Handler</code> APIs for Main dispatcher. For multi-platform, they use <code>Promise</code> APIs in JavaScript, and for native platforms like Apple, they use <code>DispatchQueue</code> under the hood. To understand this concept, <em>I’ve wrote</em> <a target="_blank" href="https://blog.shreyaspatil.dev/sleepless-concurrency-delay-vs-threadsleep"><em>another blog</em></a> <em>that goes through the concept of coroutines from platform perspective and explains how delay works in coroutine</em>. But for now let’s just talk about Android.</p>
<p>The <code>CoroutineDispatcher</code> handles dispatching tasks in coroutines, and each dispatcher must override the <code>dispatch()</code> method to execute tasks. <a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/1.10.1/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt#L110">HandlerContext</a> is the implementation of Dispatchers.Main for Android platform. Here’s how its <code>dispatch()</code> implementation looks like:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">dispatch</span><span class="hljs-params">(context: <span class="hljs-type">CoroutineContext</span>, block: <span class="hljs-type">Runnable</span>)</span></span> {
    <span class="hljs-keyword">if</span> (!handler.post(block)) {...} 
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>In short, each time whenever we say <code>coroutineScope.launch(Dispatchers.Main) { doSomething() }</code> or <code>withContext(Dispatchers.Main) { doSomething() }</code> under the hood it works as <code>handler.post { doSomething() }</code>. It justs executes that <code>Runnable</code> with Handler API.</p>
<p>Now let’s take a look at <code>HandlerContext</code> implementation from <strong><em>Dispatchers.Main.immediate</em></strong> perspective:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HandlerContext</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constructor</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> handler: Handler,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> name: String?,
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> invokeImmediately: <span class="hljs-built_in">Boolean</span> = <span class="hljs-literal">false</span>
) : HandlerDispatcher(), Delay {

    <span class="hljs-keyword">override</span> <span class="hljs-keyword">val</span> immediate: HandlerContext = <span class="hljs-keyword">if</span> (invokeImmediately) <span class="hljs-keyword">this</span> <span class="hljs-keyword">else</span>
        HandlerContext(handler, name, <span class="hljs-literal">true</span>)

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">isDispatchNeeded</span><span class="hljs-params">(context: <span class="hljs-type">CoroutineContext</span>)</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> !invokeImmediately || Looper.myLooper() != handler.looper
    }

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">dispatch</span><span class="hljs-params">(context: <span class="hljs-type">CoroutineContext</span>, block: <span class="hljs-type">Runnable</span>)</span></span> {
        <span class="hljs-keyword">if</span> (!handler.post(block)) {
            cancelOnRejection(context, block)
        }
    }
}
</code></pre>
<p>When <code>Dispatchers.Main.immediate</code> is accessed it creates the HandlerContext instance with same Handler instance but with last parameter <code>invokeImmediately</code> as <code>true</code>. Otherwise for <code>Dispatcher.Main</code> its value remains false.</p>
<p>Now notice there’s one more method: <code>isDispatcherNeeded</code>. Official docs say:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">📄</div>
<div data-node-type="callout-text">Returns <code>true</code> if the execution of the coroutine should be performed with <code>dispatch()</code> method. If this method returns <code>false</code>, the coroutine is resumed immediately in the current thread, potentially forming an event-loop to prevent stack overflows.</div>
</div>

<p>So if <code>isDispatchNeeded</code> returns true then it submits the tasks to the threadpool/executor/handler otherwise synchronously on the current thread.</p>
<p>In <code>HandlerContext</code>, <code>isDispatchNeeded()</code> returns true if currently flag <code>invokeImmediately</code> is false and current looper is not as same as Handler’s (Main thread’s) looper. It means whenever these conditions are met, a task will be dispatched and <code>dispatch()</code> method will be invoked and ultimately for main thread it’ll execute task by posting the task to Handler (<code>handler.post{}</code>) by adding it to main thread’s event queue. It means, if currently task is being executed on the main thread and if flag <code>invokeImmediately</code> is <strong>true</strong> then this method will return <strong>false</strong>. In such case, dispatch won’t be performed and it’ll immediately executed on the same thread synchronously.</p>
<details><summary>Do you know that Dispatchers.Unconfined also works in similar way?</summary><div data-type="detailsContent"><code>Dispatchers.Unconfined</code> <em>also</em> uses the <code>isDispatchNeeded</code> check, but its implementation <em>always</em> returns <code>false</code>, leading to immediate execution in the current thread. However, unlike <code>Main.immediate</code>, if it suspends and resumes, it continues in the thread the suspending function used, which can lead to unpredictable thread switching.</div></details>

<p>In Android-Kotlin extensions, <code>lifecycleScope</code>, <code>viewModelScope</code> uses <code>Dispatchers.Main.immediate</code> as a dispatcher context.</p>
<hr />
<h1 id="heading-understanding-with-example">Understanding with example</h1>
<p>Now let’s understand it better with an example. In this example, let’s understand it with a simple UI implementation with ViewModel based UI state emission. UI will subscribe the state provided by the ViewModel i.e. UI will be <strong><em>consumer</em></strong> and ViewModel will perform operation and produce the state i.e. <strong><em>producer</em></strong>.</p>
<p>So let’s create combinations of producer and consumer. Let’s try using <code>Dispatchers.Main</code> and <code>Dispatchers.Main.immediate</code> for producer and consumer combinations.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Example</strong></td><td><code>PRODUCER_DISPATCHER</code></td><td><code>CONSUMER_DISPATCHER</code></td></tr>
</thead>
<tbody>
<tr>
<td>1.</td><td>🟢 Dispatchers.Main.immediate</td><td>🟢 Dispatchers.Main.immediate</td></tr>
<tr>
<td>2.</td><td>🟢 Dispatchers.Main.immediate</td><td>🟠 Dispatchers.Main</td></tr>
<tr>
<td>3</td><td>🟠 Dispatchers.Main</td><td>🟢 Dispatchers.Main.immediate</td></tr>
<tr>
<td>4</td><td>🟠 Dispatchers.Main</td><td>🟠 Dispatchers.Main</td></tr>
</tbody>
</table>
</div><p>Simple code would look like:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainViewModel</span> : <span class="hljs-type">ViewModel</span></span>() {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> _state = MutableStateFlow&lt;String?&gt;(<span class="hljs-literal">null</span>)
    <span class="hljs-keyword">val</span> state = _state.asStateFlow().filterNotNull()

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">process</span><span class="hljs-params">(input: <span class="hljs-type">String</span>)</span></span> {
        viewModelScope.launch(PRODUCER_DISPATCHER) {
            _state.value = input
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MainActivity</span> : <span class="hljs-type">ComponentActivity</span></span>() {

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> viewModel <span class="hljs-keyword">by</span> viewModels&lt;MainViewModel&gt;()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> items = mutableStateListOf&lt;String&gt;()

    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">onCreate</span><span class="hljs-params">(savedInstanceState: <span class="hljs-type">Bundle</span>?)</span></span> {
        <span class="hljs-comment">/// ...</span>
        setContent {
            Screen(
                items = items.toList(),
                onProcess = ::executeProcess,
                onClear = { items.clear() }
            )
        }
        observeState()
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">observeState</span><span class="hljs-params">()</span></span> {
        lifecycleScope.launch(CONSUMER_DISPATCHER) {
            viewModel.state.collect { state -&gt;
                items.add(state)
                Log.d(<span class="hljs-string">"StateStacktrace"</span>, Throwable().stackTraceToString())
            }
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">executeProcess</span><span class="hljs-params">()</span></span> {
        (<span class="hljs-number">1</span>..<span class="hljs-number">5</span>).forEach {
            viewModel.process(it.toString())
        }
    }
}
</code></pre>
<p>Understanding the snippet:</p>
<ul>
<li><p><code>MainViewModel</code>: This ViewModel hoists a <code>state</code> for UI consumption. It has a method <code>process()</code> which upon execution launched task on <code>viewModelScope</code> with <strong>PRODUCER_DISPATCHER</strong> as discussed above and it just sets value coming from UI to the <code>_state</code>.</p>
</li>
<li><p><code>MainActivity</code>: Simple activity that renders Composable <code>Screen</code></p>
<ul>
<li><p><code>observeState()</code> method observes a state coming from ViewModel and subscribed to the flow on <strong>CONSUMER_DISPATCHER</strong>. Notice that we also have logged something there with current stacktrace details. We’ll use it later to understand the sequence of execution.</p>
</li>
<li><p>On clicking the "Process" button on the UI, <code>executeProcess()</code> is called. This function calls <code>viewModel.process()</code> five times, passing numbers from 1 to 5.</p>
</li>
</ul>
</li>
</ul>
<p>Here’s how UI looks like (Compose preview):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743524110143/17e5d149-bc3e-4064-836a-cda82eaefa08.png" alt class="image--center mx-auto" /></p>
<p><em>In the UI, these three circle with numbered texts are displayed from list of</em> <code>items</code> <em>assuming currently</em> <code>items</code> <em>contents are [“1”, “2“, “3“].</em></p>
<p>Now let’s try the app with our examples having four combination of dispatchers as discussed above.</p>
<h2 id="heading-example-1-produce-and-consume-on-immediate">Example 1: Produce and Consume on 🟢 <code>immediate</code></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743524486593/ba07c691-7ebf-418f-bd24-944e1898af2d.gif" alt class="image--center mx-auto" /></p>
<p>Each time “Process” is clicked, the 5 new text items are appearing on the UI. Now, if you remember, while collecting a flow of state, we logged the stack trace each time we received a new state. Let's review it.</p>
<pre><code class="lang-diff">java.lang.Throwable
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:95)</span>
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:93)</span>
     at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:396)
     at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:15)
<span class="hljs-deletion">-    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)</span>
<span class="hljs-deletion">-    .....</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)</span>
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainViewModel.process(MainActivity.kt:61)</span>
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity.executeProcess(MainActivity.kt:102)</span>
     at dev.shreyaspatil.maindispatcherexample.MainActivity.access$executeProcess(MainActivity.kt:67)
     at dev.shreyaspatil.maindispatcherexample.MainActivity$onCreate$1$1$1$1$1.invoke(MainActivity.kt:82)
     at dev.shreyaspatil.maindispatcherexample.MainActivity$onCreate$1$1$1$1$1.invoke(MainActivity.kt:82)
<span class="hljs-deletion">-    at androidx.compose.foundation.ClickableNode$clickPointerInput$3.invoke-k-4lQ0M(Clickable.kt:639)</span>
<span class="hljs-deletion">-    .....</span>
<span class="hljs-addition">!    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3144)</span>
<span class="hljs-deletion">-    .....</span>
<span class="hljs-addition">!    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)</span>
</code></pre>
<p>The stack trace at the bottom starts with a click event being dispatched from a View. It then propagates a click event from a Compose API, which invokes <code>MainActivity#executeProcess</code>. This calls <code>MainViewModel#process</code>, and from there, it directly propagates through coroutines, then StateFlow APIs, and finally into <code>MainActivity#observeState$emit</code> (collector). <strong><em>This means, from a click event till flow collector, all the operations happened synchronously.</em></strong> Let’s visualize the process:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743356490553/1f7c6367-54bd-4103-b552-e69b4c9652e2.png" alt class="image--center mx-auto" /></p>
<p>Since both <code>PRODUCER_DISPATCHER</code> and <code>CONSUMER_DISPATCHER</code> were using <strong>Dispatchers.Main.immediate</strong> and all operations were already running on the main thread, no separate dispatch operation occurred. Task executions were not managed by <code>Handler#post()</code>, even when the state was being observed in a coroutine or the ViewModel was launching a coroutine.</p>
<h2 id="heading-example-2-produce-on-immediate-consume-on-main">Example 2: Produce on <code>immediate</code>, consume on <code>Main</code></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743525147684/14cf88a8-5592-4591-9b21-6a73215deff3.gif" alt class="image--center mx-auto" /></p>
<p>Strange, no? On clicking “Process”, only last item i.e. “5” is rendered and then even after clearing UI’s state (not ViewModel’s) the data is never processed again! Let’s take a look at this combination’s stacktrace.</p>
<pre><code class="lang-diff">java.lang.Throwable
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:95)</span>
     at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:93)
     at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:396)
     at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:15)
     at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
     at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
     at android.os.Handler.handleCallback(Handler.java:995)
     at android.os.Handler.dispatchMessage(Handler.java:103)
     at android.os.Looper.loopOnce(Looper.java:239)
     at android.os.Looper.loop(Looper.java:328)
     at android.app.ActivityThread.main(ActivityThread.java:8952)
     at java.lang.reflect.Method.invoke(Native Method)
     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
<span class="hljs-addition">!    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)</span>
</code></pre>
<p>Nothing here. At very bottom we can see task has been newly executed on the Main thread and it is directly resuming on the <code>StateFlow</code>'s collector. Since this log is only added on the consumer side (flow collection), and this flow is collected on the <code>Main</code> dispatcher this time, not immediately, it means that each time a new value is produced in the flow, the collector will add the block to the main thread’s event queue (causing it to dispatch a call to <code>Handler.post{}</code>).</p>
<p>In short, when a click event is dispatched from the View APIs, the <code>MainViewModel#process</code> is called synchronously on the main thread, and flow collection occurs through dispatching.</p>
<p>Let’s visualize how this combination is working:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743356558240/5285d95d-7ff6-4e11-a584-f928a5383b6c.png" alt class="image--center mx-auto" /></p>
<p>Okay, <strong>why only 1 item is getting collected on the UI side?</strong></p>
<p>Since we are using StateFlow here, StateFlow API has conflation behaviour. In the context of Flows (<em>and data streams in general</em>), conflation means that if a producer emits new values faster than a collector can process them, <strong><em>the intermediate, unprocessed values are effectively dropped</em></strong>. The collector is <strong><em>guaranteed to get the most recent value</em></strong>, but it might miss some values that were emitted while it was busy processing a previous one.</p>
<p><strong>StateFlow's Conflation Behavior:</strong></p>
<p><code>StateFlow</code> is designed specifically as a <strong>state holder</strong>. It always holds a single, current <code>value</code>. Its conflation behavior is <strong>inherent and fundamental</strong> to its design:</p>
<ol>
<li><p><strong>Value Updates:</strong> When you update the <code>value</code> of a <code>StateFlow</code> (either by assigning to <code>mutableStateFlow.value</code> or using <code>tryEmit</code> or <code>emit</code>), the new value <em>immediately replaces</em> the previously held value. <code>StateFlow</code> doesn't maintain a buffer or queue of past values beyond the <em>current</em> one.</p>
</li>
<li><p><strong>Collector Perspective:</strong> When a collector is attached to a <code>StateFlow</code> (using <code>.collect { ... }</code>), it first receives the <em>current</em> value. Then, whenever a <em>new</em> value is emitted <em>after</em> the collector has finished processing the previous one, the collector receives that new value.</p>
</li>
<li><p><strong>The Conflation:</strong> If multiple values are emitted to the <code>StateFlow</code> <em>while</em> a collector is still busy processing a previously received value, that collector will <strong>only receive the very last value</strong> that was emitted before it became ready again. All the intermediate values emitted during its processing time are missed by that specific collector – they are conflated.</p>
</li>
</ol>
<p>In our case, each time a value is produced by the ViewModel, it is collected from the UI side on the Main dispatcher. Before the dispatch can occur (<em>main thread’s event queue polling</em>) and execute the flow collection on the consumer's side (<code>Handler.post{}</code>), the ViewModel finishes processing all the items submitted to it from "1" to "5." This happens because it runs synchronously on the same thread, and no separate dispatch occurs while setting the state of the ViewModel. <strong>Consumer doesn’t gets chance to collect all the items and before that ViewModel finishes setting the last state as “5”</strong>, so UI just gets that. Even if the UI state is cleared and if "Process" is clicked again, the same thing happens. Since the previous state was "5" and the new state is still "5" after processing and this time as well consumer doesn’t gets chance to listen to all the updates and last state is “5” again, the emission is skipped because <code>StateFlow</code> only emits values if they are distinct.</p>
<h2 id="heading-example-3-produce-on-main-consume-on-immediate">Example 3: Produce on <code>Main</code>, Consume on <code>immediate</code></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743526352970/216f80cd-a3c4-40aa-98c3-5d05bb33f013.gif" alt class="image--center mx-auto" /></p>
<p>The behaviour seems similar as first example’s but difference is visible if we take a look on the stacktrace:</p>
<pre><code class="lang-diff">java.lang.Throwable
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:95)</span>
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:93)</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:396)</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:15)</span>
<span class="hljs-deletion">-    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)</span>
<span class="hljs-deletion">-    .....</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:349)</span>
<span class="hljs-deletion">-    at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:316)</span>
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainViewModel$process$1.invokeSuspend(MainActivity.kt:62)</span>
     at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
     at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
     at android.os.Handler.handleCallback(Handler.java:995)
     at android.os.Handler.dispatchMessage(Handler.java:103)
     at android.os.Looper.loopOnce(Looper.java:239)
     at android.os.Looper.loop(Looper.java:328)
     at android.app.ActivityThread.main(ActivityThread.java:8952)
     at java.lang.reflect.Method.invoke(Native Method)
     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
<span class="hljs-addition">!    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)</span>
</code></pre>
<p>The consumer side's stack trace begins with the <code>MainViewModel#process</code> logic, where the StateFlow's value is set, and the flow collector is called right away. On the producer side, each item from "1" to "5" is processed by dispatching a separate coroutine. This means a separate dispatch occurs five times, and the StateFlow's value is set each time. However, since the collection is immediate and doesn't require a separate dispatch for the collector (consumer), all values are directly sent to the consumer without missing any. Visualizing this would help understanding it better:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743356604863/7202f2cd-9973-4159-9572-c707097d0ec7.png" alt class="image--center mx-auto" /></p>
<p>So even if the behavior of Example 1 and Example 3 looks the same, it is technically different. In Example 1, there were no dispatches on the Handler, whereas in this example, there are 5 dispatches from the producer side.</p>
<h2 id="heading-example-4-produce-on-main-consume-on-main">Example 4: Produce on <code>Main</code>, Consume on <code>Main</code></h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743526942561/4e5e6330-1725-40d4-8d0d-83101099eea3.gif" alt class="image--center mx-auto" /></p>
<p>The behaviour is same as Example 2’s and stacktrace would be same as well since in Example 2 as well, consumer uses <code>Dispatchers.Main</code>:</p>
<pre><code class="lang-diff">java.lang.Throwable
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:95)</span>
<span class="hljs-addition">!    at dev.shreyaspatil.maindispatcherexample.MainActivity$observeState$1$1.emit(MainActivity.kt:93)</span>
     at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:396)
     at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:15)
     at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
     at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
     at android.os.Handler.handleCallback(Handler.java:995)
     at android.os.Handler.dispatchMessage(Handler.java:103)
     at android.os.Looper.loopOnce(Looper.java:239)
     at android.os.Looper.loop(Looper.java:328)
     at android.app.ActivityThread.main(ActivityThread.java:8952)
     at java.lang.reflect.Method.invoke(Native Method)
     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:593)
<span class="hljs-addition">!    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)</span>
</code></pre>
<p>In this example, the producer (ViewModel) dispatches tasks on the Main thread’s event queue, and the consumer (UI) also subscribes to the flow, which needs to be dispatched on the Main thread’s event queue. Let's visualize this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1743356657292/d386b251-67c7-4a9b-901b-25543ee4f4a4.png" alt class="image--center mx-auto" /></p>
<p>Whenever <code>Handler.post{}</code> is called, it internally keeps a queue of <code>Runnable</code>s that are executed in order. In this example, both the producer and consumer need dispatching, which means the queue will first add 5 producer items, followed by the consumer’s dispatch for collection of item. According to the queue’s order, all the producer’s tasks will be processed from item “1” to “5.” By the time the consumer’s turn comes, the last value is already set to 5, so the consumer never gets a chance to collect the values from “1” to “4.”</p>
<p>Cool, that’s all about combinations of examples and here’s summary:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Example</strong></td><td><code>PRODUCER_DISPATCHER</code></td><td><code>CONSUMER_DISPATCHER</code></td><td><strong>Behaviour</strong></td><td><strong>Collection at consumer side</strong></td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>🟢 immediate</td><td>🟢 immediate</td><td>Fully synchronous on Main thread. No event queing on main thread.</td><td>All items (1-5)</td></tr>
<tr>
<td>2</td><td>🟢 immediate</td><td>🟠 Main</td><td>Producer sync, Consumer dispatches; StateFlow Conflation.</td><td>Last item (5)</td></tr>
<tr>
<td>3</td><td>🟠 Main</td><td>🟢 immediate</td><td>Producer dispatches; Consumer sync.</td><td>All items (1-5)</td></tr>
<tr>
<td>4</td><td>🟠 Main</td><td>🟠 Main</td><td>Both dispatch; Producer queue finishes before Consumer; StateFlow Conflation</td><td>Last item (5)</td></tr>
</tbody>
</table>
</div><hr />
<h1 id="heading-resuming-from-non-main-dispatcher-to-dispatchersmainimmediate">Resuming from non-main dispatcher to <code>Dispatchers.Main.immediate</code></h1>
<p>By now, you might understand how behavior changes when using combinations of <code>Dispatchers.Main</code> and <code>Dispatchers.Main.immediate</code>. As we learned earlier, the <code>isDispatchNeeded()</code> implementation causes this behavior. For the Main dispatcher, we learned that <code>Dispatchers.Main.immediate</code> doesn't dispatch a task if it's already running on the Main thread's looper. But what happens if the <code>immediate</code> dispatcher is used and it's being resumed from another dispatcher? Let’s see this small example:</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">example</span><span class="hljs-params">()</span></span> {
    lifecycleScope.launch(Dispatchers.Main.immediate) {
        setLoading(<span class="hljs-literal">true</span>)
        <span class="hljs-keyword">val</span> profileData = withContext(Dispatchers.IO) { repository.fetchProfile() }
        showProfile(profileData)
        setLoading(<span class="hljs-literal">false</span>)
    }
}
</code></pre>
<p>In this example, whenever <code>example()</code> is called, <code>setLoading(true)</code> runs immediately because it's launched with the <code>immediate</code> dispatcher. On the next line, <code>fetchProfile()</code> is called by switching to the IO dispatcher. Once fetching is complete and <code>profileData</code> is loaded, the context switches back to the main thread. This time, <code>isDispatchNeeded()</code> will return <strong>true</strong> because the current looper is not the same as the Main thread's looper, as the IO thread was just used. Therefore, dispatching occurs to the main thread, and then <code>showProfile()</code> and the following methods are called after dispatching to the main thread.</p>
<p>So, behind the scenes, the above snippet can be visualized like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">example</span><span class="hljs-params">()</span></span> {
    setLoading(<span class="hljs-literal">true</span>) <span class="hljs-comment">// Not dispatching to handler since it's on immediate</span>

    ioExecutor.execute {
        <span class="hljs-keyword">val</span> profileData = repository.fetchProfile()

        <span class="hljs-comment">// Currently it's on IO dispatcher. So will need to dispatch call to main thread.</span>
        handler.post {
            showProfile(profileData)
            setLoading(<span class="hljs-literal">false</span>)
        }
    }
}
</code></pre>
<blockquote>
<p>That's the beauty of coroutines. It just skips the writing of callback hell for developers and manages it well internally in such a way that we could synchronously write asynchronous code!</p>
</blockquote>
<hr />
<h1 id="heading-why-choose-immediate">Why Choose <code>immediate</code>?</h1>
<p>We've dug deep into <em>how</em> <code>Dispatchers.Main</code> and <code>Dispatchers.Main.immediate</code> work differently, especially regarding the <code>isDispatchNeeded</code> check and interaction with the <code>Handler</code>. This might leave you wondering: <em>why</em> would you explicitly choose <code>immediate</code>? And why did the AndroidX team decide to make <code>Dispatchers.Main.immediate</code> the default dispatcher for key extension scopes like <code>lifecycleScope</code> and <code>viewModelScope</code>?</p>
<p>The core reasons boil down to <strong>performance optimization</strong> and <strong>immediacy</strong>, particularly targeting the common case where coroutine work starts on the main thread and needs to interact with the UI promptly.</p>
<ol>
<li><p><strong>Avoiding Unnecessary Overhead:</strong></p>
<ul>
<li><p>As we saw, <code>Dispatchers.Main</code> <em>always</em> posts the coroutine's execution block (as a <code>Runnable</code>) to the main thread's <code>Handler</code> queue.</p>
</li>
<li><p>If your code launching or resuming the coroutine is <em>already</em> on the main thread, this <code>post</code> operation, while generally fast, still represents a small amount of overhead: creating the <code>Runnable</code>, enqueueing it, and waiting for the <code>Looper</code> to process it in a subsequent pass.</p>
</li>
<li><p><code>Dispatchers.Main.immediate</code>, thanks to its <code>isDispatchNeeded</code> check returning <code>false</code> when already on the main looper, skips this <a target="_blank" href="http://handler.post"><code>handler.post</code></a><code>()</code> step entirely in that specific scenario. It executes the code <em>directly</em> and synchronously within the current execution flow. This micro-optimization avoids the queueing delay and object allocation associated with the post.</p>
</li>
</ul>
</li>
<li><p><strong>Enhanced Responsiveness:</strong></p>
<ul>
<li><p>Beyond raw performance, <code>immediate</code> provides, well, <em>immediacy</em>. Imagine a button click handler (running on the main thread) launching a coroutine using <code>viewModelScope</code> to update some state and maybe show a loading indicator immediately.</p>
</li>
<li><p>With <code>Dispatchers.Main.immediate</code>, that initial <code>setLoading(true)</code> call inside the coroutine runs <em>right now</em>, within the same event cycle as the button click handler.</p>
</li>
<li><p>With <code>Dispatchers.Main</code>, that <code>setLoading(true)</code> would be posted to the Handler and run slightly later, after the current block of main thread work finishes and the Looper processes the queued item. While often imperceptible, using <code>immediate</code> guarantees the operation happens synchronously when possible, which can contribute to a snappier feel for UI updates initiated from the main thread.</p>
</li>
</ul>
</li>
</ol>
<p><strong>The AndroidX Choice:</strong></p>
<p>The designers of the AndroidX libraries likely chose <code>Dispatchers.Main.immediate</code> as the default for scopes like <code>lifecycleScope</code> and <code>viewModelScope</code> precisely because these scopes are heavily used for tasks tightly coupled with the UI lifecycle. Many operations initiated within these scopes start from main thread callbacks (like user interactions, lifecycle events, or observing data that was updated on the main thread). Using <code>immediate</code> optimizes for this frequent pattern, ensuring work happens without unnecessary dispatch delays when already on the correct thread.</p>
<p>And crucially, as we explored earlier, <code>immediate</code> doesn't break things when you <em>do</em> need dispatching. When resuming from a background thread (like after a <code>withContext(Dispatchers.IO)</code> block), <code>isDispatchNeeded</code> correctly returns <code>true</code>, and the necessary <code>handler.post{}</code> occurs to get you back to the main thread safely. It aims to provide the best of both worlds: synchronous execution when safe and efficient, and proper dispatching when required.</p>
<p>But if instant execution is not needed and if we are fine to perform operations on main thread by posting task on main thread’s event queue, it’s preferred to use <code>Dispatchers.Main</code>.</p>
<hr />
<p>I hope you got the idea about how exactly dispatcher <code>Main</code> and <code>immediate</code> works in the coroutine and how their behaviour changes in different cases 😃.</p>
<p>Awesome 🤩. I hope you've gained some valuable insights from this. If you enjoyed this write-up, please share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[#51 - My developer blogging journey so far]]></title><description><![CDATA[Hi everyone, I'm excited to share that this is the 51st blog post I'm writing. In this post, I'll share what I've learned over time that has helped me become a better version of myself in writing blogs. I'm writing this post to share the story of how...]]></description><link>https://blog.shreyaspatil.dev/51-my-developer-blogging-journey-so-far</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/51-my-developer-blogging-journey-so-far</guid><category><![CDATA[Blogging]]></category><category><![CDATA[writing]]></category><category><![CDATA[Technical writing ]]></category><category><![CDATA[journey]]></category><category><![CDATA[Android]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Flutter]]></category><category><![CDATA[Developer]]></category><category><![CDATA[content creation]]></category><category><![CDATA[blog]]></category><category><![CDATA[development]]></category><category><![CDATA[technical documentation]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Tue, 18 Feb 2025 14:10:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737892800803/c021cdf1-ee58-4f49-bcc3-7e1af59b53fb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi everyone, I'm excited to share that this is the 51st blog post I'm writing. In this post, I'll share what I've learned over time that has helped me become a better version of myself in writing blogs. I'm writing this post to share the story of how I began writing blogs and how I continued doing it, how this thing helped me in my career. I've noticed that many people want to start something new but hesitate to try it for the first time. Some even give up before they begin. I'm writing this to inspire you by showing how this journey can help you learn 2x as much and enrich your overall experiences. This blog will also guide you through that process.</p>
<hr />
<h1 id="heading-kickstart-of-journey">🎬 Kickstart of journey</h1>
<p><img src="https://ideogram.ai/assets/image/lossless/response/iL5VJ7HbSryqFucf5waoDQ" alt /></p>
<p>In 2019, during my second year of engineering college, I was just a someone who was learning and building apps in Android at that time, working on my new Android app project. I used the Firebase UI SDK to display data from the Firebase Realtime Database. I needed to show data in pages, but at that time, the official Firebase UI library didn't support pagination. Until then, I wasn't even aware of GitHub and open source. Then I learned about open source and GitHub and discovered that the <a target="_blank" href="https://github.com/firebase/FirebaseUI-Android">FirebaseUI-Android SDK</a> is open source, and we can contribute code to it. First, I cloned it, developed a pagination API, and open-sourced it <a target="_blank" href="https://github.com/PatilShreyas/FirebaseRecyclerPagination">internally</a> on my own GitHub account (<em>later I contributed it to the official SDK</em> 😀). I used my version of the API in my Android app, continued using it, and saw that it was serving its purpose.</p>
<p>I thought I should spread the word about it since it was useful for me and could help others too if they needed it. I already had accounts on Twitter and LinkedIn (<em>I just wasn't very active</em>), so I logged in again and noticed that many popular Android developers were writing posts on <strong>medium.com</strong>. At that time, <strong><em>ProAndroidDev, MindOrks, AndroidPub</em></strong>, and others were well-known publishers in the AndroidDev community. So, I signed up on medium.com and wrote my first article. I decided to submit it to <a target="_blank" href="https://proandroiddev.com/">ProAndroidDev</a>, and after a few hours, I received a couple of suggestions for improvements from their editors. I addressed all the feedback, and they approved my post. Finally, on <strong>April 8, 2019</strong>, I published my first tech blog 🎉. I kept the title very simple: “<strong>Firebase Database Pagination - Android 🔥</strong>“.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://proandroiddev.com/firebase-database-paging-android-f59e6dd0dc75">https://proandroiddev.com/firebase-database-paging-android-f59e6dd0dc75</a></div>
<p> </p>
<p>After publishing the blog, the next day on the 9th of April’s night, I shared it on Facebook, Android-related FB groups, Twitter and LinkedIn. Till this point, I never had thought that blogging would become my regular practice. Before publishing the blog, I was so nervous and I was getting so many negative thoughts and had fear of publishing something publicly. Do you know what motivated me to continue it? The next day I woke up in the morning and I saw this message on a Facebook messenger DM 😃</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737895149527/6acd30e6-94b5-4afd-bbb8-95c8c590faf6.png" alt class="image--center mx-auto" /></p>
<p>I read this early in the morning and felt so happy. It was satisfying to know that what I did helped someone. I realized that whatever we share as a learning experience can be useful to anyone, whether it's simple beginner stuff or complex advanced topics. This one message cleared all my fears that I was getting before publishing the article and boosted my confidence. That's when I decided to keep writing articles on medium.com.</p>
<p>As time went on, I started creating my open-source libraries on GitHub and exploring some Android APIs. Then I wrote a few more articles and published them on <strong>MindOrks</strong> and <strong>ProAndroidDev</strong>. I also published an article on the official <strong>Firebase Developers</strong> Medium publication. If you notice, all my early blogs were mostly tutorials kind of stuff and nothing more 😅. I was also getting a good response from them, so continued it. My initial blogs be like 👇🏻</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737895612370/0e4e1dfa-9c37-4a3e-bdbd-3f5f1268b8ba.png" alt class="image--center mx-auto" /></p>
<p>As I published open-source projects and content, I began gaining followers on Twitter and making good connections on LinkedIn. Then one day, the “<strong>Google Developers Experts</strong>” account started following me on Twitter, which boosted my confidence even more (<em>I never expected that to happen</em> 😅*. This happened two years prior of being a GDE*).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737897557263/d808b6c2-f496-466f-b6ec-5ca2b2dea9e3.png" alt class="image--center mx-auto" /></p>
<p>I was also exploring <strong><em>Flutter</em></strong> for cross-platform app development during this time. I learned about <strong><em>Google AppScript</em></strong> and how I could use it as an API for data operations in Google Sheets. So, I created a simple proof of concept with Flutter, made it open-source on GitHub, and <a target="_blank" href="https://medium.com/mindorks/storing-data-from-the-flutter-app-google-sheets-e4498e9cda5d?source=user_profile_page---------43-------------5b88bf6959e5---------------">wrote an article</a> about it. When I published it, the response was surprising. Within a day, I gained many followers on GitHub, Twitter, and LinkedIn. That repository received many stars on GitHub and became the <strong>#1 trending project in the Dart</strong> language category. I wasn't even very skilled in Flutter at that time, but I learned about the growing popularity and strong community of Flutter developers 💪🏻.</p>
<blockquote>
<p><strong>Fun fact:</strong> Even after being a native app developer, my first repo that went <strong>#1 trending</strong> on GitHub was <em>Flutter</em> 😃. And till the date, that blog has more views than my other Android or Kotlin related blogs.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737897884575/d5afb050-995a-47bf-9996-c4af6d0e5902.png" alt class="image--center mx-auto" /></p>
<hr />
<h1 id="heading-lockdown-the-golden-opportunity">🏠 Lockdown - The golden opportunity</h1>
<p><img src="https://ideogram.ai/assets/image/lossless/response/HqgY6T84RC2kVSjPME7eig" alt /></p>
<p>In 2020, I was in the third-year of my engineering when COVID-19 started, colleges closed, and I returned to my hometown, stopping online classes as well. This was a golden opportunity for me to work full-time on open-source projects and focus more on my blogs. Although the COVID period was tough with many challenges in the family, I spent most of my time learning and experimenting with Android development, open-sourcing, blogging, etc.</p>
<p>As seen in the previous section, my all articles were tutorial-ish. I learnt about the concept of Dependency Injection and the Dagger framework. I noticed that in that period, there was a lot of fear about this concept around beginners. So I thought of writing an article that simplifies the concept of Dagger. Finally, I wrote an article in which I explained this DI as a concept by giving a very simple example and relating it to real-life scenarios.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://medium.com/mindorks/introduction-to-dagger-di-by-a-life-way-d34f62540329">https://medium.com/mindorks/introduction-to-dagger-di-by-a-life-way-d34f62540329</a></div>
<p> </p>
<p>After I published this, I received a lot of positive feedback, and many developers thanked me for writing it because they found it easy to understand the basics of Dependency Injection. This gave me a great sense of satisfaction and boosted my confidence, as it was a new type of article for me. So, I continued writing similar blogs.</p>
<p>Lockdown was ongoing, and I was in the last semester of my third year. Due to my blogs and open-source work, I got an internship opportunity at <a target="_blank" href="https://scalereal.com/">ScaleReal</a>. The founder liked that I wrote blogs. One day, we were discussing it, and he asked me if we could implement regular blogging practices at ScaleReal and motivate other employees to contribute as well. He asked me to lead it, and I accepted. I created a <a target="_blank" href="https://medium.com/scalereal">Medium publication</a> of ScaleReal and became its maintainer and editor. I started submitting all my future blogs to that publication. Over time, many others also began contributing, which boosted ScaleReal's reach significantly! 😄. I used to review every blog, which helped me gain valuable experience in blog reviewing.</p>
<p>During the lockdown, I wanted to encourage blogging among my friends and college students, so I used what I learned from ScaleReal’s Medium publication. Around that time, I had already initiated a Developer's Club at my college (before COVID). Then I launched a Medium publication called <a target="_blank" href="https://medium.com/dsc-dypcoe">DevClub DYPCOE</a>. I encouraged others to write blogs for this publication. Later that DevClub got migrated into GDSC (Google Developers Student Club). It was really a helpful initiative as many students contributed and published good blogs to that publication. It was good to see that.</p>
<hr />
<h1 id="heading-that-one-opinionated-blog">🤔 That one opinionated blog</h1>
<p>I was feeling confident in development and began forming opinions about different concepts. While reading the release notes of Kotlin Coroutines, I discovered an API called <strong>StateFlow.</strong> By looking at it, it looked like an advanced version of <strong>LiveData</strong> to me. Till that time, it was not mentioned anywhere whether it’s gonna replace LiveData or not. I wrote a simple blog explaining how the StateFlow API could be used instead of LiveData in Android. Since I wasn't sure if it was truly a replacement, I added a question mark "?" at the end of the title. The title of that blog became: <strong>“🌊 StateFlow, end of LiveData?”</strong></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://medium.com/scalereal/stateflow-end-of-livedata-a473094229b3?source=user_profile_page---------38-------------5b88bf6959e5---------------">https://medium.com/scalereal/stateflow-end-of-livedata-a473094229b3?source=user_profile_page---------38-------------5b88bf6959e5---------------</a></div>
<p> </p>
<p>The response was incredible! The blog received around 2K+ reads in one night. Even people from Google reacted to it. It became so popular that some other bloggers copied the title and parts of the content 🤦🏻. My first opinionated blog was a success, <code>confidence++</code>. What could boost confidence more than that? 😄. So far in the Android-related blogs of mine, this one has the highest views on the medium.com.</p>
<hr />
<h1 id="heading-diving-deeper-into-the-context">📐 Diving deeper into the context</h1>
<p>Later, I began writing about under-the-hood topics, such as how certain things work internally, and deeper concepts of Android and Kotlin. I started exploring Jetpack Compose and wrote about it as well. I also wrote about architecture best practices and created a series of articles on how to use Kotlin language features effectively. The response was quite positive.I was also working on open-source projects alongside my writing. Whenever I learned something new, I would write about it in detail.</p>
<p>In 2021, I became the <a target="_blank" href="https://x.com/GoogleDevsIN/status/1425329067658649601">youngest Google Developer Expert for Android in India</a>. That was huge for me!</p>
<p>Meanwhile, I joined <a target="_blank" href="https://paytm.com">Paytm</a> and became a Senior Android Engineer. As we adopted Jetpack Compose, I encountered new challenges and learned something new about APIs every day. My experience at Paytm taught me a lot, and many of my later blog posts were inspired by the challenges we faced there.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">😅</div>
<div data-node-type="callout-text"><strong>Fun fact: </strong>When I joined the team at Paytm, I discovered a funny twist: the interviewer for my first round was actually hesitant to interview me. He was saying to his team that he also learns from my blogs and how can he ask me the questions in the interview. Despite his initial reluctance, we ended up having a great conversation, and now he's not just a former colleague but also a good friend 😄.</div>
</div>

<p>So, blogging has helped me a lot in my career!</p>
<p>I continued writing blogs, and now you're reading my 51st blog here! Do you think it was always smooth? Now let me share the dilemmas and challenges that I faced as well.</p>
<hr />
<h1 id="heading-english">📖 English</h1>
<p><img src="https://ideogram.ai/assets/image/lossless/response/Ex9hwp5wSiOAsG8_EC0xqg" alt /></p>
<p>Until 2021, I wasn't very good at English (and maybe I'm still not). My general communication skills were poor. I only understood tech concepts and could write about them, but I made a lot of grammatical mistakes. If you read my early blogs, you'll notice this and easily find many English-related errors. This caused me a lot of anxiety when I was about to hit the "<strong>Publish</strong>" button for my first blog post. I used to think, "<em>What will people think if they see my writing?</em>" and "<em>How will they judge me?</em>". However, I pressed that button for the first time and continued to do so many more times. My writing was very simple; I never used complicated words in any of my blogs. I avoided fancy English words and complex language. It was straightforward enough for any beginner to understand. But still, I was not so confident about my writing style and one day, a developer from community sent me a message on Twitter (now X). He mentioned that he likes whatever I like, then he wrote this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739117486890/25b786cb-66cb-47a6-bc64-a9d6cf068d05.png" alt class="image--center mx-auto" /></p>
<p>I was surprised after reading this. That day, I learned that we often overthink before taking action. Stop overthinking and just go for it! In my case, it was "English"; for others, it might be something else. So my advice is to ignore the negative thoughts and focus on positive actions. This can not only boost your confidence but also help others. After my first few blogs, I started using Grammarly, which helped me identify common English mistakes in my writing. In today's world, LLMs can also help refine your sentences effectively.</p>
<hr />
<h1 id="heading-criticism">😨 Criticism</h1>
<p><img src="https://ideogram.ai/assets/image/lossless/response/ZYmHbUM-SqeirzOPvq-Akg" alt /></p>
<p>Everything was going well while I was writing simple blog posts. It was all positive and boosted my confidence. However, things started to change when I began writing opinionated posts and posts related to architecture. Architecture is a topic that often sparks debate, and it's natural for developers to have different opinions. So whenever I wrote articles related to architecture or engineering best practices, I’ve been always criticized for them. But that doesn't mean I've never received good reviews on it. Opinions are always mixed on such topics. Another thing is that I used to include a lot of emojis in my blog posts, which led to some criticism, mostly on Reddit. Here's a glimpse of the criticism on Reddit:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739119769378/e35751fe-40b7-4ec1-a56c-45c3f9ad50e8.jpeg" alt class="image--center mx-auto" /></p>
<p>Today's world has many negative people who do nothing themselves but are always ready to criticize. We always encounter both types of people: positive and negative. So, don't just focus on the negativity. On Reddit, most comments were about my use of emojis. However, on a personal level, I once received feedback from one of my connections who actually liked how I use emojis in my blogs, saying it feels attractive. So, in my opinion, there's no perfect or wrong way here 🤷🏻‍♂️.</p>
<p>Many times, I've also engaged in tech debates on Twitter, LinkedIn, and Reddit about various topics, but always in a healthy way (<em>regardless of the other person's tone</em> 😂). But criticism is not always harmful; it can be constructive too. If I ever made a mistake, I accepted it and corrected it in my blog before it could mislead any readers. It all depends on the author's attitude towards handling criticism. Once, I wrote an article and accidentally attached the wrong code snippet from a GitHub gist. I found out about it 30 minutes after posting, thanks to a comment on Reddit, and I quickly fixed it. I learned to take such feedback positively, which ultimately helped me become a better version of myself day by day. But I also learned to ignore some comments because not all of them are helpful. Some comments are just nonsense and won't benefit anyone, neither you nor the readers. So you should know what to take and what to ignore otherwise it can impact you a lot.</p>
<h2 id="heading-learning-getting-post-reviewed">🔍 Learning - Getting post reviewed</h2>
<p>Later on, I started having my blog posts reviewed by my tech-savvy friends and colleagues. Sometimes, when I wrote in-depth about a specific concept, I reached out to experts in that field from the community for help with reviewing and proofreading the blog. I always mentioned them at the bottom of my blog. Occasionally, I even sought the help of Google DevRel Engineers to get feedback on some opinionated blogs to ensure the intention was clear. This is the best way to feel confident before publishing a blog that might be sensitive. It's a really good habit I've developed, and I highly recommend it to anyone starting their blogging journey!</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">😃</div>
<div data-node-type="callout-text">I would like to mention my friends who often reviewed my blogs and helped making it better. Many thanks to: <a target="_self" href="https://himanshoe.com/">Himanshu Singh</a>, <a target="_self" href="https://sagarviradiya.dev/">Sagar Viradiya</a>, <a target="_self" href="https://siddroid.com/">Siddhesh Patil</a>, <a target="_self" href="https://thedroidlady.com/">Niharika Arora</a> 🙏🏻. Fact: We didn't know each other before, and we only connected through blogging. We met in person after two years of knowing each other online. Now, we're not just connected through tech, we also have a good personal relations. My point is that blogging has helped me build some great personal connections too.</div>
</div>

<hr />
<h1 id="heading-self-promotion-is-important">📈 Self promotion is important!</h1>
<p><img src="https://ideogram.ai/assets/image/lossless/response/sYjs4CA2Q8WjZYUf2xvt7A" alt /></p>
<p>Once you hit "Publish," your work isn't done! I always shared my posts on Twitter, LinkedIn, Facebook, Reddit, Hackernews, as well as on community Slack channels like Kotlinlang. Newsletters like <a target="_blank" href="https://androidweekly.net">AndroidWeekly.net</a> and <a target="_blank" href="https://kotlinweekly.net">KotlinWeekly.net</a> are fantastic for reaching the right readers. Every Sunday, when my blogs were featured in these newsletters, I noticed a spike in viewers on the analytics graph. It just works well ✈️. Many people hesitate to share what they've created (<em>I did too when I was a beginner, but I learned by watching others</em>). But you have to do it. <strong>If you don’t, who else will?</strong> When you share your work, there will come a time when <em>others start sharing your blog links on their feeds</em> if it’s really a good quality work. Over time, Google search became my top source of organic referrals for my blogs (<em>Thanks to the SEO of Medium and Hashnode</em> so far).</p>
<hr />
<h1 id="heading-keep-learning">📚 Keep learning</h1>
<p>Technology is vast, and staying updated is crucial, especially if you want to continue blogging for the long term. Blogging itself is a learning experience. Even after about five years, I still feel like a beginner in writing blogs. I admit I'm still trying to improve and learn good blogging practices. I read other blogs to learn from them. I read official tech blogs, blogs by famous developers, company blogs, and my friends' blogs, which teach me a lot about technical writing. I believe there's no limit to learning, and it will always continue. So, keep learning!</p>
<hr />
<h1 id="heading-celebrating-a-small-milestone">🎉 Celebrating a small milestone</h1>
<p><img src="https://ideogram.ai/assets/image/lossless/response/3AS4pbHfSfy9DYeKSBqpSw" alt /></p>
<p>As a content creator, celebrating a small milestone not only releases dopamine for us but can also inspire others. So, don't hesitate to share your achievements or small wins too. Through this post, I'm absolutely thrilled to share that <mark>I've reached a total of </mark> <strong><mark>~605K views</mark></strong> <mark>for my blogs since I started!</mark> It's such an amazing feeling to know that what I've written has reached half a million screens! 🎉. But of course, this is not done yet, and there is much more to come in the future. I’m really excited to see what comes next for me in this journey.</p>
<hr />
<h1 id="heading-wrapping-up-this-post">🎬 Wrapping up this post</h1>
<p>So, that's been my blogging journey so far, and I'm really enjoying it. Thanks to the amazing tech community, especially the Android and Kotlin communities, the readers who supported me, my friends, tech enthusiasts, and the editors of the publications who helped review some of my posts ❤️.</p>
<p>I'm really excited to keep blogging about tech and to improve a little each day.</p>
<p>I hope this article encourages anyone who isn't confident enough to hit that "Publish" button right now. I would just say, hit "Publish" and go with the flow!</p>
<p>If you notice, I end every blog with this line 😄👇🏻</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[Benchmark Insights: Direct State Propagation vs. Lambda-based State in Jetpack Compose]]></title><description><![CDATA[Hey composers 👋🏻, welcome to this analysis blog! Here, we'll dive into some benchmark analysis on the state propagation approach in Jetpack Compose and try to reach some conclusions. This might be a bit opinionated, but feel free to share your thou...]]></description><link>https://blog.shreyaspatil.dev/benchmark-insights-direct-state-propagation-vs-lambda-based-state-in-jetpack-compose</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/benchmark-insights-direct-state-propagation-vs-lambda-based-state-in-jetpack-compose</guid><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android Studio]]></category><category><![CDATA[android apps]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[jetpack compose  UI components]]></category><category><![CDATA[jetpack]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin coroutines]]></category><category><![CDATA[UI]]></category><category><![CDATA[performance]]></category><category><![CDATA[UX]]></category><category><![CDATA[Recompose]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Tue, 19 Nov 2024 14:19:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731822480991/3e42431d-5c8f-438e-9b7a-3dd0f8f3f685.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey composers 👋🏻, welcome to this analysis blog! Here, we'll dive into some benchmark analysis on the state propagation approach in Jetpack Compose and try to reach some conclusions. This might be a bit opinionated, but feel free to share your thoughts in the comments (as this is the beauty of our Android Community ❤️).</p>
<h2 id="heading-context">💁🏻 Context</h2>
<p>Last week, I published a blog <a target="_blank" href="https://blog.shreyaspatil.dev/skipping-the-invocation-of-intermediate-composables"><strong><em>“Skipping the composition of intermediate composables“</em></strong></a> that sparked some controversial opinions and comments across Twitter and Reddit. This prompted me to write a post discussing the benchmarking analysis between the two approaches. So, if you’re reading this post directly, I recommend you read my previous blog post first and then continue from here.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.shreyaspatil.dev/skipping-the-invocation-of-intermediate-composables">https://blog.shreyaspatil.dev/skipping-the-invocation-of-intermediate-composables</a></div>
<p> </p>
<h2 id="heading-lets-start">🫴🏻 Let’s start</h2>
<p>I found the following comment on Reddit, and it gave me an idea for an application where UI updates happen frequently across multiple components.</p>
<p><a target="_blank" href="https://www.reddit.com/r/androiddev/comments/1gosan5/comment/lwle3un/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731832700240/dd8add10-392d-440f-a254-42af5dbd59e8.png" alt class="image--center mx-auto" /></a></p>
<p>I whipped up a quick prototype for a screen that shows the stock market. It includes basic stuff like market indices, an overview of investments, and holdings in a scrolling list. Here's what it looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731827996193/ff954806-d2b2-4125-8bb5-78b96e22b644.gif" alt class="image--center mx-auto" /></p>
<p>So, in this post, I won't dive into the details of the Compose implementation. Instead, I'll give you a quick overview of what the Compose tree looks like with the graphic below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731840057534/291889f3-251e-4f3b-8415-741810d52d33.png" alt class="image--center mx-auto" /></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🧑‍💻</div>
<div data-node-type="callout-text">If you want to check out the code implementation, take a look <a target="_self" href="https://github.com/PatilShreyas/compose-benchmark-lambda">at this repository</a>.</div>
</div>

<p>Since this is just a sample app pretending to be like a real app, I added fixed data that changes randomly at intervals. For example, market indices update every 300ms, the investment summary updates every 250ms, and the holdings list updates every 500ms. This means there will be a total of 9-10 data updates per second for the UI.</p>
<h3 id="heading-implementation-variants">🧑🏻‍💻 Implementation variants</h3>
<p>As you already might know the implementation variants (if you’ve read the previous blog), we have named them as “Before” and “After”.</p>
<h4 id="heading-before">Before:</h4>
<p>Direct state model propagation through the composable functions. For implementation detail, refer to the branch: <a target="_blank" href="https://github.com/PatilShreyas/compose-benchmark-lambda/tree/main"><strong><em>main</em></strong></a><strong><em>.</em></strong></p>
<h4 id="heading-after">After:</h4>
<p>State model propagation through the Kotlin Lambda function references through the composable functions. For implementation detail, refer to the branch: <a target="_blank" href="https://github.com/PatilShreyas/compose-benchmark-lambda/tree/lambda"><strong><em>lambda</em></strong></a><strong><em>.</em></strong> Can also refer to <a target="_blank" href="https://github.com/PatilShreyas/compose-benchmark-lambda/pull/1">this PR</a> to have a look on the changes.</p>
<p>This is how the change looks between both the variants:</p>
<p><a target="_blank" href="https://github.com/PatilShreyas/compose-benchmark-lambda/pull/1/files"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732029705684/bde1f405-7849-4d3f-8b2c-a237ae7a471d.png" alt class="image--center mx-auto" /></a></p>
<p>Before running the benchmarks, I ensured that the composable functions of both branches are <strong><em>stable and that all restartable functions can be skipped</em></strong> with the help of compose compiler report.</p>
<h2 id="heading-benchmark">📐 Benchmark</h2>
<p>To benchmark this, let's use macrobenchmark. For our purpose, it's straightforward: we just need to run the benchmark for a certain period, and that's it. So, in our case, let's run this benchmark for 10 seconds. We’ll perform 10 iterations of this benchmark on the app to get the aggregated data and we want to track the following metrics:</p>
<ul>
<li><p><code>FrameTimingMetric</code>: This will be useful to know the amount of time the frame takes to be produced on the CPU on both the UI thread and the <code>RenderThread</code>.</p>
</li>
<li><p><code>FrameTimingGfxInfoMetric</code>: It’s legacy than above metric but this gives the insights on the janks via <code>dumpsys gfxinfo</code>.</p>
</li>
<li><p><code>MemoryUsageMetric</code>: We’ll use this to check memory consumption. Only for HeapSize and GPU size and keeping mode as <code>Mode.Max</code> which will give us the info about maximum memory usage observed during the benchmark.</p>
</li>
</ul>
<p>So, benchmark test looks as follows:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@RunWith(AndroidJUnit4::class)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">StockScreenBenchmark</span> </span>{
    <span class="hljs-meta">@get:Rule</span>
    <span class="hljs-keyword">val</span> benchmarkRule = MacrobenchmarkRule()

    <span class="hljs-meta">@Test</span>
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">startup</span><span class="hljs-params">()</span></span> = benchmarkRule.measureRepeated(
        packageName = <span class="hljs-string">"com.example.stockexample"</span>,
        metrics = listOf(
            FrameTimingMetric(),
            FrameTimingGfxInfoMetric(),
            MemoryCountersMetric(),
            MemoryUsageMetric(
                mode = MemoryUsageMetric.Mode.Max,
                subMetrics = listOf(
                    MemoryUsageMetric.SubMetric.HeapSize,
                    MemoryUsageMetric.SubMetric.Gpu
                )
            )
        ),
        iterations = <span class="hljs-number">10</span>,
        startupMode = StartupMode.COLD
    ) {
        pressHome()
        startActivityAndWait()

        Thread.sleep(<span class="hljs-number">10000</span>)
    }
}
</code></pre>
<p>That’s it!</p>
<p>Also, as per the best practices, we are going to run these benchmark tests obviously on the <strong><em>release equivalent build</em></strong> having <strong><em>R8 enabled for optimization</em></strong> but by disabling obfuscation (<em>as benchmarking needs it</em>)</p>
<p>Now just run these benchmarks on variety of the devices and analyse the results. I ran these benchmarks on 3 devices on total out of which 2 were physical devices and last one was an emulator.</p>
<p><strong><em>Why 3 devices?</em></strong> <em>because</em> I was unable to believe the results, so I decided to run these on different devices.</p>
<p>Cool, let’s see the analysis after performing the benchmarks on the “Before” and “After” variants.</p>
<hr />
<h2 id="heading-analysis">🧐 Analysis</h2>
<p>So, first off, I kicked things off by running benchmark tests on the super low-end phone I have, which has just 2 GB of RAM. I wasn’t expecting much from these benchmark tests and guess what? Here are the results!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731857984372/f20e953f-e04c-4f3c-84af-0befc05a27ac.png" alt class="image--center mx-auto" /></p>
<p>I couldn't believe these results! So, I ran a second round of benchmark tests on the same device for both variants, and the results from Round 2 finally gave me some relief:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731858078410/310d0452-9043-42d3-8f36-adb76af53320.png" alt class="image--center mx-auto" /></p>
<p>Surprisingly, when comparing the lambda-based approach with the direct state approach, there was a huge reduction in the UI jank percentage by <strong>30-33%</strong>! And not only that, but there was a <strong>reduction</strong> <strong><em>in maximum heap memory consumption</em></strong> by <strong><em>800 KB to 2134 KB</em></strong> 🤯 (<em>this is 10 iterations’ data and that too with running benchmark tests twice</em>). Also, at the <strong><em>99th percentile</em></strong>, negative results were observed.</p>
<p>Then I decided to run these benchmarks on another device, but I didn't have one with me. That's when I remembered that Firebase Test Lab devices are now accessible through Android Studio. There are a variety of devices available there, just like real devices in the cloud. This opened up exciting possibilities for further testing!</p>
<p>I found a device: Moto G20, which runs on <strong>Android 11 and has 4 GB of RAM</strong>. So, I ran these tests on that device, and here are the results:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731858540800/3a741576-ec0c-4fbf-af76-cb353be9da10.png" alt class="image--center mx-auto" /></p>
<p>A similar trend was observed here too! There were reductions in both the jank percentages, frame time at 99th percentile and the maximum heap memory consumption.</p>
<p>Then I ran the third test on the Android Emulator (Pixel 8a - Android 14):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731858677565/945b512e-456f-4af7-ae45-92afb41d51ed.png" alt class="image--center mx-auto" /></p>
<p>On the emulator, according to the frame metrics, there were no janks. However, memory usage remained consistent, with the minimum, median, and maximum values all showing reduction of <strong>heap memory consumption by around 2400 KB</strong> 🧐.</p>
<blockquote>
<p><strong>UPDATE:</strong></p>
<p><em>After this blog was published, on X</em> <a target="_blank" href="https://x.com/keyboardsurfer/status/1858894309732278501"><em>we had a discussion</em></a> <em>about Full R8 mode and till above section’s benchmark results, Full R8 mode wasn’t enabled for the app, so I performed benchmark tests again on the same low-end device twice to confirm the benchmark results on the Full R8 mode and here was the result:</em></p>
</blockquote>
<p><strong>Round 1 (Full R8 Mode enabled)</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732033109940/d313053e-31db-4fb5-8aad-c46a1471ff14.png" alt class="image--center mx-auto" /></p>
<p><strong>Round 2 (Full R8 Mode enabled):</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732033156047/95ebefcd-e198-4751-ae0c-bae7fdde8f0a.png" alt class="image--center mx-auto" /></p>
<p>After turning on Full R8 mode, the delta for frames actually shrinked but still it’s on green side with lambdas 😄 and memory heap size usage was still the same even after R8 optimizations.</p>
<p>After running 6 rounds of benchmark tests on 3 different devices, I finally gained some confidence! 😃. Here are some key takeaways:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Parameters</strong></td><td><strong>Observation</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Frame count</strong></td><td>🟨 No major change</td></tr>
<tr>
<td><strong>Jank Percent</strong></td><td>🟩 Major improvement in jank percentage in the low-end devices in lambda-based state variant without Full R8 mode enabled. Minimal improvements when full R8 mode is turned on.</td></tr>
<tr>
<td><strong>Frame time</strong></td><td>🟩 🟥 10ms (minimum) to 200ms (maximum - P99) reduction in frame time in the lambda-based state variant. Low-end devices benefited the most. Occasionally, at P99 on low-end devices, there was a slight decrease in performance when <strong>R8 is disabled</strong>. Very minimal gains when <strong>Full R8 mode is enabled</strong>.</td></tr>
<tr>
<td><strong>Memory</strong></td><td>🟩 Significant improvements were seen in the lambda-based state variant, with a reduction of about 2000 KB in heap size consumption <strong>irrespective of R8 is enabled or not</strong>.</td></tr>
</tbody>
</table>
</div><div data-node-type="callout">
<div data-node-type="callout-emoji">📈</div>
<div data-node-type="callout-text">To check the raw benchmark results, you can <a target="_self" href="https://github.com/PatilShreyas/compose-benchmark-lambda/blob/main/benchmark_result.md">checkout this document</a>.</div>
</div>

<p>If you want to explore this project or run benchmarks, take a look at <a target="_blank" href="https://github.com/PatilShreyas/compose-benchmark-lambda/">this repository</a>.</p>
<hr />
<h2 id="heading-conclusion">🔍 Conclusion?</h2>
<p>Using lambda-based state is actually improving the runtime performance across low-end devices and actually saving the heap memory by around 2000 KB even in such a sample application, so I think lambda-based states are really useful in the scenarios in which nested sub-components change really very frequently in the application.</p>
<p><strong>What is causing heap memory saving?</strong></p>
<blockquote>
<p>When a function is called, memory is allocated for the function's local variables, parameters, and return value. Additionally, if the function creates new objects or data structures, these will also consume heap memory. The impact on heap memory depends on the complexity of the function and the amount of data it processes or generates.</p>
</blockquote>
<p>As you might have learnt from the previous blog that even if compose compiler skips the re-composition of the UI, it’s still invoking the functions and actually performs lot of operations and also checks the equality of the state models every time the state parameters are changed. So <strong>maybe</strong> that has some cost associated with it, <strong>maybe</strong> when data changes so fast, there are so many unnecessary invocations are happening causing unnecessary memory allocation in the heap, <strong>maybe</strong> that’s what this lambda-based state approach is avoiding and that’s why we are seeing the advantages after using lambdas over direct states.</p>
<p><strong>Learning?</strong></p>
<p>For me, the takeaway was that using stateful lambdas doesn't really have any major downsides. In fact, it's super helpful in situations where data updates quickly and leading it to update on the UI. Plus, less janks and memory benefits observed as this approach works great on low-end devices too.</p>
<hr />
<p>Awesome 🤩. I hope you've gained some valuable insights from this. If you enjoyed this write-up, please share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[Skipping the invocation of intermediate composables]]></title><description><![CDATA[Hey Composers 👋🏻, Jetpack Compose is now standard in Android app development, making performance optimization with Jetpack Compose an important topic. This is a short blog about recomposition optimization, where I'll walk you through the concept of...]]></description><link>https://blog.shreyaspatil.dev/skipping-the-invocation-of-intermediate-composables</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/skipping-the-invocation-of-intermediate-composables</guid><category><![CDATA[compose-compiler]]></category><category><![CDATA[Android]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android Studio]]></category><category><![CDATA[android apps]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Kotlin Multiplatform]]></category><category><![CDATA[UI]]></category><category><![CDATA[jetpack]]></category><category><![CDATA[compose]]></category><category><![CDATA[UX]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 11 Nov 2024 13:33:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731153611376/195d009c-743d-491a-8e4d-26ea4dea40a2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Composers 👋🏻, Jetpack Compose is now standard in Android app development, making performance optimization with Jetpack Compose an important topic. This is a short blog about recomposition optimization, where I'll walk you through the concept of using lambdas in the composition scope and how they can be helpful in these situations. Let’s start 😉.</p>
<h2 id="heading-revising-the-officially-recommended-best-practice">Revising the officially recommended best practice</h2>
<p>One of the best practices recommended officially is to <a target="_blank" href="https://developer.android.com/develop/ui/compose/performance/bestpractices#defer-reads">defer the reads as long as possible</a> and everyone might be familiar with this code snippet:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731299288982/4e2977d8-f062-411c-81ba-810154079147.png" alt class="image--center mx-auto" /></p>
<p>It explains that using <code>offset()</code> directly updates the value in the composition, causing recompositions whenever the value changes. However, if we use <code>offset {}</code>, a lambda-based modifier, it skips the composition phase entirely and goes directly to the layout phase. This improves overall performance because there is no recomposition.</p>
<p>Now this was about the modifier that changes the layout, but this knowledge can be applied to the composition as well.</p>
<hr />
<h2 id="heading-come-to-the-point">Come to the point…</h2>
<p>When we design a screen in Compose, it consists of components and sub-components. According to best practices, the <strong><em>screen is a stateful composable</em></strong>, and all components and sub-components within the screen should receive their state from the screen, making them <strong><em>stateless composables</em></strong>. Therefore, it is the screen's responsibility (actually taken from the ViewModel) to supply the data needed for the components.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731143173432/2640ffc4-0afb-465a-bb5a-f5dc2956ab39.png" alt class="image--center mx-auto" /></p>
<p>In the <strong><em>real applications</em></strong>, this is actually very big. Sometimes, it’s more than 15-20 state parameters for a single screen in the real app. When any of the sub-component’s state gets changed, it flows from screen to the components in the journey.</p>
<p>Now let’s say <strong>from the above figure</strong>, <strong><em>if Component 2.2’s (C 2.2) state gets updated, then how compositions will occur</em></strong>? So, it’ll be as follows:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731238112782/80d4f34a-c70a-45dc-87b0-3348905db13b.png" alt class="image--center mx-auto" /></p>
<p>In this case, <strong>Screen</strong> and <strong>Component 2</strong> won't be recomposed (unless they're unstable). Recomposition means their UI logic won't run, but some parts will still execute. Before recomposition, it checks if it can be skipped. To do this, part of the function runs. If it can be skipped, the UI logic won't run; otherwise, it will.</p>
<p><em>Understand this with this simple example:</em></p>
<p><em>See how the composable code looks like after the compilation (i.e. after reverse engineering)</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731144976431/92199875-7d2c-4490-8f3d-f9ba0764d68c.png" alt class="image--center mx-auto" /></p>
<p>Notice the yellow highlighted line that checks whether the composition can be skipped. Only then is <code>Text()</code> called; otherwise, it is not. However, this means some lightweight operations will still occur in the <code>Content()</code> composable even if the composition is skipped. This skipping is decided based on the equality of previous and next state i.e. <code>equals()</code> method of the state parameter types. Now let’s see this actual code which we’ll try to improve:</p>
<p>So have a look on the snippet:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731149409098/67f95376-ccd4-4167-9566-95990dbe5168.png" alt class="image--center mx-auto" /></p>
<p>In this example, we first create a UI model to represent the state of the UI <code>Name</code> and <code>RemainingSeconds</code>. Although it's a data class, I've intentionally implemented the <code>equals()</code> method and added logs there. <code>Screen</code> composable is a stateful composable that decreases remaining seconds value every 100ms. Also, do notice that we have added logging statements to each block ⬆️.</p>
<p>Now, this is how our other composable looks:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731149543550/d4412f54-b2bf-4c6e-883d-705cc67e1bae.png" alt class="image--center mx-auto" /></p>
<p>Overall, this is how our structure looks:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731147624385/ee33eacb-3ec9-4a3d-8378-71228130f6a7.png" alt class="image--center mx-auto" /></p>
<p>Now, we can run this. What do you think logs will be? 🧐. So logs will be like these:</p>
<pre><code class="lang-diff"> Content(Name(value=John Doe), RemainingSeconds(value=60))
 Detail(Name(value=John Doe))
 Timer(RemainingSeconds(value=60))
 -----------------
 RemainingSeconds#equals
<span class="hljs-addition">! Screen$Column</span>
<span class="hljs-addition">! Name#equals})</span>
 RemainingSeconds#equals
<span class="hljs-addition">! Content(Name(value=John Doe), RemainingSeconds(value=59))</span>
 Timer(RemainingSeconds(value=59))
<span class="hljs-comment">-----------------</span>
 RemainingSeconds#equals
<span class="hljs-addition">! Screen$Column</span>
<span class="hljs-addition">! Name#equals})</span>
 RemainingSeconds#equals
<span class="hljs-addition">! Content(Name(value=John Doe), RemainingSeconds(value=58))</span>
 Timer(RemainingSeconds(value=58))
 -----------------
 RemainingSeconds#equals
<span class="hljs-addition">! Screen$Column</span>
<span class="hljs-addition">! Name#equals})</span>
 RemainingSeconds#equals
<span class="hljs-addition">! Content(Name(value=John Doe), RemainingSeconds(value=57))</span>
 Timer(RemainingSeconds(value=57))
 -----------------
</code></pre>
<p>Now you can see that in our simple application, we are just updating <code>remainingSeconds</code> every 1000ms but still every time <code>Screen$Column</code>, <code>Content()</code> block is called (<em>obviously we knew that</em>) then equality of both <code>Name</code> and <code>RemainingSeconds</code> is checked and by compose compiler magic only <code>Timer</code> is recomposed and <code>Detail</code> is not recomposed (i.e. skipped).</p>
<p>If we get this <code>remainingSeconds</code> value via lambda, can it change this behaviour? Let’s try. So quickly making changes in the code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731149904842/f3ea9de2-784b-4a09-94fb-29d3c23f8bcc.png" alt class="image--center mx-auto" /></p>
<p>And run the app now and see the results ⬇️.</p>
<pre><code class="lang-plaintext">Screen$Column
Content(Name(value=John Doe), dev.shreyaspatil.composelambda.example.MainActivityKt$$ExternalSyntheticLambda2@233875c)
Detail(Name(value=John Doe))
Timer(60)
-----------------
RemainingSeconds#equals
Timer(59)
-----------------
RemainingSeconds#equals
Timer(58)
-----------------
RemainingSeconds#equals
Timer(57)
-----------------
</code></pre>
<p>This time, we are just seeing the equality check for <code>RemainingSeconds</code> and recomposition of <code>Timer</code> composable and this way little execution of <code>Screen$Column</code> and <code>Detail</code> composable functions is also skipped 🎉.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">😁</div>
<div data-node-type="callout-text">So we've just skipped the intermediate composables otherwise they would have to see if they can skip by running their skippability checks, just so they can skip recomposing 😂.</div>
</div>

<p>I believe we are not making any major improvements here, but we are saving a small amount of code execution time. This optimization can be particularly useful for screens where a deeply nested component is updated frequently, such as for animation states or similar scenarios. In such cases, it can reduce the number of equality checks and the number of times composable functions are invoked.</p>
<p><strong>Some examples could be:</strong></p>
<p><strong>Example 1:</strong> When a component is updated often, like a timer or a frequently changing list of data, based on the state produced by the ViewModel.</p>
<p><strong>Example 2:</strong> When a component is performing animations on the UI, and its state comes from the screen or ViewModel.</p>
<p>Additionally, this approach can be beneficial when intermediate composables are not marked as stable by the Compose compiler, allowing them to be skipped. Ultimately, decisions like these should be made based on the specific needs of the use case, as the Compose compiler already does an excellent job of skipping unnecessary recompositions. By carefully considering when to apply these optimizations, developers can ensure their applications run efficiently without unnecessary overhead.</p>
<hr />
<p>Awesome 🤩. I trust you've picked up some valuable insights from this. If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[Kotlin Exception Handling: Why Singleton Exceptions are a bad idea]]></title><description><![CDATA[Hey developers 👋🏻, in this micro-blog we’ll walk through an important learning everyone should have while working with Exceptions/Errors in the Kotlin.
Have you ever seen or declared Exceptions as object in Kotlin? Something like this:
object NoCon...]]></description><link>https://blog.shreyaspatil.dev/kotlin-exception-handling-why-singleton-exceptions-are-a-bad-idea</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/kotlin-exception-handling-why-singleton-exceptions-are-a-bad-idea</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin beginner]]></category><category><![CDATA[Android]]></category><category><![CDATA[Java]]></category><category><![CDATA[Kotlin Multiplatform]]></category><category><![CDATA[jvm]]></category><category><![CDATA[exceptionhandling]]></category><category><![CDATA[Exception Handling]]></category><category><![CDATA[best practices]]></category><category><![CDATA[coding]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[mistakes]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Wed, 18 Sep 2024 12:56:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726597748074/d6967a5c-9b75-46c0-a715-664194c565ae.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey developers 👋🏻, in this micro-blog we’ll walk through an important learning everyone should have while working with Exceptions/Errors in the Kotlin.</p>
<p>Have you ever seen or declared <strong><em>Exceptions</em></strong> as <code>object</code> in Kotlin? Something like this:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">object</span> NoConnectivityException : Exception(<span class="hljs-string">"No internet connectivity!"</span>)
<span class="hljs-keyword">object</span> ResourceNotFoundException : Exception(<span class="hljs-string">"Blah Blah Blah!"</span>)
</code></pre>
<p>Can you think what it can lead to? 🤔</p>
<p>Let’s understand it with a simple example. Have a look at the following snippet:</p>
<pre><code class="lang-kotlin"><span class="hljs-comment">/**
 * Represents a network failure
 */</span>
<span class="hljs-keyword">object</span> NoConnectivityException : Exception(<span class="hljs-string">"No internet connectivity!"</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Repository</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getUserData</span><span class="hljs-params">()</span></span>: Result&lt;String&gt; = runCatching {
        <span class="hljs-comment">// Imagine user fetching operation here and </span>
        <span class="hljs-comment">// it fails due to connectivity issue!</span>
        <span class="hljs-keyword">throw</span> NoConnectivityException
    }

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getAppData</span><span class="hljs-params">()</span></span>: Result&lt;String&gt; = runCatching {
        <span class="hljs-comment">// Imagine some data fetching operation here and </span>
        <span class="hljs-comment">// it fails due to connectivity issue!</span>
        <span class="hljs-keyword">throw</span> NoConnectivityException
    }
}
</code></pre>
<p>Here, we have declared <code>NoConnectivityException</code> as an <code>object</code> and both the repository methods throw that Exception when these methods are called.</p>
<p>Then let’s simulate a real app flow:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> repository = Repository()

<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> userDataResult = repository.getUserData()

    <span class="hljs-comment">// &lt;&lt; Few Moments Later in the real app &gt;&gt;</span>

    <span class="hljs-keyword">val</span> appDataResult = repository.getAppData()
    appDataResult.exceptionOrNull()?.printStackTrace()
}
</code></pre>
<p>So we want to print the <strong><em>stack trace</em></strong> if an error has occurred while getting the app data via <code>getAppData()</code> method. Now, can you guess what would be the output of this🌚? Here you go ▶️</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726596250200/10624b4b-5ac8-4fd7-abc3-6ef8c0f460b0.png" alt class="image--center mx-auto" /></p>
<p>Wrong stack trace for the wrong method! 😏This was expected because <code>object</code> <strong>creates a singleton instance of class lazily</strong> when it’s accessed for the first time. In the above sample, we called the method <code>getUserData()</code> before <code>getAppData()</code> and inside <code>getUserData()</code> method when <code>throw NoConnectivityException</code> was executed, the instance was created and kept forever. Next time when <code>getAppData()</code> throws the Exception, it’s throwing an Exception which was earlier created by <code>getUserData()</code> method 🤷🏻‍♂️.</p>
<p>Now imagine what would happen if it's a critical exception that gets logged into your app's dashboard (<em>like 🔥Crashlytics or any other tool</em>). It could mislead developers 🌚.</p>
<p>Ideally, <strong><mark>Exception instances shouldn’t be singletons!</mark></strong> If some Exceptions are only used internally to represent state or info that won't be monitored at scale, it might be okay to make them singletons. In places where the stack trace is important and shouldn't be ignored, it can be dangerous🔴 for the app.</p>
<p>These little things can easily be overlooked, but they have a big impact overall. Make sure to handle exceptions safely in your app 👍🏻.</p>
<hr />
<p>Awesome 🤩. I trust you've picked up some valuable insights from this. If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item><item><title><![CDATA[Effortless Compose Compiler report analysis]]></title><description><![CDATA[Hey Androiders 👋🏻, If you're building an app with Jetpack Compose, you might know that to make your app perform well with as few recompositions as possible, you should use stable parameters in the composable function, and so on.
To do this, the Com...]]></description><link>https://blog.shreyaspatil.dev/effortless-compose-compiler-report-analysis</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/effortless-compose-compiler-report-analysis</guid><category><![CDATA[Android]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Android Studio]]></category><category><![CDATA[android development]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Kotlin Multiplatform]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[compose]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[jetpack]]></category><category><![CDATA[compose multiplatform]]></category><category><![CDATA[performance]]></category><category><![CDATA[UI]]></category><category><![CDATA[debugging]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 20 May 2024 12:30:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716123456585/25c09f30-f82d-4938-b484-6bb019f7c756.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Androiders 👋🏻, If you're building an app with Jetpack Compose, you might know that to make your app perform well with as few recompositions as possible, you should use stable parameters in the composable function, and so on.</p>
<p>To do this, the <a target="_blank" href="https://developer.android.com/develop/ui/compose/performance/stability/diagnose#compose-compiler">Compose compiler report</a> helps you check the status of a <em>Composable</em> function, whether it's a restartable function or a skippable function. The report includes statistics of composable functions based on various parameters, details of classes, and <strong>specifics of a composable function</strong> (<em>which is our main focus</em>).</p>
<p>After following the steps mentioned <a target="_blank" href="https://developer.android.com/develop/ui/compose/performance/stability/diagnose#setup">here</a> for generating a report, the report of composable functions looks like this (<em>this is a sample report</em>) ⬇️.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716119621814/633c5b36-4ca0-40bd-85e4-59ef5d655d48.png" alt class="image--center mx-auto" /></p>
<p>This text file contains all the information about the composable functions in the module. It shows whether each function is <strong>restartable, skippable,</strong> or <strong>inline</strong>. Additionally, it includes details about each parameter of the function and its <strong>stability</strong>.</p>
<p>Now imagine an application built entirely with Jetpack Compose, containing many Composable functions. How can a developer look at this report and check what’s working well and what’s not in their project? Since this generates reports in <code>json</code>, <code>csv</code>, and <code>txt</code> files, they are not easily traceable for developers. Additionally, the reports on Composable functions and classes become large and difficult to review. It’s quite tedious, isn't it?</p>
<h1 id="heading-solution">💡Solution</h1>
<p>I've developed a utility (<em>Gradle plugin as well as CLI</em>) that you can simply apply to your module. That's all you need to do, and the plugin will handle the rest, helping you focus on the areas that need your attention. In this post, let's focus on the usage of the Gradle plugin which is super easy to plug and play.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/PatilShreyas/compose-report-to-html">https://github.com/PatilShreyas/compose-report-to-html</a></div>
<p> </p>
<blockquote>
<p>This tool parses the reports and metrics generated by the Compose compiler and converts them into an HTML page. It intelligently highlights problematic and non-problematic composable functions and classes, allowing you to focus on the actual issues in your Compose implementation.</p>
</blockquote>
<p>To apply the plugin, add the entry of the plugin to your module's Gradle file:</p>
<pre><code class="lang-kotlin">plugins {
  id(<span class="hljs-string">"dev.shreyaspatil.compose-compiler-report-generator"</span>) version <span class="hljs-string">"1.3.1"</span>
}
</code></pre>
<p>After syncing the project, you'll see tasks will be generated as per the variants and build types of your project. <em>Example:</em></p>
<p><img src="https://patilshreyas.github.io/compose-report-to-html/images/gradle-plugin-example.png" alt /></p>
<p>To generate the report, run the Gradle task (<em>or directly run the task from the tasks pane available on the right side of IDE</em>). <em>Example:</em></p>
<pre><code class="lang-kotlin">./gradlew :app:releaseComposeCompilerHtmlReport
</code></pre>
<p>After the task runs successfully, you'll see the output in the console with the details of the report in HTML format:</p>
<pre><code class="lang-kotlin">Compose Compiler report <span class="hljs-keyword">is</span> generated: .../noty-android/app/composeapp/build/compose_report/index.html

BUILD SUCCESSFUL <span class="hljs-keyword">in</span> <span class="hljs-number">58</span>s
<span class="hljs-number">1</span> actionable task: <span class="hljs-number">1</span> executed
</code></pre>
<blockquote>
<p>You can also <a target="_blank" href="https://patilshreyas.github.io/NotyKT/pages/noty-android/compose_report.html">view a sample report here</a> to get a glimpse of what the report looks like..</p>
</blockquote>
<p>The report will have four sections:</p>
<h3 id="heading-1-brief-statistics">1. Brief Statistics</h3>
<p>Parses metrics from <code>.json</code> file and represents in tabular format.</p>
<p><img src="https://patilshreyas.github.io/compose-report-to-html/images/brief-stats.png" alt="Brief Statistics" /></p>
<h3 id="heading-2-detailed-statistics">2. Detailed Statistics</h3>
<p>Generates report from <code>.csv</code> file and represents in tabular format. It includes package-wise details of functions and their statuses.</p>
<p><img src="https://patilshreyas.github.io/compose-report-to-html/images/detailed-stats.png" alt="Detailed Statistics" /></p>
<h3 id="heading-3-composable-report">3. Composable Report</h3>
<p>Parses the <code>-composables.txt</code> file, separates composables with and without issues, and highlights the associated problems. This is our main focus. With this, we can separate the composable report based on <strong><em>stability</em></strong> and <strong><em>instability</em></strong>, making it easier to find issues. It highlights the unstable parameters for better focus 👇🏻.</p>
<p><img src="https://patilshreyas.github.io/compose-report-to-html/images/composable-report.png" alt="Composable Report" /></p>
<h3 id="heading-4-class-report">4. Class report</h3>
<p>Parses <code>-classes.txt</code> file and separates stable and unstable classes out of it and properly highlights issues associated with them.</p>
<p><img src="https://patilshreyas.github.io/compose-report-to-html/images/class-report.png" alt="Class Report" /></p>
<p>Cool 😎, what more could a developer need since it helps highlight the issues? After reading the report, developers can focus on fixing the problems instead of spending too much time on the raw report.</p>
<p>Not only that, but the plugin is highly <strong><em>customizable</em></strong>. If you want to focus only on the Composable function part, specifically the unstable composable functions, you can configure the plugin to only have this info in the report.</p>
<p><em>For example: You can configure as follows to only include the unstable composable functions in the report at the specified directory.</em></p>
<pre><code class="lang-kotlin">htmlComposeCompilerReport {
    <span class="hljs-comment">// ONLY show unstable composables in the report without stats and classes</span>
    showOnlyUnstableComposables.<span class="hljs-keyword">set</span>(<span class="hljs-literal">true</span>)

    <span class="hljs-comment">// Output directory where report will be generated</span>
    outputDirectory.<span class="hljs-keyword">set</span>(layout.buildDirectory.dir(<span class="hljs-string">"custom_dir"</span>).<span class="hljs-keyword">get</span>().asFile) <span class="hljs-comment">// Default: module/buildDir/compose_report</span>
}
</code></pre>
<p>And then, this is how the report would look like ⬇️.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716122070750/e8537f1e-bf40-4f7b-988b-6804c8090e91.png" alt class="image--center mx-auto" /></p>
<p>Only Composables with issues. Isn't this nice? 😎</p>
<p>You can explore the plugin and its usage further by visiting the <a target="_blank" href="https://patilshreyas.github.io/compose-report-to-html/use/using-gradle-plugin/">official documentation</a>. You can also see how to <a target="_blank" href="https://patilshreyas.github.io/compose-report-to-html/use/using-cli/">use the CLI</a>. If you want to extend this for your use cases, it's also available in the form of a <a target="_blank" href="https://patilshreyas.github.io/compose-report-to-html/use/using-utility-as-library/">library as a maven artifact</a>.</p>
<p><strong><em>View sample app with implementation of this plugin:</em></strong></p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/PatilShreyas/NotyKT/blob/0305688042f5809cfa4a10c9bcecf5740a446be4/noty-android/app/composeapp/build.gradle#L182-L185">https://github.com/PatilShreyas/NotyKT/blob/0305688042f5809cfa4a10c9bcecf5740a446be4/noty-android/app/composeapp/build.gradle#L182-L185</a></div>
<p> </p>
<p><em>Currently, this plugin does not support KMP's Compose multiplatform, but it will be added soon. If you're interested in solving this, feel free to contribute.</em></p>
<hr />
<p>Awesome 🤩. I trust you've picked up some valuable insights from this. If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<hr />
<h2 id="heading-resources">📚Resources</h2>
<ul>
<li><p><a target="_blank" href="https://patilshreyas.github.io/compose-report-to-html/">Official docs - Compose Report to HTML</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/PatilShreyas/compose-report-to-html">GitHub - Compoe Report to HTML</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Capturing composable to a bitmap without losing a state]]></title><description><![CDATA[Hey Composers 👋🏻,
I'm the maintainer of a library - Capturable, that helps you to convert composable content into a Bitmap image easily. In the very first release of it, as there was no dedicated API from compose, I used to wrap composable content ...]]></description><link>https://blog.shreyaspatil.dev/capturing-composable-to-a-bitmap-without-losing-a-state</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/capturing-composable-to-a-bitmap-without-losing-a-state</guid><category><![CDATA[Android]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[compose]]></category><category><![CDATA[UI]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Mobile apps]]></category><category><![CDATA[Screenshot]]></category><category><![CDATA[capture]]></category><category><![CDATA[android apps]]></category><category><![CDATA[android development]]></category><category><![CDATA[jetpack]]></category><category><![CDATA[jetpack compose layouts and modifiers]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Wed, 20 Mar 2024 13:58:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710868310058/26bc8c80-550a-450c-b5a7-ce6bd3d2fbaf.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Composers 👋🏻,</p>
<p>I'm the maintainer of a library - <a target="_blank" href="https://github.com/PatilShreyas/Capturable">Capturable</a>, <em>that helps you to convert composable content into a Bitmap image easily</em>. In the very first release of it, as there was no dedicated API from compose, I used to wrap composable content inside a <code>ComposeView</code> and then draw a View's Canvas into a Bitmap. Later in Compose 1.6.x, the API was added by which we can redirect rendering into <code>android.graphics.Picture</code>, which can then be used to create a Bitmap.</p>
<p><em>The</em> <a target="_blank" href="https://developer.android.com/jetpack/compose/graphics/draw/modifiers#composable-to-bitmap"><em>official documentation</em></a> <em>has a guide for capturing the composable content into a Bitmap as follows</em> <strong><em>OR</em></strong> <em>see this</em> <a target="_blank" href="https://github.com/android/snippets/blob/5ae1f7852164d98d055b3cc6b463705989cff231/compose/snippets/src/main/java/com/example/compose/snippets/graphics/AdvancedGraphicsSnippets.kt#L93"><em>snippet</em></a> ⬇️</p>
<p><a target="_blank" href="https://developer.android.com/jetpack/compose/graphics/draw/modifiers#composable-to-bitmap"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710649902366/95a10aa2-a15f-4531-a524-ae150a0964ac.png" alt="https://developer.android.com/jetpack/compose/graphics/draw/modifiers#composable-to-bitmap" class="image--center mx-auto" /></a></p>
<p>As this API is more efficient than my previous approach of capturing content, I adopted it in <a target="_blank" href="https://github.com/PatilShreyas/Capturable/releases/tag/v2.0.0">Capturable v2.0.0</a>.</p>
<p>Now it's an interesting part 😁 because I started seeing issues with this and someone also opened a similar issue on GitHub which proved that the above approach is not fulfilling all the use cases. Let's understand in the detail.</p>
<h2 id="heading-issue">Issue 🧐</h2>
<p>Let's say we have a screen on which content can be changed at any time in the runtime i.e. <strong><em>stateful content</em></strong> then this issue was easily reproducible. <em>For example, you want to capture content having a network image (which will be loaded in future), or a simple count-down like continuously changing screen, etc.</em></p>
<p>Let's build a simple continuous counter and try to add a capturing modifier to it. Here is what the code would look like.</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">Counter</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">var</span> count <span class="hljs-keyword">by</span> remember { mutableIntStateOf(<span class="hljs-number">0</span>) }
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(text = <span class="hljs-string">"Count:"</span>)
        Text(text = <span class="hljs-string">"<span class="hljs-variable">$count</span>"</span>, style = MaterialTheme.typography.h4)
    }

    LaunchedEffect(key1 = <span class="hljs-built_in">Unit</span>) {
        <span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
            delay(<span class="hljs-number">1000</span>)
            count++
        }
    }
}
</code></pre>
<p>In the capturing UI, we'll put a simple button below this counter with the label "Capture" and once the user clicks on that button, the current state of the counter should be captured and will be displayed below the button. Code be like ⬇️</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">CounterCapture</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> coroutineScope = rememberCoroutineScope()
    <span class="hljs-keyword">val</span> picture = remember { Picture() }

    <span class="hljs-keyword">var</span> imageBitmap <span class="hljs-keyword">by</span> remember { mutableStateOf&lt;ImageBitmap?&gt;(<span class="hljs-literal">null</span>) }

    Column {
        <span class="hljs-comment">// Content to be captured ⬇️</span>
        Box(
            modifier = Modifier
                .drawWithCache {
                    <span class="hljs-keyword">val</span> width = <span class="hljs-keyword">this</span>.size.width.toInt()
                    <span class="hljs-keyword">val</span> height = <span class="hljs-keyword">this</span>.size.height.toInt()

                    onDrawWithContent {
                        <span class="hljs-keyword">val</span> pictureCanvas =
                            androidx.compose.ui.graphics.Canvas(
                                picture.beginRecording(
                                    width,
                                    height
                                )
                            )
                        <span class="hljs-comment">// requires at least 1.6.0-alpha01+</span>
                        draw(<span class="hljs-keyword">this</span>, <span class="hljs-keyword">this</span>.layoutDirection, pictureCanvas, <span class="hljs-keyword">this</span>.size) {
                            <span class="hljs-keyword">this</span><span class="hljs-symbol">@onDrawWithContent</span>.drawContent()
                        }
                        picture.endRecording()

                        drawIntoCanvas { canvas -&gt; canvas.nativeCanvas.drawPicture(picture) }
                    }
                }
        ) {
            Counter()
        }

        <span class="hljs-comment">// Capture button</span>
        Button(
            onClick = {
                coroutineScope.launch {
                    imageBitmap = createBitmapFromPicture(picture).asImageBitmap()
                }
            }
        ) {
            Text(<span class="hljs-string">"Capture"</span>)
        }

        Divider()

        <span class="hljs-comment">// Captured Image</span>
        imageBitmap?.let {
            Text(
                text = <span class="hljs-string">"Captured Image"</span>,
                modifier = Modifier.padding(<span class="hljs-number">8</span>.dp),
                style = MaterialTheme.typography.h6
            )

            Image(
                bitmap = it,
                contentDescription = <span class="hljs-string">"Captured Image"</span>
            )
        }
    }
}
</code></pre>
<p>But when we run this, we run into an issue 😏. See the issue below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710653417956/2ec1a635-60ae-40c5-9e91-fd734bc0ecb2.gif" alt class="image--center mx-auto" /></p>
<p>Whoa! 😮. It doesn't only break the capturing but also <strong><em>breaks the UI state</em></strong> of a component. Because the counter is not working properly with this.</p>
<p>If we remove <code>drawWithCache {}</code> Modifier from the above code, then there's no issue as such and the counter will work without any issues.</p>
<h2 id="heading-understanding-the-drawwithcache-logic">Understanding the <code>drawWithCache</code> logic 🤔</h2>
<p>Refer to this for step by step understanding of a flow ⬇️.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710864763362/b748faa8-89d4-4acd-99aa-1c454c225b08.jpeg" alt class="image--center mx-auto" /></p>
<blockquote>
<p>The code establishes a caching mechanism using <code>drawWithCache</code>. It creates a temporary <code>picture</code> object to render the composable's content. Once the content is drawn onto the <code>picture</code>, it's then transferred to the main canvas for final display. This approach avoids redundant calculations and re-drawing if the size and relevant state haven't changed, leading to improved performance for complex or frequently updated composables.</p>
</blockquote>
<h3 id="heading-spotting-the-issue">Spotting the issue 🔬</h3>
<p>As we can understand from the logic above, it captures the content from Canvas into a <code>Picture</code> and later it draws the same picture on the canvas (<em>which is going to be displayed on the UI</em>). But this is unaware of recompositions (<em>UI updates</em>). So we need a solution in such a way that we should be able to capture the content with its current state without hampering the UI updates of the content.</p>
<blockquote>
<p>Earlier, I faced the similar issue which was reported on <a target="_blank" href="https://issuetracker.google.com/issues/305653364">Google's issue-tracker</a>. In this issue was with image loading from a network and capturing content of it.</p>
</blockquote>
<h2 id="heading-solution">Solution 💡</h2>
<p>Since this issue was also affecting my library <a target="_blank" href="https://github.com/PatilShreyas/Capturable">Capturable</a>, I solved it using the recently introduced API of Modifier from the latest release of Jetpack Compose. I leveraged <code>Modifier.Node</code> API for this.</p>
<blockquote>
<p><a target="_blank" href="https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier.Node"><code>Modifier.Node</code>is a lower le</a>vel API for creating modifiers in Compose. It is the same API that Compose implements its own modifiers in and is the most performant way to create custom modifiers.</p>
<p>~ <a target="_blank" href="https://developer.android.com/jetpack/compose/custom-modifiers#implement_a_custom_modifier_using_modifiernode">Official Documentation</a></p>
</blockquote>
<p>So I created a custom Modifier node as follows:</p>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CapturableModifierNode</span></span>(...) : DelegatingNode(), DelegatableNode {
    <span class="hljs-comment">// Other logic</span>

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getCurrentContentAsPicture</span><span class="hljs-params">()</span></span>: Picture {
        <span class="hljs-keyword">return</span> Picture().apply { drawCanvasIntoPicture(<span class="hljs-keyword">this</span>) }
    }

    <span class="hljs-comment">/**
     * Draws the current content into the provided [picture]
     */</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">suspend</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">drawCanvasIntoPicture</span><span class="hljs-params">(picture: <span class="hljs-type">Picture</span>)</span></span> {
        <span class="hljs-comment">// CompletableDeferred to wait until picture is drawn from the Canvas content</span>
        <span class="hljs-keyword">val</span> pictureDrawn = CompletableDeferred&lt;<span class="hljs-built_in">Unit</span>&gt;()

        <span class="hljs-comment">// Delegate the task to draw the content into the picture</span>
        <span class="hljs-keyword">val</span> delegatedNode = delegate(
            CacheDrawModifierNode {
                <span class="hljs-keyword">val</span> width = <span class="hljs-keyword">this</span>.size.width.toInt()
                <span class="hljs-keyword">val</span> height = <span class="hljs-keyword">this</span>.size.height.toInt()

                onDrawWithContent {
                    <span class="hljs-keyword">val</span> pictureCanvas = Canvas(picture.beginRecording(width, height))

                    draw(<span class="hljs-keyword">this</span>, <span class="hljs-keyword">this</span>.layoutDirection, pictureCanvas, <span class="hljs-keyword">this</span>.size) {
                        <span class="hljs-keyword">this</span><span class="hljs-symbol">@onDrawWithContent</span>.drawContent()
                    }
                    picture.endRecording()

                    drawIntoCanvas { canvas -&gt;
                        canvas.nativeCanvas.drawPicture(picture)

                        <span class="hljs-comment">// Notify that picture is drawn</span>
                        pictureDrawn.complete(<span class="hljs-built_in">Unit</span>)
                    }
                }
            }
        )
        <span class="hljs-comment">// Wait until picture is drawn</span>
        pictureDrawn.await()

        <span class="hljs-comment">// As task is accomplished, remove the delegation of node to prevent draw operations on UI</span>
        <span class="hljs-comment">// updates or recompositions.</span>
        undelegate(delegatedNode)
    }
}
</code></pre>
<p>In this, <code>CapturableModifierNode</code> inherits from two interfaces: <code>DelegatingNode</code> and <code>DelegatableNode</code>, suggesting it can delegate drawing tasks to other nodes while also being delegatable itself (<em>as we want to re-use the</em><code>CacheDrawModifierNode</code>).</p>
<p>You can see that we are using the same code as we saw earlier inside of <code>CacheDrawModifierNode</code>.</p>
<p>But see the difference that this Modifier only gets attached when capturing of content is requested. <code>drawCanvasIntoPicture</code> is a suspend method which can be called when capturing is requested. At the time of a request (call of the method), the logic of capturing is called via <code>delegate()</code> method. Then we wait until the picture is drawn by observing <code>pictureDrawn</code> (CompletableDeferred&lt;Unit&gt;). The wait is completed after the picture is drawn on the UI from <code>drawIntoCanvas {}</code> lambda. After the picture is drawn, the same node is removed via <code>undelegate()</code> method that removes the delegated <code>CacheDrawModifierNode</code> to prevent unnecessary work.</p>
<p>This can help us solve UI state issues while capturing the content. Also, <strong><em>it ensures that content is only captured when it's requested</em></strong>. So our logic of drawing is only executed at the time of capturing the request and instantly undelegated after it.</p>
<p>After this, let's expose the Modifier element</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> Modifier.<span class="hljs-title">capturable</span><span class="hljs-params">(controller: <span class="hljs-type">CaptureController</span>)</span></span>: Modifier {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span> then CapturableModifierNodeElement(controller)
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">data</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CapturableModifierNodeElement</span></span>(
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> controller: CaptureController
) : ModifierNodeElement&lt;CapturableModifierNode&gt;() {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">create</span><span class="hljs-params">()</span></span>: CapturableModifierNode {
        <span class="hljs-keyword">return</span> CapturableModifierNode(controller)
    }
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Since this has been part of my library Capturable, with it, it can be captured as follows:</p>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">CounterCapture</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">val</span> coroutineScope = rememberCoroutineScope()
    <span class="hljs-keyword">val</span> captureController = rememberCaptureController()

    <span class="hljs-keyword">var</span> imageBitmap <span class="hljs-keyword">by</span> remember { mutableStateOf&lt;ImageBitmap?&gt;(<span class="hljs-literal">null</span>) }

    Column {
        <span class="hljs-comment">// Content to be captured ⬇️</span>
        Box(Modifier.capturable(captureController)) {
            Counter()
        }

        <span class="hljs-comment">// Capture button</span>
        Button(
            onClick = {
                coroutineScope.launch {
                    imageBitmap = captureController.captureAsync().await().
                }
            }
        ) {
            Text(<span class="hljs-string">"Capture"</span>)
        }
    }
}
</code></pre>
<p>All done! Let's see how it works</p>
<h3 id="heading-outcome">Outcome ▶️</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710867198707/5db80347-e880-4a38-977c-6e4588df2240.gif" alt class="image--center mx-auto" /></p>
<p>🚀 Issue fixed, composable content captured 🎯. Mission accomplished! 😀</p>
<p>You can see this <a target="_blank" href="https://github.com/PatilShreyas/Capturable/pull/147">pull request</a> as a reference for code changes I did for my library.</p>
<p>That's it!</p>
<hr />
<p>Awesome 🤩. I trust you've picked up some valuable insights from this. If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<hr />
<h2 id="heading-see-also">See also</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/PatilShreyas/Capturable/pull/147">https://github.com/PatilShreyas/Capturable/pull/147</a></div>
]]></content:encoded></item><item><title><![CDATA[Rich media input from the keyboard in Compose]]></title><description><![CDATA[Hey Composers 👋🏻, if you're also a fan of Jetpack Compose and working on an application that needs to interact with rich media input then finally it's available. Especially, if you're working on a chat application and also using Jetpack Compose, th...]]></description><link>https://blog.shreyaspatil.dev/rich-media-input-from-the-keyboard-in-compose</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/rich-media-input-from-the-keyboard-in-compose</guid><category><![CDATA[Android]]></category><category><![CDATA[compose]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[android app development]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[android apps]]></category><category><![CDATA[UI]]></category><category><![CDATA[UIUX]]></category><category><![CDATA[keyboard]]></category><category><![CDATA[android development]]></category><category><![CDATA[jetpack]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[Mobile Development]]></category><category><![CDATA[Mobile apps]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Fri, 01 Mar 2024 12:49:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709270167491/e1147219-f7c3-467c-ae06-3e242445fa91.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Composers 👋🏻, if you're also a fan of Jetpack Compose and working on an application that needs to interact with rich media input then finally it's available. Especially, if you're working on a chat application and also using Jetpack Compose, then this is gonna solve a use case for you.</p>
<h2 id="heading-before">Before</h2>
<p>In the View system, the API for <a target="_blank" href="https://developer.android.com/develop/ui/views/receive-rich-content"><em>Receiving rich content</em></a> was already there. But there was no such straightforward API support for compose.</p>
<p><em>Imagine we are building a chat app in which users can select GIFs or images from Keyboard and these media will be directly sent on chat.</em></p>
<p>Unfortunately, while using Compose components' TextField APIs, this was not supported, and if a user tried to insert a media, a toast used shown as below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709230456539/4eeef5c6-ef77-4289-b7a3-9d727745991b.gif" alt class="image--center mx-auto" /></p>
<p>I tried to achieve this a lot with various approaches like wrapping <code>TextField</code> composable inside <code>ComposeView</code> or <code>AbstractComposeView</code> and trying to establish a connection between the text field and the keyboard with <code>onCreateInputConnection</code>, but no luck!</p>
<p><a target="_blank" href="https://issuetracker.google.com/issues/198323023"><em>The feature request</em></a> was there on the issues-tracker for a long time but there was no workaround for this. The only thing that worked was <strong><em>wrapping View-based</em></strong><code>EditText</code><strong><em>inside</em></strong><code>AndroidView</code> but it was not gonna help much.</p>
<h2 id="heading-now">Now 🎉</h2>
<p>Finally, after a long wait, In Jetpack Compose <strong>1.7.x</strong> there is an API that can support rich media content handling.</p>
<p>A new Modifier has been introduced for this: <code>Modifier.receiveContent()</code>. In this modifier, we can specify what kind of content we wish to handle (for <em>example: Image, plain text, HTML, or anything else</em>). A variety of content could be received from another app through Drag-and-Drop, Copy/Paste, or from the Software Keyboard.</p>
<p>Let's see how can we receive content from a keyboard (<em>for our chat app use case</em>).</p>
<h3 id="heading-code">Code 🧑🏻‍💻</h3>
<p>You might have used <code>BasicTextField</code> or <code>TextField</code>. A new foundation API has been introduced for the text field: <code>BasicTextField2</code>. This has been improvised and created to replace the existing <code>BasicTextField</code> and is <em>currently an experimental API to use.</em></p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">🗞</div>
<div data-node-type="callout-text"><a target="_blank" href="https://proandroiddev.com/basictextfield2-a-textfield-of-dreams-1-2-0103fd7cc0ec"><em>Read about </em><code>BasicTextField2</code><em> in detail here</em></a> (by <a target="_blank" href="https://medium.com/@astamato">Alejandra Stamato</a>)</div>
</div>

<p>So instead of <code>Text</code> or <code>BasicTextField</code>, use <code>BasicTextField2</code> and use the modifier <code>receiveContent</code> along with it to get the rich content from keyboard input:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="9472956bcb8fb737e96d3fe42755150b"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/PatilShreyas/9472956bcb8fb737e96d3fe42755150b" class="embed-card">https://gist.github.com/PatilShreyas/9472956bcb8fb737e96d3fe42755150b</a></div><p> </p>
<p>In modifier <code>receiveContent</code>, first, you need to specify what type of content you want to get. <code>MediaType</code> holds the MIME type. As needed, you can specify your MIME type. Second is lambda which gets invoked when content is selected (<em>in this case, content will be selected from the keyboard</em>). Lambda has a parameter <code>TransferableContent</code> that contain data, metadata, etc.</p>
<p>That's it! Once you handle the media retrieval through <code>Uri</code>, you're good to go 🎉.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709230142190/7f0ceca7-09a8-49b0-9e12-27f1303ef829.gif" alt class="image--center mx-auto" /></p>
<p>How simple it was, wasn't it? 😀</p>
<p>You can explore <code>receiveContent</code> Modifier for other use cases like drag-and-drop, or from clipboard, or getting different types of files, etc.</p>
<p>You can refer to the following sample app project to try this out: <a target="_blank" href="https://github.com/PatilShreyas/ComposeKeyboardMediaInput">https://github.com/PatilShreyas/ComposeKeyboardMediaInput</a></p>
<h2 id="heading-final-notes">Final notes</h2>
<p>As commented <a target="_blank" href="https://issuetracker.google.com/issues/198323023#comment21">here</a>, there are no plans from official teams to support this <code>BasicTextField</code> because it's going to be <strong><em><mark>deprecated and replaced with </mark></em></strong> <code>BasicTextField2</code><strong><em>.</em></strong> So if you need this to fulfill a use case, you anyway have to migrate to the new text field composable.</p>
<hr />
<p>Awesome 🤩. I trust you've picked up some valuable insights from this. If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<hr />
<h1 id="heading-references">📚References</h1>
<ul>
<li><p><a target="_blank" href="https://android-review.googlesource.com/c/platform/frameworks/support/+/2909937">https://android-review.googlesource.com/c/platform/frameworks/support/+/2909937</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/PatilShreyas/ComposeKeyboardMediaInput">https://github.com/PatilShreyas/ComposeKeyboardMediaInput</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Collecting items from the Flow in chunks💦]]></title><description><![CDATA[Hey Kotliners 👋🏻, Kotlin coroutines are now widely used and many of its APIs are helping developers to simplify things a lot. Flow is one of the popular APIs that developers choose for reactive programming and is quite easy to use as well. In this ...]]></description><link>https://blog.shreyaspatil.dev/collecting-items-from-the-flow-in-chunks</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/collecting-items-from-the-flow-in-chunks</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[coroutines]]></category><category><![CDATA[kotlin coroutines]]></category><category><![CDATA[multithreading]]></category><category><![CDATA[Reactive Programming]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 23 Oct 2023 04:46:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697961492903/6a5e2dd6-433f-4d2e-b37b-97413e60e2ea.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Kotliners 👋🏻, Kotlin coroutines are now widely used and many of its APIs are helping developers to simplify things a lot. Flow is one of the popular APIs that developers choose for reactive programming and is quite easy to use as well. In this article, we'll particularly learn to collect the flow items per required intervals in chunks without losing produced data.</p>
<hr />
<h2 id="heading-why">Why</h2>
<p>Before jumping on the topic, let's understand the need.</p>
<p>You can consider any use case in which Flow produces a lot of data within a small period that can't even skipped. Imagine we are developing an application in which we heavily log analytics. <em>For example, we store logs when the user clicks on the button, user interactions, scrolls, etc</em>. It means we can assume: that whenever a user is doing something, we can say 5-6 analytic events per 2-3 seconds need to be persisted in the database (<em>or need to be sent on the network</em>).</p>
<p>Imagine we are solving the use case like above with the Flow APIs. In such cases, if we are doing operations like DB persistence or network calls frequently, then it can block our business logic for a little time. <em>For example,</em> <strong><em>presentational data saving/fetching should be prioritized over saving/fetching analytical data for any app</em></strong>. These DB calls or network operations happen on reserved thread pools that get busy doing unimportant things. In such cases, if we could get the data in the chunks after fixed intervals then that would solve such issues. Let's see how can we achieve that 😎.</p>
<h3 id="heading-whats-the-advantage-of-collecting-flow-items-in-the-chunks">What's the advantage of collecting Flow items in the chunks?</h3>
<p>When a lot of data is produced within a short period, then processing each data individually becomes costly because <strong>a thread</strong> has to process it. But, if we start processing the same data in bulk, it's not as costly as it is for an individual item.</p>
<p><em>Example: Bulk insertion of items in an SQLite table is always better than inserting each item individually.</em></p>
<hr />
<h2 id="heading-creating-an-operator-on-the-flow-chunked">Creating an operator on the Flow - <code>.chunked()</code></h2>
<p>Have you ever tried <a target="_blank" href="https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/chunked.html"><code>chunked()</code></a> on the Collections?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697903691638/6982107b-408a-42ba-afc7-76493682c11a.png" alt class="image--center mx-auto" /></p>
<p>It creates a chunk (sublist) of items based on the chunk size provided. Similarly, we have to collect the Flow items in chunks. But in our use case, we want to receive the chunks based on the time intervals, not based on the size.</p>
<p>The API interface for this should look like this... ⬇️</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> eventsFlow: Flow&lt;Event&gt; = <span class="hljs-comment">// ...</span>

eventsFlow
    .chunked(<span class="hljs-number">2</span>.seconds)
    .collect { events: List&lt;Event&gt; -&gt;
        <span class="hljs-comment">// Do something with `events`</span>
    }
</code></pre>
<p>So after <code>chunked(Duration)</code>, Flow type should be changed to <code>Flow&lt;List&lt;T&gt;&gt;</code>. Let's start the implementation.</p>
<hr />
<h2 id="heading-design-the-flow-operator-function">Design the "flow" operator function</h2>
<p>Let's create a class first for extending the behaviour of a Flow. We call it <code>TimeChunkedFlow</code> which extends the type <code>Flow&lt;List&lt;T&gt;&gt;</code></p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="2580aed71790ae776788e54a0cc10ed1"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/PatilShreyas/2580aed71790ae776788e54a0cc10ed1" class="embed-card">https://gist.github.com/PatilShreyas/2580aed71790ae776788e54a0cc10ed1</a></div><p> </p>
<p>Now our requirement is: that we have to collect the items from <code>upstream</code> flow and have to emit all the items in chunks that are collected from it after the specified <code>duration</code>.</p>
<p>So, this is what basic logic looks like:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b8c2326e84702cc5f08d22fa6c4cbe4d"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/PatilShreyas/b8c2326e84702cc5f08d22fa6c4cbe4d" class="embed-card">https://gist.github.com/PatilShreyas/b8c2326e84702cc5f08d22fa6c4cbe4d</a></div><p> </p>
<p>But there is a problem 🤔. Whenever the upstream Flow is ended, this <code>TimeChunkedFlow</code> will continue emitting <code>[]</code> (empty list) and will start behaving like a hot♨️ flow even if a cold🧊flow is used. It's because we have launched a child coroutine and collected its job in <code>emitterJob</code> and it's never canceled so <code>collect()</code> is never unblocked.</p>
<p>But wait! If we cancel it directly at the end, it will never emit the last chunk of the Flow (<em>because the coroutine job might be cancelled before emitting</em> the <em>last chunked items in the collector).</em> <strong>So we have to complete the execution of the job carefully by emitting the last chunk as well.</strong></p>
<p>For this, we'll introduce a flag <code>isFlowCompleted</code> that will be helpful for us to break the loop whenever the flow completes ⬇️.</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="36c55d694dbd0fee559e0b85c677578f"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/PatilShreyas/36c55d694dbd0fee559e0b85c677578f" class="embed-card">https://gist.github.com/PatilShreyas/36c55d694dbd0fee559e0b85c677578f</a></div><p> </p>
<p>Again, there is an issue of race condition 🏃🏻‍♂️. If due to its async nature, if list gets accessed among various threads and read/modified without safety then there is a risk of data loss or data duplication. To solve this, <strong>we can use</strong> <code>Mutex</code> <strong>as a lock to safely perform the operations on the</strong> <code>values</code> <strong>list so that it won't get accidental reads/writes</strong>. We can quickly update our logic with Mutex's implementation:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="5678e6ab24715b1ff1dc6544af5dc008"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/PatilShreyas/5678e6ab24715b1ff1dc6544af5dc008" class="embed-card">https://gist.github.com/PatilShreyas/5678e6ab24715b1ff1dc6544af5dc008</a></div><p> </p>
<p>Nice! It's ready to use 🚀. Just create an <strong>operator function for easy chaining for the Flow type.</strong></p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b3ad20f0fbc2a404c26762daea026c53"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/PatilShreyas/b3ad20f0fbc2a404c26762daea026c53" class="embed-card">https://gist.github.com/PatilShreyas/b3ad20f0fbc2a404c26762daea026c53</a></div><p> </p>
<p>Let's try this! Creating a sample flow and let's try collecting the items in chunks👇🏻.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697907079647/daa8290d-9321-42ba-99d3-5143d568d1d4.png" alt class="image--center mx-auto" /></p>
<p>Whoa! We achieved it! We can get chunked data without losing it that too with specified intervals as we wanted 😃.</p>
<p>You can <a target="_blank" href="https://gist.github.com/PatilShreyas/c501182a92aa338e10e6ca9fdfa43da7">refer to the gist here</a> for full implementation.</p>
<p>Just remember that if the upstream flow doesn't produce any item within the specified duration, an empty list <code>[]</code> will be emitted in the collector.</p>
<blockquote>
<p><strong>Note:</strong> Similarly, if you don't want chunked data by time but you want it by size, the implementation can be designed similarly. It's already in discussion on the <a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/issues/1290">GitHub issues</a></p>
</blockquote>
<h3 id="heading-but-theres-a-catch">But, There's a catch! 🔴</h3>
<p>If the Flow <strong><em>gets cancelled within the emission of the next chunk</em></strong>, then data will be lost for that specific intermediate chunk which was going to be emitted. It means the collector will never receive data of the last chunk if it's cancelled before collecting that. So use it wisely by knowing your use cases suitably. <strong>So there'll be always a problem with cancellable</strong> <code>CoroutineScope</code><strong>s</strong>. If you want a workaround for it just for a certain use case, you can wait for cancellation and emit the remaining chunk in the collector as follows:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="b7be1ab0ebb524980bde3f4c47012950"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/PatilShreyas/b7be1ab0ebb524980bde3f4c47012950" class="embed-card">https://gist.github.com/PatilShreyas/b7be1ab0ebb524980bde3f4c47012950</a></div><p> </p>
<p>But this kind of implementation won't be safe to be used within UI use cases. Because, this way, the collector will get the last emission even if the flow is cancelled. <strong><mark>So this is a kind of hack and not a recommended solution ❌</mark>.</strong></p>
<p>So <code>chunked()</code> won't be useful for you if you don't want to lose the data in the cancellation scenarios.</p>
<hr />
<h2 id="heading-remember">Remember</h2>
<p><em><mark>This is not yet a standard library implementation and is still in discussion at </mark></em> <a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/issues/1302"><em><mark>Kotlinx.coroutines</mark></em></a> <em><mark>so consider this implementation as an experiment. If you find any issues in this, feel free to comment here or on the Gist. As per my usage, it has worked so well with cold and hot flows (like SharedFlow).</mark></em></p>
<hr />
<p>Awesome 🤩. I trust you've picked up some valuable insights from this. If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>X</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<p><a target="_blank" href="https://www.whatsapp.com/channel/0029Va4DU4kLY6d03IhsCO11"><strong>Follow my channel on WhatsApp</strong></a> to get the latest updates straight to your WhatsApp inbox.</p>
]]></content:encoded></item><item><title><![CDATA[Runtime Surprise: Kotlin Breaks !!Non-Nullability Promise on Developer Cheating in Field Initialization]]></title><description><![CDATA[Hey Kotliners 👋🏻, This is a mini-blog about an issue I faced while working on some JVM-ish stuff with Kotlin. The issue was very stupid to reproduce but it highlighted the importance of a specific concept.
The issue 🐛
We are writing a small snippe...]]></description><link>https://blog.shreyaspatil.dev/runtime-surprise-kotlin-breaks-non-nullability-promise-on-developer-cheating-in-field-initialization</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/runtime-surprise-kotlin-breaks-non-nullability-promise-on-developer-cheating-in-field-initialization</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Java]]></category><category><![CDATA[Android]]></category><category><![CDATA[kotlin nullsafety]]></category><category><![CDATA[jvm]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Wed, 20 Sep 2023 04:33:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695141558752/06207e2a-2201-42b7-a027-c3968f56f0fa.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Kotliners 👋🏻, This is a mini-blog about an issue I faced while working on some JVM-ish stuff with Kotlin. The issue was very stupid to reproduce but it highlighted the importance of a specific concept.</p>
<h2 id="heading-the-issue">The issue 🐛</h2>
<p>We are writing a small snippet of <a target="_blank" href="https://pl.kotl.in/YNdgCF-Ma">code</a> as below. Do you see any issue here? 👇🏻</p>
<pre><code class="lang-kotlin"><span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">getData</span><span class="hljs-params">()</span></span> = <span class="hljs-string">"SomeData"</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Example</span> </span>{
    <span class="hljs-keyword">val</span> value1: <span class="hljs-built_in">Boolean</span> = computeValue1()
    <span class="hljs-keyword">val</span> value2: String = getData()

    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">computeValue1</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Boolean</span> {
        <span class="hljs-keyword">return</span> value2.isEmpty()
    }
}

<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    Example()
}
</code></pre>
<p>Understanding the snippet:</p>
<ul>
<li><p>There is a class <code>Example</code> having two fields</p>
</li>
<li><p>Field <code>value1</code> gets initialized by <code>computeValue1()</code> which makes computation based on <code>value2</code>'s value</p>
</li>
</ul>
<p>Whenever this snippet is run, you'll see a runtime exception like this 👇🏻</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695136041094/dd90d8b6-5e06-4d5b-802b-53abfd9d7d12.png" alt class="image--center mx-auto" /></p>
<p>Strange! 🤯 no?</p>
<p>Even if fields are declared as <strong>non-nullable</strong> in Kotlin, if we are still getting <code>NullPointerException</code> then it's a serious issue if gets missed from a developer's eye or in the code review, isn't it?</p>
<h2 id="heading-reason">Reason 🤔</h2>
<p>In the snippet, whenever the code is executed and <code>Example</code> is instantiated, first of all <code>value1</code> will be evaluated. But <code>value1</code> has an indirect dependency on the <code>value2</code> which is <strong>not even initialized at that moment</strong> causing <code>NullPointerException</code>.</p>
<p>If we remove the <code>computeValue1()</code> and directly replace the logic in the place of the initializer of <code>value1()</code> as in the <a target="_blank" href="https://pl.kotl.in/BQJYxAyXO">snippet</a> 👇🏻</p>
<pre><code class="lang-diff">class Example {
<span class="hljs-addition">!   val value1: Boolean = value2.isEmpty()</span>
    val value2: String = getData()

    // Method removed 
}
</code></pre>
<p>Here, in this case, we'll directly get a compile-time <strong>error 🔴</strong> in IDE saying "<em>Variable 'value2' must be initialized</em>" as below 👇🏻</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695136925640/dce1d1f3-2369-4974-94de-5044886d4b13.png" alt class="image--center mx-auto" /></p>
<p>So it's safe in such cases ⬆️.</p>
<p>Also, if <code>value2</code> is declared and initialized before the <code>value1</code> then there's no issue. As seen in the <a target="_blank" href="https://pl.kotl.in/eQbG7NCvT">change</a> below.</p>
<pre><code class="lang-diff"><span class="hljs-addition">+   val value2: String = getData()</span>
    val value1: Boolean = computeValue1()
<span class="hljs-deletion">-   val value2: String = getData()</span>
</code></pre>
<p>That's all!</p>
<h2 id="heading-conclusion">Conclusion 💡</h2>
<p>As observed, Kotlin will generate a compile-time error if you attempt direct initialization, but it may <strong>not catch</strong> <strong>the issue</strong> when there is an indirect dependency through a method call. This situation <strong><mark>compromises Kotlin's guarantee of non-nullability for fields on the JVM</mark></strong>, potentially leading to runtime crashes that can significantly disrupt the implementation.</p>
<p>To fix such scenarios, always make sure to <strong><em><mark>verify the order of declaration of fields to avoid indirect dependency on the uninitialized field</mark></em></strong>.</p>
<hr />
<p>Awesome 🤩. I trust you've picked up some valuable insights into addressing inadvertent problems that can arise when working with Kotlin. These solutions can not only streamline your workflow but also alleviate potential confusion, ultimately saving you significant effort.</p>
<p>If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil">X</a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<p><a target="_blank" href="https://www.whatsapp.com/channel/0029Va4DU4kLY6d03IhsCO11">Follow my channel on WhatsApp</a> to get the latest updates straight to your WhatsApp inbox.</p>
]]></content:encoded></item><item><title><![CDATA[Solving the mystery of recompositions in Compose's LazyList]]></title><description><![CDATA[Hi Composers 👋🏻, in this blog we'll discuss the issue which generally affects the performance of the application which presents data on UI with the help of Jetpack Compose's LazyList components (LazyColumn or LazyRow). I found this issue while work...]]></description><link>https://blog.shreyaspatil.dev/solving-the-mystery-of-recompositions-in-composes-lazylist</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/solving-the-mystery-of-recompositions-in-composes-lazylist</guid><category><![CDATA[Android]]></category><category><![CDATA[Jetpack Compose]]></category><category><![CDATA[Kotlin]]></category><category><![CDATA[UI]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 21 Aug 2023 13:03:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/GXNo-OJynTQ/upload/e9274c13580c669c6fb02e3856e2db91.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hi Composers 👋🏻, in this blog we'll discuss the issue which generally affects the performance of the application which presents data on UI with the help of Jetpack Compose's <strong>LazyList</strong> components (<em>LazyColumn</em> or <em>LazyRow</em>). I found this issue while working on my application and I fixed that issue. Later, I got queries about similar issues within the community which motivated me to write about this.</p>
<h2 id="heading-knowing-the-issue">Knowing the issue 🐞</h2>
<p>If you are using Jetpack compose on a regular basis and utilizing <strong>LazyList</strong> APIs then you might have faced this issue in your app. So Let's build a sample app to demonstrate the issue. At the moment, imagine that we are developing our application with <mark>Jetpack Compose</mark> <strong><em><mark>1.4.x</mark></em></strong> APIs. In the sample app, we are simply showing a list in which items are added continuously after some intervals and when clicking on an item in the list, the details are displayed in detail at the bottom of the screen. See the below preview.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692545118852/7f7365d2-cdbd-4b46-bb15-437045ae30f5.gif" alt class="image--center mx-auto" /></p>
<p>Now here's what the code of the List's implementation looks like. Inside of <code>LazyColumn</code> simply all <code>notes</code> are passed as a list and each <code>Note</code> is composed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692545721737/97a2f532-23f5-4c3c-9aad-6f75b88d22be.png" alt class="image--center mx-auto" /></p>
<p>Now when the app is run and checked for the recompositions count in the <strong><em>Layout Inspector,</em></strong> here's a surprise 😮.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692545845515/0b995193-5bed-4d24-9e7a-3444bff0270e.png" alt class="image--center mx-auto" /></p>
<p>As you can see, each item in the list is getting recomposed when a new item is added to a list 🤯. For example, a total of 50 items are added to a list then all 50 Composable note items are recomposed when a new (<em>51st</em>) item is added to a list.</p>
<p>After getting such issues, the first thought which comes to mind is <strong><em>"Stability"</em></strong>, right? We can quickly verify the stability of models and composable functions. See, all our composable functions are stable. (<em>Verified with</em> <a target="_blank" href="https://patilshreyas.github.io/compose-report-to-html/"><em>this plugin</em></a>)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692546266366/0df1142c-7d10-478c-8972-5764667c2e9e.png" alt="Compose Compiler Report generated by https://github.com/PatilShreyas/compose-report-to-html" class="image--center mx-auto" /></p>
<p>So what's the issue here? Let's understand</p>
<hr />
<h2 id="heading-getting-the-root-cause-of-the-issue">Getting the root cause of the issue</h2>
<p>After facing the issue, got a chance to study and investigate this issue in detail and the learnings were interesting.</p>
<p>While experimenting with a lot of code changes, tried one change that fixed the issue. Just note a difference in implementation from the below snippet.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692547379622/58ad80fe-ee84-476b-aa4a-ea29bff0a0ea.png" alt class="image--center mx-auto" /></p>
<p>In the previous implementation, <code>Modifier</code> was exposed as a parameter from a <code>Note</code> composable so that list can add behavior to it. Click of an item was handled inside <code>LazyColumn</code> of <code>NotesScreen</code> composable. In the new implementation, instead of exposing a modifier, we have exposed a <strong>lambda</strong> that takes care of the execution of logic on click, and internally Modifier is handled inside <code>Note</code> composable only without exposing it outside.</p>
<p>Now, after this change, see the recompositions with this new logic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692547666475/1b464b35-67ce-4b5d-9c8a-1d4e6f11c6f6.png" alt class="image--center mx-auto" /></p>
<p>Surprised🤯? With this small change, a list item never got recomposed again and it smartly skipped recomposing the items inside the list. Also, this is not the only solution. There are several variants of potential fixes:</p>
<ul>
<li><p>Remembering a modifier</p>
<pre><code class="lang-kotlin">  <span class="hljs-meta">@Composable</span> 
  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">NotesScreen</span><span class="hljs-params">(...)</span></span> {
      <span class="hljs-comment">// ...</span>
      items(notes) { note -&gt;
          <span class="hljs-keyword">val</span> clickableModifier = remember(note) { 
             Modifier.clickable { <span class="hljs-comment">/* Do something */</span> }
          }
          Note(note = note, modifier = clickableModifier)
      }
  }
</code></pre>
</li>
<li><p>Using a singleton modifier (<em>not usable in our case since we need a note</em>)</p>
<pre><code class="lang-kotlin">  <span class="hljs-keyword">val</span> clickableModifier = Modifier.clickable { <span class="hljs-comment">/* Do something */</span> }

  <span class="hljs-meta">@Composable</span>
  <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">NotesScreen</span><span class="hljs-params">(...)</span></span> {
      <span class="hljs-comment">// ...</span>
      items(notes) { note -&gt;
          Note(note = note, modifier = clickableModifier)
      }
  }
</code></pre>
</li>
</ul>
<p>But again, the question is <strong><em>"Why it's making a difference?".</em></strong></p>
<hr />
<p>To understand more in detail, I even tried using <code>Modifier.pointerInput</code> with old implementation and performing clicks with <code>onTap</code> callback and found that the same issue is <strong>not</strong> happening with this modifier but <strong>only happening</strong> with <code>clickable{}</code> Modifier.</p>
<p>So it's not an issue with all <code>Modifier</code>s but only with <code>clickable{}</code> modifier. After checking the implementation of this modifier, we can see that <em>clickable</em> is a <code>composed</code> modifier which makes it different from another modifier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692548890582/e8d3bd80-19c1-464d-9ea2-7026e6b1c6c0.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-what-is-composed-modifier">What is <code>composed</code> Modifier?</h3>
<p>Docs says:</p>
<blockquote>
<p>Declare a just-in-time composition of a Modifier that will be composed for each element it modifies. composed may be used to implement stateful modifiers that have instance-specific state for each modified element, allowing the same Modifier instance to be safely reused for multiple elements while maintaining element-specific state.</p>
</blockquote>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692551023529/cc4d0c06-6a13-41c8-a52e-357c35add525.png" alt class="image--center mx-auto" /></p>
<p>You can see that <code>composed</code> takes a <code>factory</code> which is a <code>@Composable</code> lambda that returns a value. This lambda is executed when layout nodes are created. <mark>Composable functions/lambdas that return a value are </mark> <strong><mark>not skippable </mark></strong> <mark>and also </mark> <strong><mark>not automatically remembered</mark></strong> <mark>by Compose</mark>.</p>
<p>Whenever such a modifier is used directly inside the <code>LazyListScope</code>, all existing items are recomposed because a new instance of Modifier (with <em>clickable{}</em>) gets created which is not equal to the previous instance of the modifier. <strong><em>In short, every time the function is called, it'll behave like Modifier is changed and it'll cause recompositions</em></strong>.</p>
<p>It means, <mark>if any Modifier that uses composed{} under the hood is used, then it will cause extra recompositions of composable components, despite them having the correct stability.</mark> When directly used within <code>LazyListScope</code>, it affects badly because it's a point where each item is laid inside LazyColumn/LazyRow.</p>
<h4 id="heading-the-solution-is-simple-to-use-with-lazylistscope">The solution is simple to use with LazyListScope</h4>
<ul>
<li><p>Avoid using composed{} modifier directly in <code>LazyListScope</code></p>
</li>
<li><p>If there's any use case that you need to use Modifier in such place, then make sure that modifier is remembered.</p>
</li>
</ul>
<h2 id="heading-but-how-can-we-live-with-it-further">But... how can we live with it further? 🤔</h2>
<p>Definitely, this is a genuine issue and we can't just live with it. That's a reason why the Jetpack Compose team is refactoring Modifiers. <a target="_blank" href="https://youtu.be/BjGX2RftXsU?t=462">In this talk</a> @ Android Dev Summit 2022, <a target="_blank" href="http://intelligiblebabble.com/">Leland Richardson</a> had already discussed the issue in detail and the performance issues with the usage of the composed{} modifier.</p>
<p><a target="_blank" href="https://youtu.be/BjGX2RftXsU?t=462"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692552244164/1f0bbda7-709b-4849-8ae5-d5d41edc3e12.png" alt="Android Dev Summit 2022: Compose Modifiers Deep dive" class="image--center mx-auto" /></a></p>
<p>Also, in the <a target="_blank" href="https://android-developers.googleblog.com/2023/08/whats-new-in-jetpack-compose-august-23-release.html">recent release of Jetpack Compose (August 2023)</a>, the team has already improved significant performance with the Modifier refactoring and has promised to fix the issues with the Clickable modifier in future releases 👇🏻.</p>
<p><a target="_blank" href="https://android-developers.googleblog.com/2023/08/whats-new-in-jetpack-compose-august-23-release.html"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692551905012/e4f726e1-01aa-42b4-8f66-0ae6e9f32f11.png" alt="Source: Android Developers Blog - What’s new in the Jetpack Compose August ’23 release" class="image--center mx-auto" /></a></p>
<p>So in the end, it's great news that we can soon get rid of this issue in the upcoming versions of Compose with which we won't even need to worry about these scenarios and we won't need to tweak solutions as we did so far. Till the time, we can fix the issues as we discussed 😀.</p>
<hr />
<p>Awesome 🤩. I hope you learned how can we fix the performance issues so far till the time full refactoring of the Modifiers is officially made done by the Jetpack Compose team.</p>
<p>If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>Twitter</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
<hr />
<p><em>Many thanks to</em> <a target="_blank" href="https://twitter.com/bentrengrove?s=20"><strong><em>Ben Trengrove</em></strong></a> <em>for helping me understand this issue better and reviewing this blog post</em> 😀.</p>
<hr />
<h2 id="heading-references">📚References</h2>
<ul>
<li><p><a target="_blank" href="https://youtu.be/BjGX2RftXsU">Compose Modifiers Deep dive</a></p>
</li>
<li><p><a target="_blank" href="https://issuetracker.google.com/issues/241154852#comment2">Issue Tracker - Recomposition is not skipped if clickable modifier is used</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Sleepless Concurrency: delay() vs. Thread.sleep()]]></title><description><![CDATA[Hey Kotliners 👋🏻, there's no doubt that Kotlin coroutines have made developer's life easy for asynchronous programming. Coroutine comes with feature-packed powerful APIs by which developers don't need extra effort for achieving something. Just need...]]></description><link>https://blog.shreyaspatil.dev/sleepless-concurrency-delay-vs-threadsleep</link><guid isPermaLink="true">https://blog.shreyaspatil.dev/sleepless-concurrency-delay-vs-threadsleep</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin coroutines]]></category><category><![CDATA[multithreading]]></category><category><![CDATA[Android]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Shreyas Patil]]></dc:creator><pubDate>Mon, 31 Jul 2023 04:39:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/6oFHoikwN6I/upload/a96ecf85b6abebb7e458e131adad59f1.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey Kotliners 👋🏻, there's no doubt that Kotlin coroutines have made developer's life easy for asynchronous programming. Coroutine comes with feature-packed powerful APIs by which developers don't need extra effort for achieving something. Just need to know which API to use where and that's all! When it comes to JVM, coroutines literally have improved the way of writing asynchronous code by reducing <em>callback hells.</em> But how exactly the coroutine achieves it under the hood is always an interesting thing. So here we are to know how delay API works inside the coroutines (inside JVM) 😁.</p>
<h2 id="heading-what-is-a-delay">What is a <code>delay()</code> ?</h2>
<p>Everyone who has used coroutines might have used <code>delay()</code> method already. This is what official docs say about <code>delay()</code>:</p>
<blockquote>
<p><em>"Delays coroutine for a given time without blocking a thread and resumes it after a specified time."</em></p>
</blockquote>
<p>Example: <em>Perform Task-2 two seconds after performing Task-1</em></p>
<pre><code class="lang-kotlin">scope.launch {
    doTask1()
    delay(<span class="hljs-number">2000</span>)
    doTask2()
}
</code></pre>
<p>But here are things to note about <code>delay()</code>:</p>
<ul>
<li><p>It does not block the thread that it's running on</p>
</li>
<li><p>Allows other coroutines to run (on the same thread)</p>
</li>
<li><p>When the delay has expired, the coroutine will be resumed and will continue executing</p>
</li>
</ul>
<p>Interesting! 🧐</p>
<p>Many developers compare <code>delay()</code> with <code>Thread.sleep()</code> method of Java. But actually, there's no comparison at all since they both exist for different use cases. They might look the same but they're different. Let's compare...</p>
<h2 id="heading-whats-threadsleep">What's <code>Thread.sleep()</code> 😴 ?</h2>
<p>This is Java's standard multi-threading API that <strong><em>"causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds"</em></strong></p>
<blockquote>
<p>"<a target="_blank" href="https://docs.oracle.com/javase/tutorial/essential/concurrency/sleep.html">This method is generally used for making processor time available to the other threads of an application or other applications that might be running on a computer system</a>"</p>
</blockquote>
<p>If this is used in coroutines: <strong>It's a Blocking function</strong> means that the function <strong>blocks the thread</strong> that it is running on. This means that <strong>other coroutines cannot run</strong> until the blocking function has finished executing.</p>
<p>Now, to understand it in detail, let's compare sleep() and delay()</p>
<hr />
<h2 id="heading-comparing-sleep-and-delay">Comparing sleep() and delay()</h2>
<p>Let's compare it with an example. Imagine we want to perform some tasks concurrently but only <strong><em>with a single thread.</em></strong> Just like in Android development, there's a Main-thread which is a single thread.</p>
<p>Take a look at the below snippets. <em>In this, two coroutines are launched and a delay/sleep of 1000ms is applied.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690696589757/920e1998-2fc6-4419-afaf-f90abbb9315d.png" alt class="image--center mx-auto" /></p>
<p><strong>Comparison:</strong></p>
<ul>
<li><p>When both coroutines were <strong>launched</strong>:</p>
<ul>
<li><p>With <code>delay()</code>, both coroutines were launched at <strong>same-second</strong> instant (05:48:58)</p>
</li>
<li><p>With <code>sleep()</code>, the Second coroutine was launched exactly after <strong>one second.</strong></p>
</li>
</ul>
</li>
<li><p>When both coroutines <strong>finished:</strong></p>
<ul>
<li><p>With <code>delay()</code>, it took a total execution time of <strong><em>1045ms</em></strong></p>
</li>
<li><p>with <code>sleep()</code>, it took a total execution time of <strong>2044ms</strong></p>
</li>
</ul>
</li>
</ul>
<p>This brings us to conclude the same as we described initially that <code>delay()</code> just <strong><em>suspends the coroutine and allows the other coroutine to re-use the same thread</em></strong> whereas <code>Thread.sleep()</code> directly <strong>blocks the Thread</strong> for a specified duration.</p>
<p>Want to see a different superpower of a coroutine? Then just look at the snippet and case below:</p>
<p><strong>Case:</strong> <em>There is a Thread pool context of a</em> <strong><em>Maximum of 2 threads.</em></strong> <em>The first coroutine is launched, it does some work there, and then it adds a</em> <code>delay()</code> <em>of one second and after that delay, it does some work. Concurrent with the first coroutine, a second coroutine is launched which performs heavy tasks in such a way that a Thread will be spending most of its time executing that task.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690698296577/aac7a2ef-ea66-451b-9cb3-735eca9aab4d.png" alt class="image--center mx-auto" /></p>
<p>Amazing! Before a delay, it was running on a Thread <strong>Duet-1.</strong> After a delay, it resumed on another thread, <strong>Duet-2.</strong> Why so? Since the other thread was busy performing heavy work in another launched coroutine, so it resumed on a different thread after a delay.</p>
<p>Interesting 🧐 , isn't it? That's the thing when Coroutine's docs say...</p>
<blockquote>
<p>"<a target="_blank" href="https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html#debugging-coroutines-and-threads">Coroutines can suspend on one thread and resume on another thread."</a></p>
</blockquote>
<p>Since we now know the powers of delay(), now let's understand how it works under the hood.</p>
<hr />
<h2 id="heading-dissecting-delay-under-the-hood">Dissecting <code>delay()</code> under the hood 🕵🏻‍♀️</h2>
<p>Whenever we call a <code>delay()</code> method, it finds for a <code>Delay</code>'s an implementation in the current Coroutine context and returns that.</p>
<pre><code class="lang-diff">public suspend fun delay(timeMillis: Long) {
    if (timeMillis &lt;= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation&lt;Unit&gt; -&gt;
        if (timeMillis &lt; Long.MAX_VALUE) {
<span class="hljs-addition">!           cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)</span>
        }
    }
}
</code></pre>
<p><a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/Delay.kt#L21C1-L57C2"><code>Delay</code></a> is an interface which has support for methods which schedules the execution of the coroutine after some delay. The methods <code>delay()</code>, <code>withTimeout()</code> are backed by the implementation of this interface.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Delay</span> </span>{
    <span class="hljs-comment">/**
     * Schedules resume of a specified [continuation] after a specified delay [timeMillis].
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">scheduleResumeAfterDelay</span><span class="hljs-params">(timeMillis: <span class="hljs-type">Long</span>, continuation: <span class="hljs-type">CancellableContinuation</span>&lt;<span class="hljs-type">Unit</span>&gt;)</span></span>

    <span class="hljs-comment">/**
     * Schedules invocation of a specified [block] after a specified delay [timeMillis].
     * The resulting [DisposableHandle] can be used to [dispose][DisposableHandle.dispose] of this invocation
     * request if it is not needed anymore.
     */</span>
    <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">invokeOnTimeout</span><span class="hljs-params">(timeMillis: <span class="hljs-type">Long</span>, block: <span class="hljs-type">Runnable</span>, context: <span class="hljs-type">CoroutineContext</span>)</span></span>: DisposableHandle =
        DefaultDelay.invokeOnTimeout(timeMillis, block, context)
}
</code></pre>
<p>Then this interface is implemented by the coroutine executor dispatchers. It provides freedom for dispatchers to implement this functionality as they want. <em>For example, real dispatchers like IO, Default supports delay. Test dispatchers (used in unit testing) can control the delay as needed.</em></p>
<p><code>CoroutineDispatcher</code> is also an abstract class. Dispatchers make use of core threading APIs in the implementation dispatchers. For example, in JVM, Dispatchers like <code>Dispatchers.Default</code>, <code>Dispatchers.IO</code> use implementation of core <a target="_blank" href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/package-summary.html"><code>java.util.concurrent</code></a> APIs like <code>Executor</code>. In Android, <code>Dispatchers.Main</code> makes use of Handler APIs. So if dispatchers want to support delaying, they also implement the <code>Delay</code> interface. The method <code>scheduleResumeAfterDelay()</code> resumes a continuation of a coroutine after a specified delay of milliseconds.</p>
<p>This is how <a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/src/Executors.kt#L143-L156"><code>ExecutorCoroutineDispatcherImpl</code></a> implements the method.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">scheduleResumeAfterDelay</span><span class="hljs-params">(timeMillis: <span class="hljs-type">Long</span>, continuation: <span class="hljs-type">CancellableContinuation</span>&lt;<span class="hljs-type">Unit</span>&gt;)</span></span> {
    (executor <span class="hljs-keyword">as</span>? ScheduledExecutorService)?.scheduleBlock(
        ResumeUndispatchedRunnable(<span class="hljs-keyword">this</span>, continuation),
        continuation.context,
        timeMillis
    )
    <span class="hljs-comment">// Other implementation </span>
}
</code></pre>
<p>Thus, it just schedules a resumption of continuation with the help of <code>schedule()</code> method of <a target="_blank" href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ScheduledExecutorService.html"><code>ScheduledExecutorService</code></a>.</p>
<p>Let's also take a look at Android's implementation of a Dispatcher (<a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/kotlinx-coroutines-android/src/HandlerDispatcher.kt">HandlerDispatcher</a>).</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">scheduleResumeAfterDelay</span><span class="hljs-params">(timeMillis: <span class="hljs-type">Long</span>, continuation: <span class="hljs-type">CancellableContinuation</span>&lt;<span class="hljs-type">Unit</span>&gt;)</span></span> {
    <span class="hljs-keyword">val</span> block = Runnable {
        with(continuation) { resumeUndispatched(<span class="hljs-built_in">Unit</span>) }
    }
    handler.postDelayed(block, timeMillis.coerceAtMost(MAX_DELAY))
    <span class="hljs-comment">// Other implementation </span>
}
</code></pre>
<p>Straightforward 😄. It just posts continuation runnable on the <code>Handler</code> with <code>postDelayed()</code> with delay. <strong><em>That's the reason calling a</em></strong> <code>delay()</code> <strong><em>doesn't block the thread.</em></strong></p>
<p>Example: <em>So when you're writing delayed business logic in a single thread context (Android's Main thread in this example), this is how it's treated under the hood (just for imagination).</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690706214294/3f17eede-0cae-40a8-93d8-847a787a1bde.png" alt class="image--center mx-auto" /></p>
<p>Interesting 🤨, isn't it? So whenever you call <code>delay()</code> in Android, it just creates a nested callback chain of <code>Handler#postDelayed()</code>. The same goes for JVM with <code>Executor</code> APIs. Whereas, if you write the same logic with <code>Thread.sleep()</code>, it blocks that thread till that duration. So, delay() and sleep() are two different things that are not similar.</p>
<hr />
<p>Awesome 🤩. That's the beauty of coroutines. It just skips the writing of callback hell for developers and manages it well internally in such a way that we could synchronously write asynchronous code!</p>
<p>I hope you got the idea about how exactly <code>delay()</code> works in the coroutine 😃.</p>
<p>If you like this write-up, do share it 😉, because...</p>
<p><strong><em>"Sharing is Caring"</em></strong></p>
<p>Thank you! 😄</p>
<p>Let's catch up on <a target="_blank" href="https://twitter.com/imShreyasPatil"><strong>Twitter</strong></a> or <a target="_blank" href="https://shreyaspatil.dev/"><strong>visit my site</strong></a> to know more about me 😎.</p>
]]></content:encoded></item></channel></rss>