<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Prem Singh</title>
    <description>The latest articles on DEV Community by Prem Singh (@prem_singh_1997).</description>
    <link>https://dev.to/prem_singh_1997</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4191368%2F5f3a58fe-0bb6-4a1f-9812-183d3a1fcd36.jpeg</url>
      <title>DEV Community: Prem Singh</title>
      <link>https://dev.to/prem_singh_1997</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prem_singh_1997"/>
    <language>en</language>
    <item>
      <title>Your AI Feature Deserves Better Than a Spinner: A Jetpack Compose Response Card</title>
      <dc:creator>Prem Singh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:38:22 +0000</pubDate>
      <link>https://dev.to/prem_singh_1997/your-ai-feature-deserves-better-than-a-spinner-a-jetpack-compose-response-card-4n83</link>
      <guid>https://dev.to/prem_singh_1997/your-ai-feature-deserves-better-than-a-spinner-a-jetpack-compose-response-card-4n83</guid>
      <description>&lt;p&gt;An AI response can take a moment. During that pause, the interface has a job: explain what is happening, preserve context, and give the user a way forward.&lt;/p&gt;

&lt;p&gt;Let's design a compact response card for an Android writing assistant. The visual direction is simple: a rounded Material surface, a clear heading, generous spacing, and one primary action. The interesting part is how the card behaves when the answer isn't ready.&lt;/p&gt;

&lt;p&gt;This is a UI implementation example, not an AI integration. The response text is supplied by the caller. No model, API key, or network request is involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with four explicit states
&lt;/h2&gt;

&lt;p&gt;Avoid a collection of independent booleans such as &lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;hasError&lt;/code&gt;, and &lt;code&gt;hasResult&lt;/code&gt;. Those can accidentally describe contradictory combinations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Idle&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each state should answer a user question:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;What the interface explains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Idle&lt;/td&gt;
&lt;td&gt;What can I do here?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loading&lt;/td&gt;
&lt;td&gt;Did my action start?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer&lt;/td&gt;
&lt;td&gt;What did I get back?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error&lt;/td&gt;
&lt;td&gt;How can I recover?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Build the card around state and events
&lt;/h2&gt;

&lt;p&gt;The following snippet uses Compose Foundation, Animation, and Material 3 inside an existing Compose app. Include the standard imports from those libraries. It is an illustrative snippet; it has not been compiled or device-tested as a standalone project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;AssistantCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;onGenerate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;colors&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colorScheme&lt;/span&gt;

    &lt;span class="nc"&gt;Surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RoundedCornerShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;surfaceContainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tonalElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;verticalArrangement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arrangement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spacedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Writing assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;titleLarge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onSurface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Turn a rough note into a clearer draft."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bodyMedium&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onSurfaceVariant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="nc"&gt;AnimatedContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;targetState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"assistant_content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
                &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Idle&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="s"&gt;"Ready when you are."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onSurface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;horizontalArrangement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Arrangement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spacedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="n"&gt;verticalAlignment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Alignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CenterVertically&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                            &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="n"&gt;strokeWidth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Preparing your draft…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onSurface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Answer&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bodyLarge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;onSurface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Couldn't prepare a draft. Please try again."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nc"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;onGenerate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;modifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Modifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillMaxWidth&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Preparing…"&lt;/span&gt;
                        &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Try again"&lt;/span&gt;
                        &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Answer&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Generate another draft"&lt;/span&gt;
                        &lt;span class="nc"&gt;AssistantState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Idle&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"Generate draft"&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card receives immutable state and emits an event. It doesn't own the request. This makes it easier to preview each state and connect the same interface to a fake response or a real service. Choose the state owner based on the logic involved; business logic typically belongs in a screen-level state holder such as a ViewModel. See the &lt;a href="https://developer.android.com/develop/ui/compose/state-hoisting" rel="noopener noreferrer"&gt;Android state-hoisting guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AnimatedContent&lt;/code&gt; uses its lambda parameter (&lt;code&gt;current&lt;/code&gt;) to render the transitioning state. Keep that detail: reading the outer &lt;code&gt;state&lt;/code&gt; instead can break the intended transition behavior. The &lt;a href="https://developer.android.com/develop/ui/compose/animation/composables-modifiers" rel="noopener noreferrer"&gt;official animation documentation&lt;/a&gt; explains the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that make it feel finished
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use motion to explain change.&lt;/strong&gt; A gentle content transition is enough here. Constant shimmer or a dramatic glow can compete with the answer. For streamed responses, keep the answer container stable rather than restarting a transition for every token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be honest about progress.&lt;/strong&gt; This example shows indeterminate progress. Display a percentage only when the underlying work can actually measure it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design the recovery path.&lt;/strong&gt; An error message without an action is a dead end. Preserve the original input outside this card so retry doesn't require typing it again. A real request owner should also reject duplicate requests and handle cancellation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check more than the screenshot.&lt;/strong&gt; Test large font sizes, dark mode, long answers, and TalkBack. This sample leaves announcements to the surrounding screen; consider a concise polite live-region announcement when a result becomes available. Avoid announcing every streamed token. Read the &lt;a href="https://developer.android.com/develop/ui/compose/accessibility/semantics" rel="noopener noreferrer"&gt;Compose semantics guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the result identifiable.&lt;/strong&gt; Label AI-generated output and let the user review it before using it elsewhere. A polished surface should never imply the answer has been verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in your next Compose experiment
&lt;/h2&gt;

&lt;p&gt;Render the card in all four states before connecting a model. If the loading and error views feel like afterthoughts, the feature isn't finished yet.&lt;/p&gt;

&lt;p&gt;What would you add next: cancellation, editable output, or a copy action?&lt;/p&gt;




&lt;p&gt;Prem Singh · Lead Android Engineer · Kotlin, Jetpack Compose, and practical Android engineering&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/premsinghsodha7" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/premsinghsodha7/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>design</category>
      <category>kotlin</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Beyond the Run Button: Android Debugging with ADB and Gradle</title>
      <dc:creator>Prem Singh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 03:38:16 +0000</pubDate>
      <link>https://dev.to/prem_singh_1997/beyond-the-run-button-android-debugging-with-adb-and-gradle-5gnf</link>
      <guid>https://dev.to/prem_singh_1997/beyond-the-run-button-android-debugging-with-adb-and-gradle-5gnf</guid>
      <description>&lt;p&gt;“It works when I press Run” is a starting point. A useful debugging workflow also lets another developer reproduce the same build, launch the same route, and collect evidence of the failure.&lt;/p&gt;

&lt;p&gt;Here is a compact terminal workflow for a debug app. Each step answers a question rather than adding another command to memorize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;Examples use macOS/Linux shell syntax, an &lt;code&gt;app&lt;/code&gt; module, and a &lt;code&gt;debug&lt;/code&gt; variant. On Windows, use &lt;code&gt;gradlew.bat&lt;/code&gt; and adapt shell variables and quoting. Run Gradle commands from the project root. Your configured JDK must be compatible with the project's Android Gradle Plugin.&lt;/p&gt;

&lt;p&gt;Have SDK Platform Tools on your PATH and an authorized test device or emulator connected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb devices &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANDROID_SERIAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;emulator-5554
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the serial with one from the output. &lt;code&gt;ANDROID_SERIAL&lt;/code&gt; selects the target for subsequent ADB commands. An &lt;code&gt;unauthorized&lt;/code&gt; device needs approval on the unlocked device.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Can someone else build this exact variant?
&lt;/h2&gt;

&lt;p&gt;Use the project's Gradle Wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew :app:assembleDebug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a conventional project, the APK is at &lt;code&gt;app/build/outputs/apk/debug/app-debug.apk&lt;/code&gt;. Flavors and output customization change tasks and paths; inspect the actual project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew :app:tasks &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a flavor named &lt;code&gt;demo&lt;/code&gt;, look for &lt;code&gt;assembleDemoDebug&lt;/code&gt;. A successful build produces an artifact; it doesn't mean the app was installed or launched. These task conventions are described in the &lt;a href="https://developer.android.com/build/building-cmdline" rel="noopener noreferrer"&gt;Android command-line build guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Is the build on the intended device?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; app/build/outputs/apk/debug/app-debug.apk
adb shell am start &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; com.example.debugcli/.MainActivity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the application ID and activity with your debug variant's actual values. &lt;code&gt;-r&lt;/code&gt; replaces an installed compatible package while retaining its data. A signature mismatch needs investigation; deleting an app is not a neutral fix because it removes local data.&lt;/p&gt;

&lt;p&gt;Alternatively, &lt;code&gt;./gradlew :app:installDebug&lt;/code&gt; builds and installs. Check its output to confirm the selected device or devices when several are connected. See the &lt;a href="https://developer.android.com/tools/adb" rel="noopener noreferrer"&gt;ADB guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Which logs belong to the running process?
&lt;/h2&gt;

&lt;p&gt;After launching the app, get its current PID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell pidof &lt;span class="nt"&gt;-s&lt;/span&gt; com.example.debugcli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose it prints &lt;code&gt;12345&lt;/code&gt;. Use that value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb logcat &lt;span class="nt"&gt;--pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;12345 &lt;span class="nt"&gt;-v&lt;/span&gt; threadtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop streaming with Ctrl+C. An empty PID usually means the process isn't running or the application ID is wrong. Recheck it after a restart: a new process can have a new PID. Separate app processes need separate consideration.&lt;/p&gt;

&lt;p&gt;A PID filter is focused, but can hide useful system-side evidence. For crash investigation, also inspect the crash buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb logcat &lt;span class="nt"&gt;-b&lt;/span&gt; crash &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; threadtime &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; crash-log.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This dumps available crash-buffer entries, which can include other apps and older failures. Match timestamps and package/process details. It is not a complete ANR diagnosis. Inspect supported options with &lt;code&gt;adb logcat --help&lt;/code&gt;; see the &lt;a href="https://developer.android.com/tools/logcat" rel="noopener noreferrer"&gt;Logcat command-line reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Does the deep link actually reach the right screen?
&lt;/h2&gt;

&lt;p&gt;Assume your manifest declares &lt;code&gt;debugcli://product/42&lt;/code&gt; as a supported route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell am start &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; android.intent.action.VIEW &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'debugcli://product/42'&lt;/span&gt; com.example.debugcli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package restriction checks routing inside your app. Also test without that restriction to observe normal system resolution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell am start &lt;span class="nt"&gt;-W&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; android.intent.action.VIEW &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'debugcli://product/42'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success means more than an activity opening: verify the destination, arguments, sign-in behavior, and Back navigation. A custom scheme doesn't prove HTTPS App Links verification. For HTTPS links, test the real domain and its verification separately. See &lt;a href="https://developer.android.com/training/app-links/create-deeplinks" rel="noopener noreferrer"&gt;Create deep links&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Does the feature handle a denied permission?
&lt;/h2&gt;

&lt;p&gt;For an app that declares CAMERA and supports runtime permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell pm revoke com.example.debugcli android.permission.CAMERA
adb shell pm grant com.example.debugcli android.permission.CAMERA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run these separately: revoke, exercise the denied path, then grant and exercise the granted path. Permission changes can affect the app process, so recheck its PID.&lt;/p&gt;

&lt;p&gt;Shell grants don't test the permission dialog or every denial state. Also test the real request flow and the feature's fallback behavior. See &lt;a href="https://developer.android.com/training/permissions/requesting" rel="noopener noreferrer"&gt;Request runtime permissions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Can you attach evidence to the issue?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb exec-out screencap &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; debug-screen.png
adb shell screenrecord &lt;span class="nt"&gt;--time-limit&lt;/span&gt; 20 /sdcard/debug-demo.mp4
adb pull /sdcard/debug-demo.mp4 ./debug-demo.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recording command waits while recording. Pull the file after it finishes. Standard &lt;code&gt;screenrecord&lt;/code&gt; doesn't record audio; protected content may be unavailable. See &lt;a href="https://developer.android.com/tools/adb#screencap" rel="noopener noreferrer"&gt;ADB screen capture and recording&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the workflow into a useful bug report
&lt;/h2&gt;

&lt;p&gt;Attach a short reproduction recipe with the variant, application ID, device/Android version, triggering command, expected result, and observed result. Add a timestamped log excerpt and a short recording when they explain the failure. Use test data and review logs and media before sharing them.&lt;/p&gt;

&lt;p&gt;For your portfolio, include a &lt;code&gt;DEBUGGING.md&lt;/code&gt; beside a sample app. Document one reproducible bug, the evidence that isolated it, and the fix. That shows the reasoning behind your tooling skills.&lt;/p&gt;

&lt;p&gt;Which command saves you the most time when a bug refuses to reproduce from Android Studio?&lt;/p&gt;




&lt;p&gt;Prem Singh · Lead Android Engineer · Kotlin, Jetpack Compose &amp;amp; Android architecture&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/premsinghsodha7" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/premsinghsodha7/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>The Compose Search Bug That Debounce Doesn't Fix</title>
      <dc:creator>Prem Singh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 03:23:04 +0000</pubDate>
      <link>https://dev.to/prem_singh_1997/the-compose-search-bug-that-debounce-doesnt-fix-1kbf</link>
      <guid>https://dev.to/prem_singh_1997/the-compose-search-bug-that-debounce-doesnt-fix-1kbf</guid>
      <description>&lt;p&gt;A user searches for &lt;code&gt;cat&lt;/code&gt;, then changes the query to &lt;code&gt;car&lt;/code&gt;. For a moment, results for cats appear under the new query.&lt;/p&gt;

&lt;p&gt;You already added &lt;code&gt;debounce(300)&lt;/code&gt; and &lt;code&gt;flatMapLatest&lt;/code&gt;. So why can the old request still finish?&lt;/p&gt;

&lt;p&gt;The answer is operator placement. Debouncing reduces request frequency. It does not automatically cancel an in-flight search the instant the user edits the text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The familiar pipeline has a timing window
&lt;/h2&gt;

&lt;p&gt;Consider this simplified chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;queries&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMapLatest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;searchFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the user types &lt;code&gt;car&lt;/code&gt;, the new value waits inside &lt;code&gt;debounce&lt;/code&gt;. Until that value reaches &lt;code&gt;flatMapLatest&lt;/code&gt;, the previous inner flow can continue. If the &lt;code&gt;cat&lt;/code&gt; response arrives during that gap, it can still be emitted.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/debounce.html" rel="noopener noreferrer"&gt;Kotlin debounce reference&lt;/a&gt; describes suppression of rapidly followed values. The &lt;a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flat-map-latest.html" rel="noopener noreferrer"&gt;flatMapLatest reference&lt;/a&gt; describes cancellation when a new upstream value reaches that operator. Those are different moments.&lt;/p&gt;

&lt;p&gt;This behavior can be acceptable when the UI deliberately keeps old results visible and labels them correctly. If you want a new query to cancel old work promptly, move the waiting period inside the switched flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switch first, wait second
&lt;/h2&gt;

&lt;p&gt;The following integration sketch accepts a Flow of query strings and a cancellable suspend search function. It separates waiting, loading, empty results, and failure so the UI can communicate each state clearly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.CancellationException&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.ExperimentalCoroutinesApi&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.delay&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.Flow&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.buffer&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.distinctUntilChanged&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.flatMapLatest&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.flow&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.map&lt;/span&gt;

&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;SearchHit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;SearchState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Idle&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SearchState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Waiting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SearchState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SearchState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchHit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SearchState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SearchState&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@OptIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ExperimentalCoroutinesApi&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;searchStates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;suspend&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchHit&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;distinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMapLatest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Idle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Waiting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="nf"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;cancelled&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SearchState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a Flow-layer example, not a complete Compose app. Connect your repository and UI using dependencies compatible with your project. The experimental opt-in reflects the linked API reference; check the status for your coroutines version.&lt;/p&gt;

&lt;p&gt;Now a new distinct, trimmed query reaches &lt;code&gt;flatMapLatest&lt;/code&gt; before the delay. It cancels the previous inner flow, including its waiting period or cooperative in-flight request. The new flow then starts its own waiting period.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buffer(0)&lt;/code&gt; avoids an extra queued output buffer after the switching operator. It cannot retract a value the UI has already received, so keep the query attached to every result and render it honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep text input responsive
&lt;/h2&gt;

&lt;p&gt;Debounce the search operation, not the text field's displayed value. Typing and cursor movement should update immediately.&lt;/p&gt;

&lt;p&gt;For state-based Compose text fields, &lt;code&gt;TextFieldState&lt;/code&gt; owns editable text, selection, and composition. You can observe its text with &lt;code&gt;snapshotFlow&lt;/code&gt; and feed that into the search pipeline. Preserve the text field state rather than replacing it with a fresh instance on every recomposition. The &lt;a href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/input/TextFieldState" rel="noopener noreferrer"&gt;Android TextFieldState reference&lt;/a&gt; includes examples.&lt;/p&gt;

&lt;p&gt;Choose the lifetime of the collector deliberately. For screen state on Android, lifecycle-aware collection is generally appropriate; a search operation should not accidentally gain a lifetime longer than its owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three details worth getting right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A blank query must reach the switch.&lt;/strong&gt; Filtering out short queries before &lt;code&gt;flatMapLatest&lt;/code&gt; can leave an older search active when the user clears the field. This example emits &lt;code&gt;Idle&lt;/code&gt; inside the switched flow instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catch errors per request.&lt;/strong&gt; The failure handler sits inside the inner flow. A failed request becomes a state while the outer query stream remains available for later input. A single &lt;code&gt;catch&lt;/code&gt; after the whole pipeline can instead complete that collection after an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cancellation is cooperative.&lt;/strong&gt; A suspend function is not automatically well behaved. The repository must support cancellation and avoid blocking the calling thread. Cancelling a client request also doesn't guarantee the server stopped processing it. Never swallow &lt;code&gt;CancellationException&lt;/code&gt; as an ordinary search error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about retrying the same query?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;distinctUntilChanged&lt;/code&gt; intentionally suppresses equal adjacent queries. Assigning the same string again won't implement retry.&lt;/p&gt;

&lt;p&gt;Model retry as a separate action. For example, include an attempt counter in a request model and increment it on retry, so the request stream sees a new value even when the query is unchanged. Decide whether explicit retries should bypass the typing delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the timing, not just the final list
&lt;/h2&gt;

&lt;p&gt;Use a fake search implementation and virtual time to verify these scenarios:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Expected behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rapid typing&lt;/td&gt;
&lt;td&gt;Only the settled query starts a search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit during a request&lt;/td&gt;
&lt;td&gt;Previous cooperative request is cancelled when the new query reaches the switch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clear the field&lt;/td&gt;
&lt;td&gt;State becomes Idle; previous work is cancelled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request fails, then query changes&lt;/td&gt;
&lt;td&gt;A new search can still run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry unchanged text&lt;/td&gt;
&lt;td&gt;Explicit retry creates another attempt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A small demo showing the query, current state, and cancellation logs makes this a useful portfolio feature. It demonstrates an engineering decision that a polished screenshot alone cannot show.&lt;/p&gt;

&lt;p&gt;The next time you review a search pipeline, ask: does cancellation happen when the user changes intent, or only after the debounce window?&lt;/p&gt;




&lt;p&gt;Prem Singh · Lead Android Engineer · Kotlin, Jetpack Compose &amp;amp; Android architecture&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/premsinghsodha7" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/premsinghsodha7/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
    </item>
    <item>
      <title>Your Compose Screen Is Gone. Why Is Your Flow Still Running?</title>
      <dc:creator>Prem Singh</dc:creator>
      <pubDate>Sun, 18 Oct 2026 03:10:19 +0000</pubDate>
      <link>https://dev.to/prem_singh_1997/your-compose-screen-is-gone-why-is-your-flow-still-running-4e2m</link>
      <guid>https://dev.to/prem_singh_1997/your-compose-screen-is-gone-why-is-your-flow-still-running-4e2m</guid>
      <description>&lt;p&gt;Press Home on an Android app. The screen disappears, but that doesn't automatically mean every coroutine feeding it has stopped.&lt;/p&gt;

&lt;p&gt;For a search screen, live dashboard, or AI response view, this distinction matters. A UI collector and the producer behind it have separate lifetimes. Reviewing only one can leave unnecessary work running—or stop work that the product actually needs to finish.&lt;/p&gt;

&lt;p&gt;Here's a small pattern worth understanding before adding more architecture to a Compose screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Collect screen state with the Android lifecycle
&lt;/h2&gt;

&lt;p&gt;For an Android Compose route backed by a ViewModel, use lifecycle-aware collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.Composable&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.compose.runtime.getValue&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.lifecycle.compose.collectAsStateWithLifecycle&lt;/span&gt;

&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;FeedRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FeedViewModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collectAsStateWithLifecycle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;FeedScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FeedScreen&lt;/code&gt; is your rendering composable; &lt;code&gt;FeedViewModel&lt;/code&gt; is supplied by your app's normal ViewModel creation mechanism. Add &lt;code&gt;androidx.lifecycle:lifecycle-runtime-compose&lt;/code&gt; using a version aligned with your project's Lifecycle dependencies.&lt;/p&gt;

&lt;p&gt;By default, collection runs when the lifecycle is at least &lt;code&gt;STARTED&lt;/code&gt;. It stops below that state and starts again when the lifecycle becomes active. This is different from ordinary &lt;code&gt;collectAsState()&lt;/code&gt;, whose collection follows the composition rather than applying this lifecycle threshold. See the &lt;a href="https://developer.android.com/develop/ui/compose/state" rel="noopener noreferrer"&gt;Android Compose state guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That fixes the consumer side. Now look upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Give the upstream an explicit sharing policy
&lt;/h2&gt;

&lt;p&gt;Here is an integration sketch for a repository that observes feed items. It assumes a cold Flow whose observation work is cancelled when its collector is cancelled. Adapt the model and repository to your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.lifecycle.ViewModel&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;androidx.lifecycle.viewModelScope&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.CancellationException&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.Flow&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.SharingStarted&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.StateFlow&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.catch&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.map&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;kotlinx.coroutines.flow.stateIn&lt;/span&gt;

&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FeedUiState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FeedUiState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FeedUiState&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FeedUiState&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;FeedRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;observeItems&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeedViewModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FeedRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;uiState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedUiState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observeItems&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FeedItem&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="nc"&gt;FeedUiState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;FeedUiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cause&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;CancellationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;cause&lt;/span&gt;
            &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;FeedUiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stateIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SharingStarted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;WhileSubscribed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;stopTimeoutMillis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;initialValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FeedUiState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The explicit &lt;code&gt;map&lt;/code&gt; result type allows both &lt;code&gt;Content&lt;/code&gt; and &lt;code&gt;Error&lt;/code&gt; to flow through the same pipeline. The cancellation guard makes the intended behavior clear: cancelling observation should not become a visible failure.&lt;/p&gt;

&lt;p&gt;These are focused integration snippets, not a complete sample app. The repository, UI rendering, and dependency setup remain application-specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Read the timeline, not just the API name
&lt;/h2&gt;

&lt;p&gt;With the policy above, the expected sequence is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UI starts collecting       -&amp;gt; upstream observation starts
last subscriber disappears -&amp;gt; five-second grace period begins
subscriber returns early   -&amp;gt; existing observation continues
no subscriber after 5s     -&amp;gt; sharing cancels upstream collection
subscriber returns later   -&amp;gt; upstream collection starts again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The grace period can bridge short subscriber gaps such as configuration changes. Five seconds is a choice, not a universal performance rule.&lt;/p&gt;

&lt;p&gt;The default replay expiration retains the last value: stopping the upstream doesn't automatically put &lt;code&gt;uiState&lt;/code&gt; back into &lt;code&gt;Loading&lt;/code&gt;. Returning users can see cached content while observation restarts. Read the &lt;a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-sharing-started/-companion/-while-subscribed.html" rel="noopener noreferrer"&gt;Kotlin WhileSubscribed reference&lt;/a&gt; before changing cache behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Know what this pattern does not stop
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;WhileSubscribed&lt;/code&gt; controls this sharing coroutine. It doesn't cancel every job in the ViewModel or shut down an independently running hot producer.&lt;/p&gt;

&lt;p&gt;For example, this job has a different owner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;init&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;viewModelScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refreshOnce&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;refreshOnce()&lt;/code&gt; represents an application-specific suspend function. Its job can continue after the UI stops collecting because it was launched separately in &lt;code&gt;viewModelScope&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This can be intentional. An explicit user action and an observational feed don't always need the same lifetime. Decide whether an operation should stop when the screen becomes inactive, survive a navigation change, or require durable execution. Then choose its owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Handle failures and retry deliberately
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;catch&lt;/code&gt; above turns a failure into an error state, but it does not continuously retry. Once that upstream attempt completes, a retry action needs an explicit strategy, such as a refresh trigger that starts a new observation, or a bounded &lt;code&gt;retryWhen&lt;/code&gt; policy before &lt;code&gt;catch&lt;/code&gt; for suitable transient failures.&lt;/p&gt;

&lt;p&gt;Don't rely on backgrounding and reopening the app as the user's retry mechanism. Also consider retaining existing content alongside a refresh error when that serves the screen better.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful code review exercise
&lt;/h2&gt;

&lt;p&gt;For the next Flow-backed screen you review, trace four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which lifecycle controls the UI collector?&lt;/li&gt;
&lt;li&gt;Which scope owns the producer?&lt;/li&gt;
&lt;li&gt;Does cancellation actually reach the underlying work?&lt;/li&gt;
&lt;li&gt;What happens after failure, and who triggers another attempt?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a portfolio demo, instrument a fake cold Flow with start and cancellation logs, then record navigating away and returning before and after the timeout. Show the logs beside the screen recording. That demonstrates the behavior far more clearly than a screenshot of an API call.&lt;/p&gt;

&lt;p&gt;The key design decision is ownership: the screen's visibility, the observer's lifetime, and the operation's lifetime should each be intentional.&lt;/p&gt;

&lt;p&gt;What do you usually find in reviews: missing lifecycle-aware collection, or upstream work with an unexpected lifetime?&lt;/p&gt;




&lt;p&gt;Prem Singh · Lead Android Engineer · Kotlin, Jetpack Compose &amp;amp; Android architecture&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/premsinghsodha7" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/premsinghsodha7/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
