<?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>Forem: Nadeem Iqbal</title>
    <description>The latest articles on Forem by Nadeem Iqbal (@nadeemiqbal).</description>
    <link>https://forem.com/nadeemiqbal</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3934741%2Fb61f9ea7-1731-470c-af30-6473565b03ea.jpeg</url>
      <title>Forem: Nadeem Iqbal</title>
      <link>https://forem.com/nadeemiqbal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/nadeemiqbal"/>
    <language>en</language>
    <item>
      <title>Building an AI Chat Starter Kit for CMP: ~20 Lines from Empty Screen to ChatGPT-Quality Streaming</title>
      <dc:creator>Nadeem Iqbal</dc:creator>
      <pubDate>Sun, 17 May 2026 12:21:18 +0000</pubDate>
      <link>https://forem.com/nadeemiqbal/building-an-ai-chat-starter-kit-for-cmp-20-lines-from-empty-screen-to-chatgpt-quality-streaming-3he8</link>
      <guid>https://forem.com/nadeemiqbal/building-an-ai-chat-starter-kit-for-cmp-20-lines-from-empty-screen-to-chatgpt-quality-streaming-3he8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faf9sssscgra9ozak7349.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faf9sssscgra9ozak7349.gif" alt="PromptBar and LlmTypewriter working together in an iPhone simulator — slash commands, @-mentions, attachment chips, and a streaming assistant reply with live Markdown and syntax-highlighted code" width="360" height="783"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I open-sourced two Compose Multiplatform libraries that pair into an AI chat starter kit you can drop into any &lt;strong&gt;Android / iOS / Desktop / Web app&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🤖 &lt;strong&gt;&lt;a href="https://github.com/NadeemIqbal/prompt-bar" rel="noopener noreferrer"&gt;prompt-bar&lt;/a&gt;&lt;/strong&gt; — the composer (slash commands, mentions, attachments, send/stop)&lt;/li&gt;
&lt;li&gt;💬 &lt;strong&gt;&lt;a href="https://github.com/NadeemIqbal/llm-typewriter" rel="noopener noreferrer"&gt;llm-typewriter&lt;/a&gt;&lt;/strong&gt; — the renderer (&lt;code&gt;Flow&amp;lt;String&amp;gt;&lt;/code&gt; streaming, live markdown, progressive syntax highlighting)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point isn't either lib in isolation. The point is they're built to wire together so you get a ChatGPT-quality streaming chat UI in ~20 lines on every CMP target.&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;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nadeemiqbal:prompt-bar:0.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nadeemiqbal:llm-typewriter:0.1.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I was working on a side project — a Kotlin Multiplatform app that wraps an LLM API — and I needed a chat UI. The kind you've used a hundred times: textfield at the bottom with slash commands and &lt;code&gt;@&lt;/code&gt;-mentions, attachment chips above it, a Send button that becomes Stop while the assistant streams a reply, markdown-rich responses with syntax-highlighted code blocks.&lt;/p&gt;

&lt;p&gt;Two days later I had a half-working prototype, no tests, and a growing TODO list. The CMP ecosystem just didn't have these pieces wired together.&lt;/p&gt;

&lt;p&gt;Here's what I found when I went looking:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I needed&lt;/th&gt;
&lt;th&gt;What exists today&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chat composer&lt;/td&gt;
&lt;td&gt;Stream Chat ships a polished AI composer for Android, tied to their commercial backend.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slash commands + @ mentions in a composer&lt;/td&gt;
&lt;td&gt;No CMP option I could find. React has good references (Vercel AI Elements is actively working on these).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming typewriter (Flow-based)&lt;/td&gt;
&lt;td&gt;Typist-CMP and Texty cover typewriter animation for static strings. I needed a Flow-of-String source so tokens paint the moment they arrive from an LLM SDK.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live progressive markdown rendering&lt;/td&gt;
&lt;td&gt;No CMP option I could find. Even in React, Vercel's streamdown is the only popular one.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syntax-highlighted code blocks (built up live)&lt;/td&gt;
&lt;td&gt;No CMP option I could find — needs an incremental tokenizer.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The existing libraries are good at what they do. What I needed for my project was a different combination — these five things working together as one experience, on every CMP target. So I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in prompt-bar
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PromptBar&lt;/code&gt; is one composable plus a headless &lt;code&gt;PromptBarState&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inputs you wire:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;slashCommands: List&amp;lt;SlashCommand&amp;gt;&lt;/code&gt; — name + description + hotkey + &lt;code&gt;onSelect&lt;/code&gt; lambda. Type &lt;code&gt;/&lt;/code&gt; and an autocomplete dropdown opens.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mentionProvider: MentionProvider&lt;/code&gt; — &lt;code&gt;suspend fun suggest(query: String): List&amp;lt;Mention&amp;gt;&lt;/code&gt;. Plug in your contact / file / symbol source. Async by design so you can hit Room / Ktor / system contacts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;templates: List&amp;lt;PromptTemplate&amp;gt;&lt;/code&gt; — quick-prompt chips above the input. Tap to populate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;modelSelector: @Composable () -&amp;gt; Unit&lt;/code&gt; — slot for whatever model picker UI fits your app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;onVoiceTap: () -&amp;gt; Unit&lt;/code&gt; — mic button slot. Library doesn't decode audio (BYO).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;State the library owns:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;text&lt;/code&gt; / &lt;code&gt;fieldValue&lt;/code&gt; — the textfield content (live token counter / char count derived)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;attachments: List&amp;lt;PromptAttachment&amp;gt;&lt;/code&gt; — chips above the input; &lt;code&gt;addAttachment&lt;/code&gt; / &lt;code&gt;removeAttachment&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sendState: SendState&lt;/code&gt; — &lt;code&gt;Disabled&lt;/code&gt; / &lt;code&gt;Ready&lt;/code&gt; / &lt;code&gt;Sending&lt;/code&gt; / &lt;code&gt;Streaming&lt;/code&gt; — derived from content, overridable with &lt;code&gt;markSending()&lt;/code&gt; / &lt;code&gt;markStreaming()&lt;/code&gt; / &lt;code&gt;markReady()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selectedModel: ModelOption?&lt;/code&gt; — currently-selected model&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;activeTrigger: ActiveTrigger&lt;/code&gt; — what autocomplete is currently open&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key design decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; and &lt;code&gt;@&lt;/code&gt; only open the dropdown at the start of a line or after whitespace — so &lt;code&gt;email@domain&lt;/code&gt; doesn't spuriously trigger mentions.&lt;/li&gt;
&lt;li&gt;The Send button is &lt;em&gt;one&lt;/em&gt; button that morphs visually based on &lt;code&gt;sendState&lt;/code&gt;. No "Send disabled until text" + "separate Stop button" UX — one button, four visual states.&lt;/li&gt;
&lt;li&gt;Smart paste tokenizer for blobs: paste &lt;code&gt;a@x.com, b@y.com&lt;/code&gt; (comma- or newline-separated) and &lt;code&gt;pasteTokensAsAttachments&lt;/code&gt; splits it into chips.&lt;/li&gt;
&lt;li&gt;Headless state. &lt;code&gt;PromptBarState&lt;/code&gt; can be constructed without composition (handy for ViewModels and tests).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's in llm-typewriter
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;StreamingTypewriter&lt;/code&gt; takes a &lt;code&gt;Flow&amp;lt;String&amp;gt;&lt;/code&gt; of tokens — typically straight from your LLM SDK's streaming API — and reveals them at the cadence dictated by a &lt;code&gt;SpeedCurve&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Flow-of-String API matters.&lt;/strong&gt; A typewriter that takes a static &lt;code&gt;String&lt;/code&gt; means you have to buffer the entire LLM response before showing anything. With a &lt;code&gt;Flow&lt;/code&gt;, the first token paints the moment it arrives — exactly what you want for an LLM chat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live progressive Markdown.&lt;/strong&gt; The renderer re-parses the revealed text every frame using a &lt;em&gt;prefix-stable&lt;/em&gt; parser — the same prefix of input always yields the same prefix of tokens. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;**bold&lt;/code&gt; mid-stream renders as plain text. The moment &lt;code&gt;**&lt;/code&gt; closes, it flips to bold.&lt;/li&gt;
&lt;li&gt;Headings (&lt;code&gt;# Title&lt;/code&gt;) render once the line completes.&lt;/li&gt;
&lt;li&gt;Fenced code blocks (the triple-backtick &lt;code&gt;kotlin&lt;/code&gt; kind) render progressively as the lines arrive — &lt;em&gt;with syntax highlighting&lt;/em&gt;. Code keywords / strings / numbers / comments highlight live, line by line, as the model emits them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Three speed curves.&lt;/strong&gt; A &lt;code&gt;fun interface SpeedCurve&lt;/code&gt; lets you tune the cadence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;SpeedCurve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Linear&lt;/span&gt;    &lt;span class="c1"&gt;// constant — every char takes the same time&lt;/span&gt;
&lt;span class="nc"&gt;SpeedCurve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EaseOut&lt;/span&gt;   &lt;span class="c1"&gt;// slight stretch on whitespace&lt;/span&gt;
&lt;span class="nc"&gt;SpeedCurve&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Natural&lt;/span&gt;   &lt;span class="c1"&gt;// pauses on .!?,;:\n like a human typist&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or write your own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;excitedTypist&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpeedCurve&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;base&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="n"&gt;next&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;next&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="sc"&gt;'!'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Other table-stakes things:&lt;/strong&gt; tap-to-skip (reveal everything immediately on tap), graceful stop-mid-stream (&lt;code&gt;state.stop()&lt;/code&gt; shows a "(stopped)" ghost indicator), custom &lt;code&gt;@Composable&lt;/code&gt; cursor (block, line, underscore, or anything you want), screen-reader-friendly live region.&lt;/p&gt;




&lt;h2&gt;
  
  
  The integration — why these are pitched as a pair
&lt;/h2&gt;

&lt;p&gt;Either library alone is useful. Together, they cover a workflow:&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;ChatScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ChatViewModel&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;prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberPromptBarState&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;typewriter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberStreamingTypewriterState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;// Send/Stop button auto-syncs with the typewriter's lifecycle.&lt;/span&gt;
    &lt;span class="nc"&gt;LaunchedEffect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typewriter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isStreaming&lt;/span&gt;&lt;span class="p"&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;typewriter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isStreaming&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markStreaming&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markReady&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="c1"&gt;// Your message list ... assistant bubble uses StreamingTypewriter:&lt;/span&gt;
        &lt;span class="nc"&gt;StreamingTypewriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responseFlow&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;typewriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;renderer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberMarkdownTypewriterRenderer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// The composer:&lt;/span&gt;
        &lt;span class="nc"&gt;PromptBar&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;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;onSend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;onStop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;typewriter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancelStream&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;slashCommands&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;SlashCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clear"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Clear conversation"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&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;mentionProvider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MentionProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contacts&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;That's the whole integration. Tap Send → message goes out → assistant bubble starts streaming → button morphs to Stop → tap Stop → typewriter freezes mid-token + shows "(stopped)" + button flips back to Send. The polish layer that usually takes a weekend from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Both are 0.1.x. Backlog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;prompt-bar&lt;/strong&gt;: server-driven prompt templates, image attachment previews (composable slot for actual bitmap), full prompt-history scrollback, drag-to-reorder chips, command palette mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llm-typewriter&lt;/strong&gt;: more languages in the highlighter (Rust, Go, Swift), table rendering, footnotes, custom thinking-block recognition, voice-of-thought style cycling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issues / PRs / feedback very welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nadeemiqbal:prompt-bar:0.1.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nadeemiqbal:llm-typewriter:0.1.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache 2.0. Android (minSdk 24) · iOS (x64/arm64/sim) · Desktop (JVM 11) · Web (wasmJs).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://github.com/NadeemIqbal/prompt-bar" rel="noopener noreferrer"&gt;github.com/NadeemIqbal/prompt-bar&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔗 &lt;a href="https://github.com/NadeemIqbal/llm-typewriter" rel="noopener noreferrer"&gt;github.com/NadeemIqbal/llm-typewriter&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://central.sonatype.com/artifact/io.github.nadeemiqbal/prompt-bar" rel="noopener noreferrer"&gt;Maven Central — prompt-bar&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="https://central.sonatype.com/artifact/io.github.nadeemiqbal/llm-typewriter" rel="noopener noreferrer"&gt;Maven Central — llm-typewriter&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built because I needed it. Hopefully saves you a weekend.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>cmp</category>
      <category>ai</category>
      <category>kmp</category>
    </item>
    <item>
      <title>Frosted glass for Compose Multiplatform without OOMing on low-end Android</title>
      <dc:creator>Nadeem Iqbal</dc:creator>
      <pubDate>Sat, 16 May 2026 13:37:50 +0000</pubDate>
      <link>https://forem.com/nadeemiqbal/frosted-glass-for-compose-multiplatform-without-ooming-on-low-end-android-5hkj</link>
      <guid>https://forem.com/nadeemiqbal/frosted-glass-for-compose-multiplatform-without-ooming-on-low-end-android-5hkj</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c1xrs82p04baqkv24wb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c1xrs82p04baqkv24wb.gif" alt="liquid-glass demo: frosted glass surfaces with the quality-tier picker over a colorful backdrop" width="320" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;liquid-glass&lt;/code&gt; is a small Compose Multiplatform library that gives you iOS 26-style frosted backdrop blur. It ships a &lt;code&gt;Modifier.liquidGlass()&lt;/code&gt; plus three composables: &lt;code&gt;GlassCard&lt;/code&gt;, &lt;code&gt;GlassButton&lt;/code&gt;, and &lt;code&gt;GlassNavBar&lt;/code&gt;. Same code runs on Android, iOS, Desktop, and Web.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Compose's built-in &lt;code&gt;Modifier.blur&lt;/code&gt; blurs a composable's own content, not the backdrop behind it. Chris Banes's &lt;a href="https://github.com/chrisbanes/haze" rel="noopener noreferrer"&gt;&lt;code&gt;haze&lt;/code&gt;&lt;/a&gt; library solves the backdrop-blur problem cleanly and I'd happily recommend it. The remaining gap, for me, was graceful degradation. The iOS 26 "liquid glass" effect is heavy enough that Apple itself disables it on older hardware. On Android the same effect can chew through GPU memory on a 2GB device. I wanted a single composable I could drop in without writing per-device branching every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Three explicit quality tiers, auto-picked by platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Blur radius&lt;/th&gt;
&lt;th&gt;Saturation&lt;/th&gt;
&lt;th&gt;Backdrop layer&lt;/th&gt;
&lt;th&gt;Auto-picked on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;24.dp&lt;/td&gt;
&lt;td&gt;1.4x&lt;/td&gt;
&lt;td&gt;Full-res&lt;/td&gt;
&lt;td&gt;Android 12+ (non-low-RAM), iOS 17+, Desktop, Web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;16.dp&lt;/td&gt;
&lt;td&gt;1.2x&lt;/td&gt;
&lt;td&gt;0.5x downsampled&lt;/td&gt;
&lt;td&gt;iOS 15 to 16 (opt-in elsewhere)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback&lt;/td&gt;
&lt;td&gt;0.dp&lt;/td&gt;
&lt;td&gt;1.0x&lt;/td&gt;
&lt;td&gt;None, zero alloc&lt;/td&gt;
&lt;td&gt;Android &amp;lt; 12 or &lt;code&gt;isLowRamDevice&lt;/code&gt;, iOS &amp;lt; 15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;Fallback&lt;/code&gt; tier is the part I care about most. It allocates &lt;strong&gt;zero offscreen buffers&lt;/strong&gt; and skips the blur entirely. The same code that draws frosted glass on a Pixel 9 quietly draws a flat tint with an edge sheen on a 2GB Android 11 device, without an OOM.&lt;/p&gt;

&lt;p&gt;The composables consume the auto-detected tier through a &lt;code&gt;LiquidGlassState&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;Screen&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="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLiquidGlassState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// auto-picks per device&lt;/span&gt;

    &lt;span class="nc"&gt;Box&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;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1) Anything inside this box becomes the backdrop the glass samples from.&lt;/span&gt;
        &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;painter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;painterResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drawable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scenery&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;contentDescription&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;contentScale&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ContentScale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Crop&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;fillMaxSize&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;liquidGlassSource&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="c1"&gt;// 2) A floating glass card on top, sampling the backdrop above.&lt;/span&gt;
        &lt;span class="nc"&gt;GlassCard&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;state&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;align&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;Center&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="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;"Frosted, light-refracting surface, drop-in"&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;You can force a specific tier (for a brand-mandated "Full everywhere" look) or downgrade for low-end shells:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLiquidGlassState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LiquidGlassQuality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Full&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="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rememberLiquidGlassState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LiquidGlassQuality&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Fallback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it isn't
&lt;/h2&gt;

&lt;p&gt;It's 0.1.0. There's no &lt;code&gt;GlassDialog&lt;/code&gt; or &lt;code&gt;GlassBottomSheet&lt;/code&gt; Material 3 wrapper yet, no dynamic-color edge sheen sampled from the captured backdrop, and no Sk SL refraction shader. Those are on the roadmap. If you need any of those today, &lt;code&gt;haze&lt;/code&gt; is more mature and very flexible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// gradle/libs.versions.toml&lt;/span&gt;
&lt;span class="na"&gt;[libraries]&lt;/span&gt;
&lt;span class="n"&gt;liquid-glass&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"io.github.nadeemiqbal:liquid-glass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// commonMain dependencies&lt;/span&gt;
&lt;span class="nf"&gt;kotlin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;sourceSets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;commonMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;liquid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;glass&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;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/NadeemIqbal/liquid-glass" rel="noopener noreferrer"&gt;https://github.com/NadeemIqbal/liquid-glass&lt;/a&gt;&lt;br&gt;
Maven Central: &lt;a href="https://central.sonatype.com/artifact/io.github.nadeemiqbal/liquid-glass" rel="noopener noreferrer"&gt;https://central.sonatype.com/artifact/io.github.nadeemiqbal/liquid-glass&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>compose</category>
      <category>android</category>
      <category>cmp</category>
    </item>
  </channel>
</rss>
