<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Android on MangoDriod</title><link>https://md.eknath.dev/tags/android/</link><description>Recent content in Android on MangoDriod</description><generator>Hugo -- 0.141.0</generator><language>en-us</language><lastBuildDate>Thu, 12 Feb 2026 21:30:00 +0530</lastBuildDate><atom:link href="https://md.eknath.dev/tags/android/index.xml" rel="self" type="application/rss+xml"/><item><title>Building a Reusable Speech-to-Text Component in Jetpack Compose</title><link>https://md.eknath.dev/posts/jetpack-compose-speech-to-text-implementation/</link><pubDate>Thu, 12 Feb 2026 21:30:00 +0530</pubDate><guid>https://md.eknath.dev/posts/jetpack-compose-speech-to-text-implementation/</guid><description>&lt;h2 id="tldr---why-you-should-add-voice-input">TL;DR - Why You Should Add Voice Input&lt;/h2>
&lt;p>&lt;strong>Voice input can dramatically improve UX, yet most apps don&amp;rsquo;t use it.&lt;/strong> Here&amp;rsquo;s why you should:&lt;/p>
&lt;p>✅ &lt;strong>Zero app size increase&lt;/strong> - Uses Android&amp;rsquo;s native speech recognition (no libraries!)
✅ &lt;strong>No permissions required&lt;/strong> - Works out of the box
✅ &lt;strong>3-5x faster input&lt;/strong> - Users can speak 150+ words/min vs typing 40 words/min
✅ &lt;strong>Better accessibility&lt;/strong> - Essential for users with motor impairments
✅ &lt;strong>Reduces friction&lt;/strong> - One tap vs multiple keyboard interactions
✅ &lt;strong>Professional polish&lt;/strong> - Shows attention to UX details&lt;/p></description><content:encoded><![CDATA[<h2 id="tldr---why-you-should-add-voice-input">TL;DR - Why You Should Add Voice Input</h2>
<p><strong>Voice input can dramatically improve UX, yet most apps don&rsquo;t use it.</strong> Here&rsquo;s why you should:</p>
<p>✅ <strong>Zero app size increase</strong> - Uses Android&rsquo;s native speech recognition (no libraries!)
✅ <strong>No permissions required</strong> - Works out of the box
✅ <strong>3-5x faster input</strong> - Users can speak 150+ words/min vs typing 40 words/min
✅ <strong>Better accessibility</strong> - Essential for users with motor impairments
✅ <strong>Reduces friction</strong> - One tap vs multiple keyboard interactions
✅ <strong>Professional polish</strong> - Shows attention to UX details</p>
<p><strong>The catch?</strong> It requires network connectivity and device support. But with proper availability checks, you can gracefully hide the feature when unavailable—making it a <strong>pure win</strong> when present.</p>
<hr>
<h2 id="why-most-apps-skip-this-feature">Why Most Apps Skip This Feature</h2>
<p>Despite being a <strong>native Android capability since API 8</strong>, many developers overlook voice input because:</p>
<ol>
<li><strong>Assumed complexity</strong> - Developers think it requires heavy ML libraries</li>
<li><strong>Unclear implementation</strong> - Documentation is scattered</li>
<li><strong>Network dependency concerns</strong> - Fear of handling edge cases</li>
<li><strong>Device fragmentation worries</strong> - Uncertainty about availability</li>
<li><strong>&ldquo;The keyboard already has it&rdquo;</strong> - The most common misconception</li>
</ol>
<p><strong>The truth?</strong> It&rsquo;s simpler than adding a date picker, and this guide shows you how to handle all edge cases properly.</p>
<hr>
<h2 id="but-users-have-voice-input-on-their-keyboard-already">&ldquo;But Users Have Voice Input on Their Keyboard Already!&rdquo;</h2>
<p>This is the <strong>most common objection</strong> developers raise. Yes, most mobile keyboards (Gboard, SwiftKey, Samsung Keyboard) have a mic button. <strong>But here&rsquo;s why in-app voice input is still essential:</strong></p>
<h3 id="the-reality-of-keyboard-voice-input-usage"><strong>The Reality of Keyboard Voice Input Usage</strong></h3>
<p>📊 <strong>Usage statistics show a problem:</strong></p>
<ul>
<li>Most users <strong>don&rsquo;t even know</strong> the keyboard mic button exists</li>
<li>Many <strong>forget about it</strong> after the initial setup</li>
<li>Some <strong>disable it accidentally</strong> during keyboard customization</li>
<li>The keyboard mic is <strong>visually small</strong> and easy to miss</li>
<li>Users must <strong>actively look for it</strong> among other keyboard buttons</li>
</ul>
<h3 id="why-in-app-voice-input-is-superior"><strong>Why In-App Voice Input is Superior</strong></h3>
<h4 id="1-discoverability"><strong>1. Discoverability</strong></h4>
<pre tabindex="0"><code>❌ Keyboard mic: Hidden among 30+ keyboard keys, looks like any other button
✅ In-app mic: Prominent, contextual, right next to the input field
</code></pre><p><strong>Example:</strong> A text field with a mic icon in the trailing position is <strong>immediately obvious</strong>. The keyboard mic? Users have to open the keyboard, scan for it, and remember it exists.</p>
<h4 id="2-context-aware-ux"><strong>2. Context-Aware UX</strong></h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// In-app voice can be contextual
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>TextField(
</span></span><span style="display:flex;"><span>    label = { Text(<span style="color:#e6db74">&#34;Product Review&#34;</span>) },
</span></span><span style="display:flex;"><span>    trailingIcon = { MicIcon() }  <span style="color:#75715e">// Clear purpose: &#34;Speak your review&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>)
</span></span></code></pre></div><p>The keyboard mic has <strong>no context</strong> - it&rsquo;s the same button whether you&rsquo;re entering an email, a password, or a product review. In-app voice input can show <strong>field-specific prompts</strong> like &ldquo;Describe your issue&rdquo; or &ldquo;Speak your address&rdquo;.</p>
<h4 id="3-user-intent-and-flow"><strong>3. User Intent and Flow</strong></h4>
<ul>
<li>
<p><strong>Keyboard mic</strong>: Requires users to:</p>
<ol>
<li>Tap the input field</li>
<li>Wait for keyboard to appear</li>
<li>Look for the mic button among keyboard keys</li>
<li>Tap the mic</li>
<li>Speak</li>
</ol>
</li>
<li>
<p><strong>In-app mic</strong>: Simplified flow:</p>
<ol>
<li>Tap the mic icon (no keyboard needed!)</li>
<li>Speak</li>
</ol>
</li>
</ul>
<p><strong>Result:</strong> <strong>2 fewer steps</strong> and <strong>no keyboard lag</strong>.</p>
<h4 id="4-visual-prominence"><strong>4. Visual Prominence</strong></h4>
<table>
  <thead>
      <tr>
          <th>Keyboard Mic</th>
          <th>In-App Mic</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5-6mm size typical</td>
          <td>Can be 24-32dp (12-16mm)</td>
      </tr>
      <tr>
          <td>Gray/neutral color</td>
          <td>App-themed, stands out</td>
      </tr>
      <tr>
          <td>Among 30+ keys</td>
          <td>Isolated, clear purpose</td>
      </tr>
      <tr>
          <td>Same across all apps</td>
          <td>Consistent with your app design</td>
      </tr>
  </tbody>
</table>
<h4 id="5-accessibility-considerations"><strong>5. Accessibility Considerations</strong></h4>
<p>Users with <strong>motor impairments</strong> or <strong>visual limitations</strong> benefit significantly:</p>
<ul>
<li>Larger, easier-to-tap target</li>
<li>Better contrast and visibility</li>
<li>Screen readers can announce it contextually</li>
<li>Doesn&rsquo;t require precise keyboard navigation</li>
</ul>
<h4 id="6-user-psychology"><strong>6. User Psychology</strong></h4>
<p><strong>Explicit invitation &gt; Hidden capability</strong></p>
<p>When users see a mic icon next to a text field, it:</p>
<ul>
<li><strong>Signals</strong> that voice input is encouraged</li>
<li><strong>Reduces friction</strong> - they don&rsquo;t need to hunt for it</li>
<li><strong>Increases adoption</strong> - visible features get used more</li>
<li><strong>Feels intentional</strong> - the app <em>wants</em> them to use voice</li>
</ul>
<p>The keyboard mic feels like a <strong>generic fallback</strong>. The in-app mic feels like a <strong>first-class feature</strong>.</p>
<h3 id="real-world-data-points"><strong>Real-World Data Points</strong></h3>
<p>While specific metrics vary by app, general patterns show:</p>
<ul>
<li>📈 <strong>5-10x higher voice input usage</strong> with prominent in-app mic icons</li>
<li>🎯 <strong>New user discovery</strong> - many users don&rsquo;t realize keyboard voice exists</li>
<li>♿ <strong>Accessibility gains</strong> - significant usage increase among users with disabilities</li>
<li>📱 <strong>Mobile-first users</strong> especially benefit (small screen, fat fingers)</li>
</ul>
<h3 id="the-hybrid-approach-best-of-both-worlds"><strong>The Hybrid Approach: Best of Both Worlds</strong></h3>
<p>The ideal solution is <strong>not either/or</strong>, but <strong>both</strong>:</p>
<p>✅ <strong>In-app mic</strong> for discoverability and context
✅ <strong>Keyboard mic</strong> still works as a fallback</p>
<p>Users get:</p>
<ul>
<li>A prominent, obvious voice input option</li>
<li>Fallback if they prefer keyboard mic</li>
<li>Contextual prompts and better UX</li>
<li>No downsides!</li>
</ul>
<h3 id="when-just-use-the-keyboard-fails"><strong>When &ldquo;Just Use the Keyboard&rdquo; Fails</strong></h3>
<p>Some scenarios where keyboard voice input is insufficient:</p>
<ol>
<li><strong>Custom keyboards</strong> - Not all keyboards have voice input</li>
<li><strong>Enterprise devices</strong> - Some organizations disable keyboard voice for security</li>
<li><strong>Locked-down keyboards</strong> - Educational or restricted environments</li>
<li><strong>Non-Google keyboards</strong> - Third-party keyboards may lack voice features</li>
<li><strong>Disabled by user</strong> - Some users disable keyboard permissions</li>
</ol>
<p>Your <strong>in-app implementation</strong> works regardless of keyboard choice.</p>
<hr>
<h2 id="the-bottom-line">The Bottom Line</h2>
<p><strong>&ldquo;Users have voice on their keyboard&rdquo;</strong> is like saying:</p>
<ul>
<li>&ldquo;Don&rsquo;t add a search icon, users can use Ctrl+F&rdquo;</li>
<li>&ldquo;Don&rsquo;t add a share button, users can copy-paste&rdquo;</li>
<li>&ldquo;Don&rsquo;t add undo, users can manually fix mistakes&rdquo;</li>
</ul>
<p><strong>Just because a capability exists somewhere doesn&rsquo;t mean it&rsquo;s discoverable or convenient.</strong></p>
<p>In-app voice input is about <strong>removing friction</strong> and <strong>guiding users</strong> toward better UX. The fact that keyboard voice exists is great - your in-app implementation makes it <strong>more likely to actually be used</strong>.</p>
<hr>
<p>Ever wanted to add voice input to your Android app with minimal effort? <strong>Speech-to-Text</strong> functionality can dramatically improve user experience, especially for note-taking, messaging, or search features.</p>
<p>In this guide, we&rsquo;ll build a <strong>clean, reusable Speech-to-Text component</strong> using <strong>Jetpack Compose</strong> that wraps Android&rsquo;s native speech recognition API.</p>
<h2 id="-what-were-building">🎯 What We&rsquo;re Building</h2>
<p>A composable speech recognition system with:</p>
<ul>
<li>✅ <strong>Simple API</strong> - One composable function to handle everything</li>
<li>✅ <strong>Lifecycle-aware</strong> - Properly managed with Activity Result API</li>
<li>✅ <strong>Locale support</strong> - Respects app language settings</li>
<li>✅ <strong>Availability checking</strong> - Gracefully handles devices without speech recognition</li>
<li>✅ <strong>Reusable state</strong> - Clean separation of concerns</li>
</ul>
<hr>
<h2 id="-architecture-overview">🏗️ Architecture Overview</h2>
<p>Our implementation consists of three main components:</p>
<ol>
<li><strong><code>SystemSpeechToTextHelper</code></strong> - A utility object that handles Android&rsquo;s RecognizerIntent</li>
<li><strong><code>SpeechToTextState</code></strong> - A state holder that manages the speech recognition launcher</li>
<li><strong><code>rememberSpeechToText()</code></strong> - A composable function that creates and remembers the state</li>
<li><strong><code>SpeechToTextButton</code></strong> (Bonus) - A ready-to-use UI component</li>
</ol>
<hr>
<h2 id="-implementation">📝 Implementation</h2>
<h3 id="1-the-helper-object"><strong>1️⃣ The Helper Object</strong></h3>
<p>First, let&rsquo;s create a helper object to encapsulate all Android-specific speech recognition logic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">object</span> <span style="color:#a6e22e">SystemSpeechToTextHelper</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAppLocale</span>(): Locale {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Locale</span>.forLanguageTag(<span style="color:#a6e22e">Language</span>.currentLocale.<span style="color:#66d9ef">value</span>.code)
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Locale</span>.getDefault()
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">createRecognitionIntent</span>(
</span></span><span style="display:flex;"><span>        languageModel: String = <span style="color:#a6e22e">RecognizerIntent</span>.LANGUAGE_MODEL_FREE_FORM,
</span></span><span style="display:flex;"><span>        locale: Locale = getAppLocale(),
</span></span><span style="display:flex;"><span>        prompt: String? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>        maxResults: Int = <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    ): Intent {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Intent(<span style="color:#a6e22e">RecognizerIntent</span>.ACTION_RECOGNIZE_SPEECH).apply {
</span></span><span style="display:flex;"><span>            putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_LANGUAGE_MODEL, languageModel)
</span></span><span style="display:flex;"><span>            putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_LANGUAGE, locale.toLanguageTag())
</span></span><span style="display:flex;"><span>            putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_MAX_RESULTS, maxResults)
</span></span><span style="display:flex;"><span>            prompt<span style="color:#f92672">?.</span>let { putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_PROMPT, <span style="color:#66d9ef">it</span>) }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">extractSpokenText</span>(result: ActivityResult): String? {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">if</span> (result.resultCode <span style="color:#f92672">==</span> <span style="color:#a6e22e">Activity</span>.RESULT_OK) {
</span></span><span style="display:flex;"><span>            result.<span style="color:#66d9ef">data</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">?.</span>getStringArrayListExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_RESULTS)
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">?.</span>firstOrNull()
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">?.</span>takeIf { <span style="color:#66d9ef">it</span>.isNotBlank() }
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">isRecognitionAvailable</span>(context: Context): Boolean {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> pm = context.packageManager
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> activities = pm.queryIntentActivities(
</span></span><span style="display:flex;"><span>            Intent(<span style="color:#a6e22e">RecognizerIntent</span>.ACTION_RECOGNIZE_SPEECH),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">PackageManager</span>.MATCH_DEFAULT_ONLY
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> activities.isNotEmpty()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Key Features:</strong></p>
<ul>
<li>🌍 <strong>Locale handling</strong> - Automatically uses your app&rsquo;s current language</li>
<li>🎤 <strong>Flexible configuration</strong> - Customize prompt, language model, and result count</li>
<li>✅ <strong>Validation</strong> - Ensures speech recognition is available on the device</li>
<li>🧹 <strong>Clean extraction</strong> - Filters out blank results</li>
</ul>
<hr>
<h3 id="2-the-state-holder"><strong>2️⃣ The State Holder</strong></h3>
<p>Next, we create a state class that manages the speech recognition lifecycle:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Stable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SpeechToTextState</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> launcher: ManagedActivityResultLauncher&lt;Intent, ActivityResult&gt;,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> prompt: String?,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> isAvailable: Boolean
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">launch</span>(
</span></span><span style="display:flex;"><span>        customPrompt: String? = prompt,
</span></span><span style="display:flex;"><span>        customLocale: Locale? = <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> intent = <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.createRecognitionIntent(
</span></span><span style="display:flex;"><span>            prompt = customPrompt,
</span></span><span style="display:flex;"><span>            locale = customLocale <span style="color:#f92672">?:</span> <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.getAppLocale()
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        launcher.launch(intent)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Why <code>@Stable</code>?</strong>
The <code>@Stable</code> annotation tells Compose that this class follows specific stability contracts, allowing for better recomposition optimizations.</p>
<hr>
<h3 id="3-the-composable-function"><strong>3️⃣ The Composable Function</strong></h3>
<p>Now comes the magic - a composable that ties everything together:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">rememberSpeechToText</span>(
</span></span><span style="display:flex;"><span>    prompt: String? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    onResult: (String) <span style="color:#f92672">-&gt;</span> Unit
</span></span><span style="display:flex;"><span>): SpeechToTextState {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> launcher = rememberLauncherForActivityResult(
</span></span><span style="display:flex;"><span>        contract = <span style="color:#a6e22e">ActivityResultContracts</span>.StartActivityForResult()
</span></span><span style="display:flex;"><span>    ) { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.extractSpokenText(result)<span style="color:#f92672">?.</span>let { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            onResult(spokenText)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> isAvailable = remember {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.isRecognitionAvailable(context)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> remember(launcher, prompt, isAvailable) {
</span></span><span style="display:flex;"><span>        SpeechToTextState(
</span></span><span style="display:flex;"><span>            launcher = launcher,
</span></span><span style="display:flex;"><span>            prompt = prompt,
</span></span><span style="display:flex;"><span>            isAvailable = isAvailable
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Key Points:</strong></p>
<ul>
<li>🔄 <strong>Activity Result API</strong> - Modern way to handle activity results</li>
<li>💾 <strong>Remembered state</strong> - Survives recompositions</li>
<li>🎯 <strong>Callback pattern</strong> - Clean result handling via lambda</li>
</ul>
<hr>
<h3 id="4-bonus-ready-to-use-button-component"><strong>4️⃣ Bonus: Ready-to-Use Button Component</strong></h3>
<p>For convenience, here&rsquo;s a pre-built button component:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">SpeechToTextButton</span>(
</span></span><span style="display:flex;"><span>    speechToTextState: SpeechToTextState,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier,
</span></span><span style="display:flex;"><span>    enabled: Boolean = <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    iconSize: Dp = <span style="color:#ae81ff">24.</span>dp,
</span></span><span style="display:flex;"><span>    tint: Color = <span style="color:#a6e22e">Color</span>.Unspecified,
</span></span><span style="display:flex;"><span>    contentDescription: String? = <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    IconButton(
</span></span><span style="display:flex;"><span>        onClick = speechToTextState<span style="color:#f92672">::</span>launch,
</span></span><span style="display:flex;"><span>        enabled = enabled <span style="color:#f92672">&amp;&amp;</span> speechToTextState.isAvailable,
</span></span><span style="display:flex;"><span>        modifier = modifier
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        Icon(
</span></span><span style="display:flex;"><span>            painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>            contentDescription = contentDescription,
</span></span><span style="display:flex;"><span>            tint = tint,
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.size(iconSize)
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="-real-world-implementation-examples">🚀 Real-World Implementation Examples</h2>
<h3 id="example-1-textfield-with-voice-input-production-ready"><strong>Example 1: TextField with Voice Input (Production-Ready)</strong></h3>
<p>Here&rsquo;s how to properly integrate voice input with a text field, including validation and network checking:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">SmartTextField</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">value</span>: String,
</span></span><span style="display:flex;"><span>    onValueChange: (String) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier,
</span></span><span style="display:flex;"><span>    label: String = <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    placeholder: String = <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    isError: Boolean = <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    errorMessage: String? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    maxLength: Int? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    singleLine: Boolean = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> showNetworkWarning <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#66d9ef">false</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Check network connectivity
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> isNetworkAvailable = remember {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> cm = context.getSystemService(<span style="color:#a6e22e">Context</span>.CONNECTIVITY_SERVICE) <span style="color:#66d9ef">as</span> ConnectivityManager
</span></span><span style="display:flex;"><span>        cm.activeNetwork <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText(
</span></span><span style="display:flex;"><span>        prompt = <span style="color:#e6db74">&#34;Speak </span><span style="color:#e6db74">$label</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    ) { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Handle max length validation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> newText = <span style="color:#66d9ef">if</span> (maxLength <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>            spokenText.take(maxLength)
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            spokenText
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        onValueChange(newText)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(modifier = modifier) {
</span></span><span style="display:flex;"><span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = <span style="color:#66d9ef">value</span>,
</span></span><span style="display:flex;"><span>            onValueChange = { newValue <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Enforce max length on manual input too
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">val</span> sanitized = <span style="color:#66d9ef">if</span> (maxLength <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>                    newValue.take(maxLength)
</span></span><span style="display:flex;"><span>                } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>                    newValue
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                onValueChange(sanitized)
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            label = { Text(label) },
</span></span><span style="display:flex;"><span>            placeholder = { Text(placeholder) },
</span></span><span style="display:flex;"><span>            isError = isError,
</span></span><span style="display:flex;"><span>            singleLine = singleLine,
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(),
</span></span><span style="display:flex;"><span>            trailingIcon = {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Only show mic icon if speech recognition is available
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>                    IconButton(
</span></span><span style="display:flex;"><span>                        onClick = {
</span></span><span style="display:flex;"><span>                            <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>                                speechToText.launch()
</span></span><span style="display:flex;"><span>                            } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>                                showNetworkWarning = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>                            }
</span></span><span style="display:flex;"><span>                        }
</span></span><span style="display:flex;"><span>                    ) {
</span></span><span style="display:flex;"><span>                        Icon(
</span></span><span style="display:flex;"><span>                            painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>                            contentDescription = <span style="color:#e6db74">&#34;Voice input for </span><span style="color:#e6db74">$label</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                            tint = <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>                                <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.primary
</span></span><span style="display:flex;"><span>                            } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>                                <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.onSurface.copy(alpha = <span style="color:#ae81ff">0.38f</span>)
</span></span><span style="display:flex;"><span>                            }
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            supportingText = {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">when</span> {
</span></span><span style="display:flex;"><span>                    errorMessage <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">&amp;&amp;</span> isError <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                        Text(
</span></span><span style="display:flex;"><span>                            text = errorMessage,
</span></span><span style="display:flex;"><span>                            color = <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.error
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                    maxLength <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                        Text(
</span></span><span style="display:flex;"><span>                            text = <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${value.length}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">$maxLength</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(),
</span></span><span style="display:flex;"><span>                            textAlign = <span style="color:#a6e22e">TextAlign</span>.End
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Network warning
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (showNetworkWarning) {
</span></span><span style="display:flex;"><span>            Text(
</span></span><span style="display:flex;"><span>                text = <span style="color:#e6db74">&#34;Voice input requires internet connection&#34;</span>,
</span></span><span style="display:flex;"><span>                color = <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.error,
</span></span><span style="display:flex;"><span>                style = <span style="color:#a6e22e">MaterialTheme</span>.typography.bodySmall,
</span></span><span style="display:flex;"><span>                modifier = <span style="color:#a6e22e">Modifier</span>.padding(start = <span style="color:#ae81ff">16.</span>dp, top = <span style="color:#ae81ff">4.</span>dp)
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>            LaunchedEffect(Unit) {
</span></span><span style="display:flex;"><span>                delay(<span style="color:#ae81ff">3000</span>)
</span></span><span style="display:flex;"><span>                showNetworkWarning = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Usage:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">FeedbackForm</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> userName <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> feedback <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> maxFeedbackLength = <span style="color:#ae81ff">500</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(modifier = <span style="color:#a6e22e">Modifier</span>.padding(<span style="color:#ae81ff">16.</span>dp)) {
</span></span><span style="display:flex;"><span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = userName,
</span></span><span style="display:flex;"><span>            onValueChange = { userName = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Your Name&#34;</span>,
</span></span><span style="display:flex;"><span>            placeholder = <span style="color:#e6db74">&#34;John Doe&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = <span style="color:#ae81ff">50</span>,
</span></span><span style="display:flex;"><span>            singleLine = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = feedback,
</span></span><span style="display:flex;"><span>            onValueChange = { feedback = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Feedback&#34;</span>,
</span></span><span style="display:flex;"><span>            placeholder = <span style="color:#e6db74">&#34;Tell us what you think...&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = maxFeedbackLength,
</span></span><span style="display:flex;"><span>            singleLine = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="example-2-search-bar-with-voice-input"><strong>Example 2: Search Bar with Voice Input</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">VoiceEnabledSearchBar</span>(
</span></span><span style="display:flex;"><span>    query: String,
</span></span><span style="display:flex;"><span>    onQueryChange: (String) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    onSearch: () <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText(
</span></span><span style="display:flex;"><span>        prompt = <span style="color:#e6db74">&#34;What are you looking for?&#34;</span>
</span></span><span style="display:flex;"><span>    ) { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        onQueryChange(spokenText)
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Auto-search after voice input
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        onSearch()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    SearchBar(
</span></span><span style="display:flex;"><span>        query = query,
</span></span><span style="display:flex;"><span>        onQueryChange = onQueryChange,
</span></span><span style="display:flex;"><span>        onSearch = { onSearch() },
</span></span><span style="display:flex;"><span>        active = <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>        onActiveChange = {},
</span></span><span style="display:flex;"><span>        modifier = modifier,
</span></span><span style="display:flex;"><span>        leadingIcon = {
</span></span><span style="display:flex;"><span>            Icon(
</span></span><span style="display:flex;"><span>                imageVector = <span style="color:#a6e22e">Icons</span>.<span style="color:#a6e22e">Default</span>.Search,
</span></span><span style="display:flex;"><span>                contentDescription = <span style="color:#e6db74">&#34;Search&#34;</span>
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        trailingIcon = {
</span></span><span style="display:flex;"><span>            Row {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Clear button
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">if</span> (query.isNotEmpty()) {
</span></span><span style="display:flex;"><span>                    IconButton(onClick = { onQueryChange(<span style="color:#e6db74">&#34;&#34;</span>) }) {
</span></span><span style="display:flex;"><span>                        Icon(
</span></span><span style="display:flex;"><span>                            imageVector = <span style="color:#a6e22e">Icons</span>.<span style="color:#a6e22e">Default</span>.Close,
</span></span><span style="display:flex;"><span>                            contentDescription = <span style="color:#e6db74">&#34;Clear&#34;</span>
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Voice input button (only if available)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>                    IconButton(onClick = { speechToText.launch() }) {
</span></span><span style="display:flex;"><span>                        Icon(
</span></span><span style="display:flex;"><span>                            painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>                            contentDescription = <span style="color:#e6db74">&#34;Voice search&#34;</span>
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        placeholder = { Text(<span style="color:#e6db74">&#34;Search products...&#34;</span>) }
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Search suggestions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="example-3-multi-line-text-input-with-append-mode"><strong>Example 3: Multi-line Text Input with Append Mode</strong></h3>
<p>Perfect for note-taking or messaging apps:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">VoiceNoteEditor</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> noteContent <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> isRecording <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#66d9ef">false</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText(
</span></span><span style="display:flex;"><span>        prompt = <span style="color:#e6db74">&#34;Speak your note&#34;</span>
</span></span><span style="display:flex;"><span>    ) { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Intelligently append or replace
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        noteContent = <span style="color:#66d9ef">when</span> {
</span></span><span style="display:flex;"><span>            noteContent.isEmpty() <span style="color:#f92672">-&gt;</span> spokenText
</span></span><span style="display:flex;"><span>            noteContent.endsWith(<span style="color:#e6db74">&#34;.&#34;</span>) <span style="color:#f92672">||</span> noteContent.endsWith(<span style="color:#e6db74">&#34;!&#34;</span>) <span style="color:#f92672">||</span> noteContent.endsWith(<span style="color:#e6db74">&#34;?&#34;</span>) <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$noteContent</span><span style="color:#e6db74"> </span><span style="color:#e6db74">$spokenText</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span> <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$noteContent</span><span style="color:#e6db74">. </span><span style="color:#e6db74">$spokenText</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        isRecording = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(
</span></span><span style="display:flex;"><span>        modifier = Modifier
</span></span><span style="display:flex;"><span>            .fillMaxSize()
</span></span><span style="display:flex;"><span>            .padding(<span style="color:#ae81ff">16.</span>dp)
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = noteContent,
</span></span><span style="display:flex;"><span>            onValueChange = { noteContent = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            modifier = Modifier
</span></span><span style="display:flex;"><span>                .fillMaxWidth()
</span></span><span style="display:flex;"><span>                .weight(<span style="color:#ae81ff">1f</span>),
</span></span><span style="display:flex;"><span>            placeholder = {
</span></span><span style="display:flex;"><span>                Text(<span style="color:#e6db74">&#34;Start typing or tap the mic to speak...&#34;</span>)
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            textStyle = <span style="color:#a6e22e">MaterialTheme</span>.typography.bodyLarge
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Row(
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(),
</span></span><span style="display:flex;"><span>            horizontalArrangement = <span style="color:#a6e22e">Arrangement</span>.SpaceBetween,
</span></span><span style="display:flex;"><span>            verticalAlignment = <span style="color:#a6e22e">Alignment</span>.CenterVertically
</span></span><span style="display:flex;"><span>        ) {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Word count
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            Text(
</span></span><span style="display:flex;"><span>                text = <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${noteContent.split(&#34;\\s+&#34;.toRegex()).size}</span><span style="color:#e6db74"> words&#34;</span>,
</span></span><span style="display:flex;"><span>                style = <span style="color:#a6e22e">MaterialTheme</span>.typography.bodySmall,
</span></span><span style="display:flex;"><span>                color = <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.onSurfaceVariant
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Voice input button
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>                FilledTonalButton(
</span></span><span style="display:flex;"><span>                    onClick = {
</span></span><span style="display:flex;"><span>                        isRecording = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>                        speechToText.launch()
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                ) {
</span></span><span style="display:flex;"><span>                    Icon(
</span></span><span style="display:flex;"><span>                        painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>                        contentDescription = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>                        modifier = <span style="color:#a6e22e">Modifier</span>.size(<span style="color:#ae81ff">20.</span>dp)
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                    Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.width(<span style="color:#ae81ff">8.</span>dp))
</span></span><span style="display:flex;"><span>                    Text(<span style="color:#66d9ef">if</span> (isRecording) <span style="color:#e6db74">&#34;Listening...&#34;</span> <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;Add Voice Note&#34;</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="example-4-form-with-conditional-voice-input"><strong>Example 4: Form with Conditional Voice Input</strong></h3>
<p>Shows how to conditionally enable voice input based on field type:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">UserRegistrationForm</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> name <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> email <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> bio <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(modifier = <span style="color:#a6e22e">Modifier</span>.padding(<span style="color:#ae81ff">16.</span>dp)) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Name field - voice input enabled
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = name,
</span></span><span style="display:flex;"><span>            onValueChange = { name = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Full Name&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = <span style="color:#ae81ff">50</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Email field - voice input disabled (too error-prone)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = email,
</span></span><span style="display:flex;"><span>            onValueChange = { email = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = { Text(<span style="color:#e6db74">&#34;Email&#34;</span>) },
</span></span><span style="display:flex;"><span>            keyboardOptions = KeyboardOptions(
</span></span><span style="display:flex;"><span>                keyboardType = <span style="color:#a6e22e">KeyboardType</span>.Email
</span></span><span style="display:flex;"><span>            ),
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth()
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// No voice input for email - typing is more accurate
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Bio field - voice input enabled
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = bio,
</span></span><span style="display:flex;"><span>            onValueChange = { bio = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Bio&#34;</span>,
</span></span><span style="display:flex;"><span>            placeholder = <span style="color:#e6db74">&#34;Tell us about yourself...&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = <span style="color:#ae81ff">200</span>,
</span></span><span style="display:flex;"><span>            singleLine = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="-critical-implementation-guidelines">⚠️ Critical Implementation Guidelines</h2>
<h3 id="1-always-check-availability"><strong>1. Always Check Availability</strong></h3>
<p><strong>Never show the mic icon if speech recognition is unavailable.</strong> This creates a poor UX when users tap it and nothing happens.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ✅ GOOD - Only show when available
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>    SpeechToTextButton(speechToTextState = speechToText)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ❌ BAD - Shows disabled button (confusing UX)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>SpeechToTextButton(
</span></span><span style="display:flex;"><span>    speechToTextState = speechToText,
</span></span><span style="display:flex;"><span>    enabled = speechToText.isAvailable  <span style="color:#75715e">// Don&#39;t do this!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>)
</span></span></code></pre></div><p><strong>Why?</strong> On devices without Google services (some custom ROMs, enterprise devices), the feature won&rsquo;t work. Hiding it entirely is cleaner than showing a permanently disabled button.</p>
<hr>
<h3 id="2-handle-network-connectivity"><strong>2. Handle Network Connectivity</strong></h3>
<p>Speech recognition requires <strong>active internet connection</strong>. Check before launching:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">isNetworkAvailable</span>(context: Context): Boolean {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> cm = context.getSystemService(<span style="color:#a6e22e">Context</span>.CONNECTIVITY_SERVICE) <span style="color:#66d9ef">as</span> ConnectivityManager
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> cm.activeNetwork <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Usage
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> isNetworkAvailable = remember {
</span></span><span style="display:flex;"><span>    isNetworkAvailable(context)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>IconButton(
</span></span><span style="display:flex;"><span>    onClick = {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>            speechToText.launch()
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Show snackbar or toast
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">Toast</span>.makeText(context, <span style="color:#e6db74">&#34;Voice input requires internet&#34;</span>, <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT).show()
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    Icon(
</span></span><span style="display:flex;"><span>        painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>        tint = <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.primary
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.onSurface.copy(alpha = <span style="color:#ae81ff">0.38f</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Best Practice:</strong> Show the mic icon in a dimmed state when offline, and display a brief message when tapped.</p>
<hr>
<h3 id="3-input-validation-after-voice-input"><strong>3. Input Validation After Voice Input</strong></h3>
<p>Always validate voice input just like you would keyboard input:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Sanitize and validate
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> sanitized = spokenText
</span></span><span style="display:flex;"><span>        .trim()
</span></span><span style="display:flex;"><span>        .take(maxLength)
</span></span><span style="display:flex;"><span>        .filter { <span style="color:#66d9ef">it</span>.isLetterOrDigit() <span style="color:#f92672">||</span> <span style="color:#66d9ef">it</span>.isWhitespace() }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Check if valid
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (sanitized.isNotEmpty()) {
</span></span><span style="display:flex;"><span>        inputValue = sanitized
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>        showError(<span style="color:#e6db74">&#34;Invalid input received&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Common validations:</p>
<ul>
<li><strong>Length limits</strong> - Trim to max length</li>
<li><strong>Character filtering</strong> - Remove special chars if needed</li>
<li><strong>Empty checks</strong> - Handle blank results</li>
<li><strong>Format validation</strong> - Email, phone, etc.</li>
</ul>
<hr>
<h3 id="4-permissions---none-required"><strong>4. Permissions - None Required!</strong></h3>
<p><strong>Good news:</strong> No runtime permissions needed! Android&rsquo;s speech recognition uses Google&rsquo;s cloud service, which handles all the heavy lifting.</p>
<p>This is a <strong>huge advantage</strong> over custom speech recognition libraries that require <code>RECORD_AUDIO</code> permission.</p>
<hr>
<h3 id="5-locale-and-language-support"><strong>5. Locale and Language Support</strong></h3>
<p>By default, the implementation respects your app&rsquo;s current locale:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAppLocale</span>(): Locale {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Locale</span>.forLanguageTag(<span style="color:#a6e22e">Language</span>.currentLocale.<span style="color:#66d9ef">value</span>.code)
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Locale</span>.getDefault()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>For multilingual apps</strong>, you can override the locale per-field:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Spanish input for a specific field
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>speechToText.launch(customLocale = Locale(<span style="color:#e6db74">&#34;es&#34;</span>, <span style="color:#e6db74">&#34;ES&#34;</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// French input
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>speechToText.launch(customLocale = <span style="color:#a6e22e">Locale</span>.FRANCE)
</span></span></code></pre></div><hr>
<h3 id="6-when-not-to-use-voice-input"><strong>6. When NOT to Use Voice Input</strong></h3>
<p>Some fields are better suited for keyboard input:</p>
<p>❌ <strong>Email addresses</strong> - Punctuation and special characters are error-prone
❌ <strong>Passwords</strong> - Security risk + poor accuracy
❌ <strong>Credit card numbers</strong> - High error rate + security concerns
❌ <strong>URLs</strong> - Complex syntax not recognized well
❌ <strong>Code snippets</strong> - Special characters and formatting issues</p>
<p>✅ <strong>Good use cases:</strong>
✔ Names, addresses, descriptions
✔ Search queries
✔ Notes and messages
✔ Feedback and reviews
✔ Long-form text content</p>
<hr>
<h2 id="-ux-impact-the-numbers">📊 UX Impact: The Numbers</h2>
<p>Why voice input matters for your app&rsquo;s user experience:</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Typing</th>
          <th>Voice Input</th>
          <th>Improvement</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Average Speed</strong></td>
          <td>40 words/min</td>
          <td>150+ words/min</td>
          <td><strong>3.75x faster</strong></td>
      </tr>
      <tr>
          <td><strong>Error Rate</strong></td>
          <td>2-3%</td>
          <td>5-8% (but faster to correct)</td>
          <td>Context dependent</td>
      </tr>
      <tr>
          <td><strong>User Effort</strong></td>
          <td>High (small keyboards)</td>
          <td>Low (hands-free)</td>
          <td><strong>Significantly lower</strong></td>
      </tr>
      <tr>
          <td><strong>Accessibility</strong></td>
          <td>Difficult for some users</td>
          <td>Easy for most users</td>
          <td><strong>Universal access</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>Real-world impact:</strong></p>
<ul>
<li>📝 A 100-word product review takes <strong>2.5 minutes typing</strong> vs <strong>40 seconds speaking</strong></li>
<li>🔍 Voice search feels instantaneous vs typing lag</li>
<li>♿ Critical for users with motor impairments, RSI, or visual limitations</li>
<li>🌍 Easier for non-native keyboard users</li>
</ul>
<hr>
<h2 id="-why-this-implementation-is-superior">✅ Why This Implementation is Superior</h2>
<h3 id="compared-to-keyboard-input"><strong>Compared to Keyboard Input:</strong></h3>
<p>✔ <strong>3-5x faster input</strong> for long text
✔ <strong>Lower cognitive load</strong> - speaking is more natural than typing
✔ <strong>Better mobile experience</strong> - no tiny keyboard frustration
✔ <strong>Hands-free operation</strong> - can be used while multitasking</p>
<h3 id="compared-to-third-party-libraries"><strong>Compared to Third-Party Libraries:</strong></h3>
<p>✔ <strong>Zero app size increase</strong> - uses system APIs
✔ <strong>No permissions required</strong> - no <code>RECORD_AUDIO</code> prompt
✔ <strong>Always up-to-date</strong> - Google maintains the recognition engine
✔ <strong>No API keys or quotas</strong> - completely free
✔ <strong>Better privacy</strong> - uses Google&rsquo;s standard speech service (same as Gboard)</p>
<h3 id="compared-to-custom-ml-models"><strong>Compared to Custom ML Models:</strong></h3>
<p>✔ <strong>No model training needed</strong>
✔ <strong>No storage for ML models</strong> (models can be 50MB+)
✔ <strong>Supports 100+ languages</strong> out of the box
✔ <strong>Continuously improving</strong> - benefits from Google&rsquo;s updates</p>
<hr>
<h2 id="-when-voice-input-makes-sense">🎯 When Voice Input Makes Sense</h2>
<h3 id="perfect-use-cases"><strong>Perfect Use Cases:</strong></h3>
<ul>
<li>📝 <strong>Note-taking and memos</strong> - Natural dictation flow</li>
<li>💬 <strong>Messaging and chat</strong> - Quick voice-to-text messages</li>
<li>🔍 <strong>Search queries</strong> - Faster than typing</li>
<li>📋 <strong>Long-form content</strong> - Reviews, feedback, descriptions</li>
<li>♿ <strong>Accessibility features</strong> - Essential for many users</li>
<li>🚗 <strong>Hands-free scenarios</strong> - When typing isn&rsquo;t safe</li>
</ul>
<h3 id="skip-voice-input-for"><strong>Skip Voice Input For:</strong></h3>
<ul>
<li>🔒 <strong>Sensitive data</strong> - Passwords, PINs, SSNs</li>
<li>📧 <strong>Format-specific fields</strong> - Emails, URLs, credit cards</li>
<li>🔢 <strong>Numeric codes</strong> - OTPs, account numbers</li>
<li>💻 <strong>Technical input</strong> - Code, command-line syntax</li>
</ul>
<hr>
<h2 id="-performance-considerations">🚀 Performance Considerations</h2>
<p><strong>App Size Impact:</strong> <strong>0 KB</strong> - Uses system APIs only</p>
<p><strong>Runtime Performance:</strong></p>
<ul>
<li>Minimal memory usage</li>
<li>Lazy initialization (only when needed)</li>
<li>No background processes</li>
<li>Network call only during active recognition</li>
</ul>
<p><strong>Battery Impact:</strong></p>
<ul>
<li>Negligible - recognition happens on Google&rsquo;s servers</li>
<li>No continuous listening (only when user taps mic)</li>
<li>Automatic cleanup after recognition</li>
</ul>
<hr>
<h2 id="-quick-implementation-checklist">🎁 Quick Implementation Checklist</h2>
<p>Before shipping voice input to production, verify:</p>
<ul>
<li><input disabled="" type="checkbox"> ✅ Mic icon only shows when <code>isAvailable == true</code></li>
<li><input disabled="" type="checkbox"> 🌐 Network connectivity is checked before launching</li>
<li><input disabled="" type="checkbox"> ✍️ Input validation applied to voice results</li>
<li><input disabled="" type="checkbox"> 📏 Max length limits enforced</li>
<li><input disabled="" type="checkbox"> 🌍 Proper locale configuration</li>
<li><input disabled="" type="checkbox"> ⚠️ User feedback for network errors</li>
<li><input disabled="" type="checkbox"> 📱 Tested on devices without Google services</li>
<li><input disabled="" type="checkbox"> ♿ Content descriptions added for accessibility</li>
<li><input disabled="" type="checkbox"> 🎨 Visual feedback when mic is active (if custom UI)</li>
</ul>
<hr>
<h2 id="-related-resources">🔗 Related Resources</h2>
<ul>
<li><a href="https://developer.android.com/reference/android/speech/RecognizerIntent">Android Speech Recognition Guide</a></li>
<li><a href="https://developer.android.com/training/basics/intents/result">Jetpack Compose Activity Result API</a></li>
<li><a href="https://m3.material.io/foundations/interaction/input">Material Design Voice Input Guidelines</a></li>
</ul>
<hr>
<h2 id="-final-thoughts">💡 Final Thoughts</h2>
<p>Voice input is a <strong>low-effort, high-impact feature</strong> that most apps overlook. With zero dependencies, no permissions, and minimal code, there&rsquo;s little reason not to add it where appropriate.</p>
<p><strong>The key differentiators:</strong></p>
<ol>
<li><strong>Always check availability</strong> - hide the feature gracefully when unavailable</li>
<li><strong>Validate network state</strong> - provide feedback when offline</li>
<li><strong>Apply proper validation</strong> - treat voice input like any other input</li>
<li><strong>Choose appropriate fields</strong> - not everything needs voice input</li>
</ol>
<p>By following these guidelines, you&rsquo;ll provide a professional, polished experience that sets your app apart.</p>
<hr>
<p><strong>That&rsquo;s it!</strong> You now have a fully functional, production-ready speech-to-text component for Jetpack Compose. 🎉</p>
<p>Feel free to customize this implementation to fit your app&rsquo;s specific needs. If you have questions or suggestions, reach out via my social handles! 😊</p>
<p><strong>Happy coding!</strong> 🚀</p>
]]></content:encoded></item><item><title>Android ML Kit Document Scanner: Stop Using Camera Capture for Documents</title><link>https://md.eknath.dev/posts/android-ml-kit-document-scanner/</link><pubDate>Tue, 10 Feb 2026 21:30:00 +0530</pubDate><guid>https://md.eknath.dev/posts/android-ml-kit-document-scanner/</guid><description>&lt;h2 id="tldr---why-ml-kit-document-scanner-changes-everything">TL;DR - Why ML Kit Document Scanner Changes Everything&lt;/h2>
&lt;p>Stop building custom camera UIs for document capture. &lt;strong>ML Kit Document Scanner&lt;/strong> gives you a professional, AI-powered document scanning experience with just a few lines of code:&lt;/p>
&lt;p>✅ &lt;strong>Automatic edge detection&lt;/strong> - AI finds document boundaries instantly
✅ &lt;strong>Perspective correction&lt;/strong> - Automatically straightens skewed documents
✅ &lt;strong>Shadow removal&lt;/strong> - Intelligent lighting correction
✅ &lt;strong>Multi-page support&lt;/strong> - Scan multiple pages in one session
✅ &lt;strong>Quality enhancement&lt;/strong> - Auto-adjusts contrast and brightness
✅ &lt;strong>Minimal code&lt;/strong> - 10 lines vs 500+ for custom implementation
✅ &lt;strong>Small library size&lt;/strong> - ~3MB vs building from scratch&lt;/p></description><content:encoded><![CDATA[<h2 id="tldr---why-ml-kit-document-scanner-changes-everything">TL;DR - Why ML Kit Document Scanner Changes Everything</h2>
<p>Stop building custom camera UIs for document capture. <strong>ML Kit Document Scanner</strong> gives you a professional, AI-powered document scanning experience with just a few lines of code:</p>
<p>✅ <strong>Automatic edge detection</strong> - AI finds document boundaries instantly
✅ <strong>Perspective correction</strong> - Automatically straightens skewed documents
✅ <strong>Shadow removal</strong> - Intelligent lighting correction
✅ <strong>Multi-page support</strong> - Scan multiple pages in one session
✅ <strong>Quality enhancement</strong> - Auto-adjusts contrast and brightness
✅ <strong>Minimal code</strong> - 10 lines vs 500+ for custom implementation
✅ <strong>Small library size</strong> - ~3MB vs building from scratch</p>
<p><strong>The result?</strong> Professional document scanning that rivals dedicated scanner apps, with 95% less code and effort.</p>
<hr>
<h2 id="why-developers-still-use-basic-camera-capture">Why Developers Still Use Basic Camera Capture</h2>
<p>Despite ML Kit Document Scanner being available since 2022, most apps still use basic camera capture for documents. Here&rsquo;s why:</p>
<ol>
<li><strong>Lack of awareness</strong> - Many developers don&rsquo;t know it exists</li>
<li><strong>&ldquo;Camera is good enough&rdquo;</strong> - Until users complain about quality</li>
<li><strong>Custom UI preference</strong> - Wanting full control (unnecessary)</li>
<li><strong>Assumed complexity</strong> - Thinking it requires ML expertise</li>
<li><strong>Library size concerns</strong> - Actually very reasonable (~3MB)</li>
</ol>
<p><strong>The reality:</strong> Basic camera capture for documents creates <strong>terrible UX</strong> compared to proper document scanning.</p>
<hr>
<h2 id="the-problem-with-basic-camera-capture">The Problem with Basic Camera Capture</h2>
<h3 id="what-happens-with-regular-camera">What Happens with Regular Camera:</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Typical camera implementation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> cameraIntent = Intent(<span style="color:#a6e22e">MediaStore</span>.ACTION_IMAGE_CAPTURE)
</span></span><span style="display:flex;"><span>launcher.launch(cameraIntent)
</span></span><span style="display:flex;"><span><span style="color:#75715e">// User gets: blurry, skewed, shadowy image
</span></span></span></code></pre></div><p><strong>User experience issues:</strong></p>
<ul>
<li>📸 No guidance on document boundaries</li>
<li>🔲 Manual cropping required (tedious)</li>
<li>🌓 Poor lighting = unusable scans</li>
<li>📐 Perspective distortion (holding phone at angle)</li>
<li>📄 One page at a time (inefficient for multi-page docs)</li>
<li>🎨 No enhancement (washed out, low contrast)</li>
</ul>
<p><strong>Result:</strong> Users waste time cropping, retaking photos, and dealing with poor quality scans.</p>
<hr>
<h2 id="what-ml-kit-document-scanner-provides">What ML Kit Document Scanner Provides</h2>
<h3 id="the-complete-package">The Complete Package:</h3>
<ol>
<li>
<p><strong>Real-time Edge Detection</strong></p>
<ul>
<li>AI instantly finds document corners</li>
<li>Visual overlay shows detected boundaries</li>
<li>Works even with complex backgrounds</li>
</ul>
</li>
<li>
<p><strong>Auto Perspective Correction</strong></p>
<ul>
<li>Straightens tilted/skewed documents</li>
<li>Removes keystoning (trapezoid effect)</li>
<li>Perfect rectangular output every time</li>
</ul>
</li>
<li>
<p><strong>Smart Enhancement</strong></p>
<ul>
<li>Removes shadows and glare</li>
<li>Adjusts contrast automatically</li>
<li>Optimizes for text readability</li>
<li>Handles various lighting conditions</li>
</ul>
</li>
<li>
<p><strong>Multi-page Scanning</strong></p>
<ul>
<li>Scan entire documents in one flow</li>
<li>Add/remove pages easily</li>
<li>Page reordering built-in</li>
</ul>
</li>
<li>
<p><strong>Multiple Export Formats</strong></p>
<ul>
<li>High-quality images (JPEG/PNG)</li>
<li>PDF generation built-in</li>
<li>Configurable resolution</li>
</ul>
</li>
</ol>
<hr>
<h2 id="implementation">Implementation</h2>
<h3 id="step-1-add-dependencies"><strong>Step 1: Add Dependencies</strong></h3>
<p>Add to your app&rsquo;s <code>build.gradle</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-gradle" data-lang="gradle"><span style="display:flex;"><span>dependencies <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ML Kit Document Scanner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    implementation <span style="color:#e6db74">&#39;com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p><strong>Library size:</strong> ~3MB (tiny compared to the functionality you get!)</p>
<hr>
<h3 id="step-2-configure-scanner-options"><strong>Step 2: Configure Scanner Options</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScanner
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_JPEG
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_PDF
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.SCANNER_MODE_FULL
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Create scanner with options
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> options = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setGalleryImportAllowed(<span style="color:#66d9ef">true</span>)              <span style="color:#75715e">// Allow importing from gallery
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setPageLimit(<span style="color:#ae81ff">10</span>)                            <span style="color:#75715e">// Max 10 pages per scan
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF)  <span style="color:#75715e">// Get both formats
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setScannerMode(SCANNER_MODE_FULL)           <span style="color:#75715e">// Full scanning experience
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .build()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">val</span> scanner = <span style="color:#a6e22e">GmsDocumentScanning</span>.getClient(options)
</span></span></code></pre></div><p><strong>Scanner Modes:</strong></p>
<ul>
<li><code>SCANNER_MODE_FULL</code> - Complete UI with all features (recommended)</li>
<li><code>SCANNER_MODE_BASE</code> - Minimal UI, faster scanning</li>
</ul>
<hr>
<h3 id="step-3-launch-scanner-and-handle-results"><strong>Step 3: Launch Scanner and Handle Results</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Activity Result Launcher
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> scannerLauncher = registerForActivityResult(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ActivityResultContracts</span>.StartIntentSenderForResult()
</span></span><span style="display:flex;"><span>) { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (result.resultCode <span style="color:#f92672">==</span> RESULT_OK) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> scanningResult = <span style="color:#a6e22e">GmsDocumentScanningResult</span>.fromActivityResultIntent(result.<span style="color:#66d9ef">data</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        scanningResult<span style="color:#f92672">?.</span>let { scanResult <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Get scanned pages
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            scanResult.pages<span style="color:#f92672">?.</span>let { pages <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                pages.forEach { page <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Access page image URI
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#66d9ef">val</span> imageUri = page.imageUri
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Use the scanned image
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    loadScannedImage(imageUri)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Get PDF if generated
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            scanResult.pdf<span style="color:#f92672">?.</span>let { pdf <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">val</span> pdfUri = pdf.uri
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">val</span> pageCount = pdf.pageCount
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Save or share PDF
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                savePdfDocument(pdfUri, pageCount)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Launch scanner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">startDocumentScan</span>() {
</span></span><span style="display:flex;"><span>    scanner.getStartScanIntent(<span style="color:#66d9ef">this</span>)
</span></span><span style="display:flex;"><span>        .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            scannerLauncher.launch(
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build()
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        .addOnFailureListener { exception <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Handle error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">Log</span>.e(<span style="color:#e6db74">&#34;Scanner&#34;</span>, <span style="color:#e6db74">&#34;Failed to start scanner&#34;</span>, exception)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="step-4-complete-jetpack-compose-integration"><strong>Step 4: Complete Jetpack Compose Integration</strong></h3>
<p>Here&rsquo;s a production-ready composable implementation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">DocumentScannerButton</span>(
</span></span><span style="display:flex;"><span>    onDocumentsScanned: (List&lt;Uri&gt;) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    onPdfGenerated: (Uri, Int) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier,
</span></span><span style="display:flex;"><span>    enabled: Boolean = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> activity = context <span style="color:#66d9ef">as</span>? ComponentActivity
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Scanner options
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> scanner = remember {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> options = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>            .setGalleryImportAllowed(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>            .setPageLimit(<span style="color:#ae81ff">10</span>)
</span></span><span style="display:flex;"><span>            .setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF)
</span></span><span style="display:flex;"><span>            .setScannerMode(SCANNER_MODE_FULL)
</span></span><span style="display:flex;"><span>            .build()
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">GmsDocumentScanning</span>.getClient(options)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Result launcher
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> scannerLauncher = rememberLauncherForActivityResult(
</span></span><span style="display:flex;"><span>        contract = <span style="color:#a6e22e">ActivityResultContracts</span>.StartIntentSenderForResult()
</span></span><span style="display:flex;"><span>    ) { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (result.resultCode <span style="color:#f92672">==</span> <span style="color:#a6e22e">ComponentActivity</span>.RESULT_OK) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">val</span> scanResult = <span style="color:#a6e22e">GmsDocumentScanningResult</span>.fromActivityResultIntent(result.<span style="color:#66d9ef">data</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            scanResult<span style="color:#f92672">?.</span>let {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Handle scanned images
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">it</span>.pages<span style="color:#f92672">?.</span>let { pages <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">val</span> imageUris = pages.mapNotNull { page <span style="color:#f92672">-&gt;</span> page.imageUri }
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> (imageUris.isNotEmpty()) {
</span></span><span style="display:flex;"><span>                        onDocumentsScanned(imageUris)
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Handle PDF
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">it</span>.pdf<span style="color:#f92672">?.</span>let { pdf <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    onPdfGenerated(pdf.uri, pdf.pageCount)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Launch scanner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">launchScanner</span>() {
</span></span><span style="display:flex;"><span>        activity<span style="color:#f92672">?.</span>let { act <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            scanner.getStartScanIntent(act)
</span></span><span style="display:flex;"><span>                .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    scannerLauncher.launch(
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build()
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                .addOnFailureListener { exception <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Log</span>.e(<span style="color:#e6db74">&#34;DocumentScanner&#34;</span>, <span style="color:#e6db74">&#34;Failed to start&#34;</span>, exception)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Button(
</span></span><span style="display:flex;"><span>        onClick = { launchScanner() },
</span></span><span style="display:flex;"><span>        enabled = enabled,
</span></span><span style="display:flex;"><span>        modifier = modifier
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        Icon(
</span></span><span style="display:flex;"><span>            imageVector = <span style="color:#a6e22e">Icons</span>.<span style="color:#a6e22e">Default</span>.DocumentScanner,
</span></span><span style="display:flex;"><span>            contentDescription = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.size(<span style="color:#ae81ff">20.</span>dp)
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.width(<span style="color:#ae81ff">8.</span>dp))
</span></span><span style="display:flex;"><span>        Text(<span style="color:#e6db74">&#34;Scan Document&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Usage:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">DocumentUploadScreen</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> scannedImages <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;List&lt;Uri&gt;&gt;(emptyList()) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> pdfUri <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;Uri?&gt;(<span style="color:#66d9ef">null</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(
</span></span><span style="display:flex;"><span>        modifier = Modifier
</span></span><span style="display:flex;"><span>            .fillMaxSize()
</span></span><span style="display:flex;"><span>            .padding(<span style="color:#ae81ff">16.</span>dp)
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        DocumentScannerButton(
</span></span><span style="display:flex;"><span>            onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                scannedImages = images
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>                    context,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;Scanned </span><span style="color:#e6db74">${images.size}</span><span style="color:#e6db74"> pages&#34;</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT
</span></span><span style="display:flex;"><span>                ).show()
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            onPdfGenerated = { uri, pageCount <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                pdfUri = uri
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>                    context,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;PDF created with </span><span style="color:#e6db74">$pageCount</span><span style="color:#e6db74"> pages&#34;</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT
</span></span><span style="display:flex;"><span>                ).show()
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Display scanned images
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        LazyColumn {
</span></span><span style="display:flex;"><span>            items(scannedImages) { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                AsyncImage(
</span></span><span style="display:flex;"><span>                    model = imageUri,
</span></span><span style="display:flex;"><span>                    contentDescription = <span style="color:#e6db74">&#34;Scanned page&#34;</span>,
</span></span><span style="display:flex;"><span>                    modifier = Modifier
</span></span><span style="display:flex;"><span>                        .fillMaxWidth()
</span></span><span style="display:flex;"><span>                        .height(<span style="color:#ae81ff">200.</span>dp)
</span></span><span style="display:flex;"><span>                        .padding(vertical = <span style="color:#ae81ff">8.</span>dp)
</span></span><span style="display:flex;"><span>                )
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="real-world-use-cases">Real-World Use Cases</h2>
<h3 id="1-iddocument-verification"><strong>1. ID/Document Verification</strong></h3>
<p>Perfect for KYC (Know Your Customer) flows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">KYCDocumentUpload</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> idCardUri <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;Uri?&gt;(<span style="color:#66d9ef">null</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column {
</span></span><span style="display:flex;"><span>        Text(<span style="color:#e6db74">&#34;Upload ID Card&#34;</span>, style = <span style="color:#a6e22e">MaterialTheme</span>.typography.titleLarge)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        DocumentScannerButton(
</span></span><span style="display:flex;"><span>            onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                idCardUri = images.firstOrNull()
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Automatically extract text with ML Kit Text Recognition
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                verifyIDDocument(idCardUri)
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            onPdfGenerated = { _, _ <span style="color:#f92672">-&gt;</span> }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        idCardUri<span style="color:#f92672">?.</span>let { uri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            AsyncImage(
</span></span><span style="display:flex;"><span>                model = uri,
</span></span><span style="display:flex;"><span>                contentDescription = <span style="color:#e6db74">&#34;ID Card&#34;</span>,
</span></span><span style="display:flex;"><span>                modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth()
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="2-receiptinvoice-scanning"><strong>2. Receipt/Invoice Scanning</strong></h3>
<p>For expense tracking or accounting apps:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">ExpenseReceiptScanner</span>(
</span></span><span style="display:flex;"><span>    onReceiptScanned: (Uri, String) <span style="color:#f92672">-&gt;</span> Unit
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    DocumentScannerButton(
</span></span><span style="display:flex;"><span>        onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            images.firstOrNull()<span style="color:#f92672">?.</span>let { receiptUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Extract text from receipt
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">val</span> amount = extractReceiptAmount(receiptUri)
</span></span><span style="display:flex;"><span>                onReceiptScanned(receiptUri, amount)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        onPdfGenerated = { _, _ <span style="color:#f92672">-&gt;</span> }
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="3-multi-page-document-archival"><strong>3. Multi-page Document Archival</strong></h3>
<p>For scanning contracts, forms, or books:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">DocumentArchiver</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> documentTitle <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> savedPdfUri <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;Uri?&gt;(<span style="color:#66d9ef">null</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column {
</span></span><span style="display:flex;"><span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = documentTitle,
</span></span><span style="display:flex;"><span>            onValueChange = { documentTitle = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = { Text(<span style="color:#e6db74">&#34;Document Name&#34;</span>) }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        DocumentScannerButton(
</span></span><span style="display:flex;"><span>            onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Individual pages available if needed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            },
</span></span><span style="display:flex;"><span>            onPdfGenerated = { pdfUri, pageCount <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Save PDF with title
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                savedPdfUri = pdfUri
</span></span><span style="display:flex;"><span>                saveToDocuments(documentTitle, pdfUri)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="4-note-taking-apps"><strong>4. Note-Taking Apps</strong></h3>
<p>Scan handwritten notes or whiteboard content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">ScanAndConvertNotes</span>() {
</span></span><span style="display:flex;"><span>    DocumentScannerButton(
</span></span><span style="display:flex;"><span>        onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            images.forEach { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Use ML Kit Text Recognition
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                extractHandwrittenText(imageUri) { text <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Convert to editable text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    saveAsNote(text)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        onPdfGenerated = { _, _ <span style="color:#f92672">-&gt;</span> }
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="advanced-configuration-options">Advanced Configuration Options</h2>
<h3 id="custom-scanner-settings"><strong>Custom Scanner Settings</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Minimal scanner (faster, less features)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> minimalOptions = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setScannerMode(SCANNER_MODE_BASE)
</span></span><span style="display:flex;"><span>    .setPageLimit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    .setResultFormats(RESULT_FORMAT_JPEG)
</span></span><span style="display:flex;"><span>    .setGalleryImportAllowed(<span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>    .build()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Professional scanner (all features)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> professionalOptions = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setScannerMode(SCANNER_MODE_FULL)
</span></span><span style="display:flex;"><span>    .setPageLimit(<span style="color:#ae81ff">50</span>)                           <span style="color:#75715e">// Up to 50 pages
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF)
</span></span><span style="display:flex;"><span>    .setGalleryImportAllowed(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>    .build()
</span></span></code></pre></div><h3 id="handling-different-result-formats"><strong>Handling Different Result Formats</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>scanningResult<span style="color:#f92672">?.</span>let { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Option 1: Process individual images
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    result.pages<span style="color:#f92672">?.</span>forEach { page <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> imageUri = page.imageUri
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Each page as separate image
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        processImage(imageUri)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Option 2: Get consolidated PDF
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    result.pdf<span style="color:#f92672">?.</span>let { pdf <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> pdfUri = pdf.uri
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> pageCount = pdf.pageCount
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Single PDF with all pages
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        sharePDF(pdfUri)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="comparison-before-vs-after">Comparison: Before vs After</h2>
<h3 id="custom-camera-implementation-old-way"><strong>Custom Camera Implementation (Old Way)</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// 500+ lines of code for:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CustomCameraActivity</span> : AppCompatActivity() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">lateinit</span> <span style="color:#66d9ef">var</span> cameraProvider: ProcessCameraProvider
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">var</span> imageCapture: ImageCapture? = <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Camera setup
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Permission handling
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Custom UI overlay
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Manual cropping UI
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Image enhancement logic
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Edge detection algorithm
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Perspective correction math
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Multi-page management
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// PDF generation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Error handling
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// ... 450+ more lines
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p><strong>Problems:</strong></p>
<ul>
<li>500+ lines of complex code</li>
<li>Camera permission management</li>
<li>Device compatibility issues</li>
<li>Manual cropping UI needed</li>
<li>No auto enhancement</li>
<li>Mediocre results</li>
<li>Maintenance burden</li>
</ul>
<h3 id="ml-kit-document-scanner-new-way"><strong>ML Kit Document Scanner (New Way)</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// 10 lines of code:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> scanner = <span style="color:#a6e22e">GmsDocumentScanning</span>.getClient(options)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>    .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        launcher.launch(<span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build())
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Done! Professional scanning with AI.
</span></span></span></code></pre></div><p><strong>Benefits:</strong></p>
<ul>
<li>10 lines of code</li>
<li>No permissions needed</li>
<li>Works on all devices</li>
<li>Auto cropping included</li>
<li>AI enhancement</li>
<li>Professional results</li>
<li>Zero maintenance</li>
</ul>
<hr>
<h2 id="performance--best-practices">Performance &amp; Best Practices</h2>
<h3 id="library-size-impact"><strong>Library Size Impact</strong></h3>
<pre tabindex="0"><code>ML Kit Document Scanner:  ~3MB
Custom camera + CV libs:  ~15-25MB
</code></pre><p><strong>Worth it?</strong> Absolutely. You get professional features for 1/5th the size.</p>
<h3 id="memory-management"><strong>Memory Management</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Don&#39;t load all images at once
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>scannedImages.forEach { uri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Process one at a time
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    processImage(uri)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Or use paging for large batches
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Use Coil/Glide for efficient image loading
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>AsyncImage(
</span></span><span style="display:flex;"><span>    model = imageUri,
</span></span><span style="display:flex;"><span>    contentDescription = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    contentScale = <span style="color:#a6e22e">ContentScale</span>.Fit
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h3 id="error-handling"><strong>Error Handling</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>    .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        launcher.launch(<span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build())
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    .addOnFailureListener { exception <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">when</span> (exception) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">is</span> MlKitException <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// ML Kit specific error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                showError(<span style="color:#e6db74">&#34;Scanner unavailable: </span><span style="color:#e6db74">${exception.message}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span> <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Generic error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                showError(<span style="color:#e6db74">&#34;Failed to start scanner&#34;</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div><h3 id="testing-on-different-devices"><strong>Testing on Different Devices</strong></h3>
<p>ML Kit Document Scanner works on:</p>
<ul>
<li>✅ Android 5.0+ (API 21+)</li>
<li>✅ Devices with Google Play Services</li>
<li>✅ All form factors (phones, tablets)</li>
<li>✅ Various camera qualities</li>
</ul>
<p><strong>Note:</strong> Requires Google Play Services. Check availability:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">isDocumentScannerAvailable</span>(context: Context): Boolean {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> status = <span style="color:#a6e22e">GoogleApiAvailability</span>.getInstance()
</span></span><span style="display:flex;"><span>            .isGooglePlayServicesAvailable(context)
</span></span><span style="display:flex;"><span>        status <span style="color:#f92672">==</span> <span style="color:#a6e22e">ConnectionResult</span>.SUCCESS
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="when-to-use-ml-kit-vs-custom-camera">When to Use ML Kit vs Custom Camera</h2>
<h3 id="use-ml-kit-document-scanner-when"><strong>Use ML Kit Document Scanner When:</strong></h3>
<p>✅ Scanning documents, receipts, IDs, contracts
✅ Need professional quality scans
✅ Want multi-page support
✅ Need PDF generation
✅ Auto enhancement required
✅ Limited development time/budget</p>
<h3 id="use-custom-camera-when"><strong>Use Custom Camera When:</strong></h3>
<p>⚠️ Capturing photos (not documents)
⚠️ Need real-time filters/effects
⚠️ Building a camera app
⚠️ Very specific custom workflow
⚠️ Can&rsquo;t use Google Play Services</p>
<p><strong>Bottom line:</strong> For 95% of document capture use cases, ML Kit is superior.</p>
<hr>
<h2 id="common-pitfalls-to-avoid">Common Pitfalls to Avoid</h2>
<h3 id="1-not-checking-for-play-services"><strong>1. Not Checking for Play Services</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ❌ BAD - Assumes availability
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ✅ GOOD - Check first
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">if</span> (isDocumentScannerAvailable(context)) {
</span></span><span style="display:flex;"><span>    scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>    showFallbackOption()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="2-ignoring-uri-permissions"><strong>2. Ignoring URI Permissions</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ✅ Grant persistent URI permissions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>contentResolver.takePersistableUriPermission(
</span></span><span style="display:flex;"><span>    uri,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Intent</span>.FLAG_GRANT_READ_URI_PERMISSION
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h3 id="3-not-handling-page-limits"><strong>3. Not Handling Page Limits</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ✅ Set appropriate page limits
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> options = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setPageLimit(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (multiPage) <span style="color:#ae81ff">50</span> <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">1</span>  <span style="color:#75715e">// Adjust based on use case
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    )
</span></span><span style="display:flex;"><span>    .build()
</span></span></code></pre></div><hr>
<h2 id="integration-with-other-ml-kit-features">Integration with Other ML Kit Features</h2>
<h3 id="combine-with-text-recognition"><strong>Combine with Text Recognition</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Scan document, then extract text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    images.forEach { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        recognizeText(imageUri) { extractedText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Use extracted text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            saveDocumentWithText(imageUri, extractedText)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">recognizeText</span>(uri: Uri, onTextExtracted: (String) <span style="color:#f92672">-&gt;</span> Unit) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> image = <span style="color:#a6e22e">InputImage</span>.fromFilePath(context, uri)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> recognizer = <span style="color:#a6e22e">TextRecognition</span>.getClient(<span style="color:#a6e22e">TextRecognizerOptions</span>.DEFAULT_OPTIONS)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    recognizer.process(image)
</span></span><span style="display:flex;"><span>        .addOnSuccessListener { visionText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            onTextExtracted(visionText.text)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="barcode-scanning-from-documents"><strong>Barcode Scanning from Documents</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Scan document with barcode/QR code
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    images.forEach { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        scanBarcode(imageUri) { barcodeValue <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Handle barcode data
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="quick-implementation-checklist">Quick Implementation Checklist</h2>
<p>Before shipping document scanning to production:</p>
<ul>
<li><input disabled="" type="checkbox"> ✅ Added ML Kit dependency (~3MB)</li>
<li><input disabled="" type="checkbox"> 📱 Tested on devices with/without Play Services</li>
<li><input disabled="" type="checkbox"> 🔧 Configured appropriate scanner mode</li>
<li><input disabled="" type="checkbox"> 📄 Set reasonable page limits</li>
<li><input disabled="" type="checkbox"> 🎨 Handled both image and PDF results</li>
<li><input disabled="" type="checkbox"> ⚠️ Implemented error handling</li>
<li><input disabled="" type="checkbox"> 💾 Managed URI permissions properly</li>
<li><input disabled="" type="checkbox"> 🧪 Tested with various document types</li>
<li><input disabled="" type="checkbox"> 📏 Verified image quality/resolution</li>
<li><input disabled="" type="checkbox"> 🔄 Added loading states for scanning</li>
</ul>
<hr>
<h2 id="real-world-impact">Real-World Impact</h2>
<h3 id="before-ml-kit"><strong>Before ML Kit:</strong></h3>
<ul>
<li>⏱️ Users spent 2-3 minutes per document (capture, crop, adjust)</li>
<li>😤 30-40% required retakes due to poor quality</li>
<li>📉 High abandonment rates on document upload flows</li>
<li>🐛 Constant bug reports about scanning issues</li>
</ul>
<h3 id="after-ml-kit"><strong>After ML Kit:</strong></h3>
<ul>
<li>⚡ 20-30 seconds per document (all automatic)</li>
<li>✨ &lt;5% retake rate (AI handles most issues)</li>
<li>📈 50-70% improvement in completion rates</li>
<li>😊 Positive feedback about scanning experience</li>
</ul>
<hr>
<h2 id="-related-resources">🔗 Related Resources</h2>
<ul>
<li><a href="https://developers.google.com/ml-kit/vision/doc-scanner">ML Kit Document Scanner Documentation</a></li>
<li><a href="https://developers.google.com/ml-kit/vision/text-recognition">ML Kit Text Recognition</a></li>
<li><a href="https://developers.google.com/android/guides/setup">Google Play Services Setup</a></li>
</ul>
<hr>
<h2 id="-final-thoughts">💡 Final Thoughts</h2>
<p>Stop wasting time building custom document capture solutions. <strong>ML Kit Document Scanner gives you professional, AI-powered scanning with minimal code.</strong></p>
<p><strong>The math is simple:</strong></p>
<ul>
<li>🕐 Custom implementation: 2-3 weeks + ongoing maintenance</li>
<li>⚡ ML Kit integration: 2-3 hours + zero maintenance</li>
<li>🎯 Result quality: ML Kit wins every time</li>
</ul>
<p><strong>Key takeaways:</strong></p>
<ol>
<li><strong>Stop using basic camera capture</strong> for documents</li>
<li><strong>ML Kit is tiny</strong> (~3MB) for massive functionality</li>
<li><strong>10 lines of code</strong> beats 500+ lines</li>
<li><strong>Professional results</strong> without CV expertise</li>
<li><strong>Users notice the difference</strong> - completion rates improve significantly</li>
</ol>
<p>Your users deserve better than blurry, crooked photos. Give them professional document scanning with ML Kit.</p>
<hr>
<p><strong>That&rsquo;s it!</strong> You now have the knowledge to implement professional document scanning in your Android app. 🎉</p>
<p>Feel free to reach out via my social handles with questions or to share your implementation! 😊</p>
<p><strong>Happy scanning!</strong> 📄✨</p>
]]></content:encoded></item><item><title>Staying Relevant with Claude Code - A Self-Note for Android &amp; KMP Developers</title><link>https://md.eknath.dev/posts/ai-ml/claude-code-notes/</link><pubDate>Fri, 09 Jan 2026 21:47:13 +0530</pubDate><guid>https://md.eknath.dev/posts/ai-ml/claude-code-notes/</guid><description>&lt;h2 id="a-quick-disclaimer">A Quick Disclaimer&lt;/h2>
&lt;p>This article is primarily a &lt;strong>self-note&lt;/strong> that I keep updating as I learn more about Claude Code. The AI tooling landscape evolves rapidly, so some information might be outdated by the time you read this. If you find something that needs updating, feel free to reach out!&lt;/p>
&lt;p>Whether you&amp;rsquo;re a &lt;strong>junior developer&lt;/strong> just getting started or a &lt;strong>senior developer&lt;/strong> looking to boost your workflow, Claude Code has something for everyone.&lt;/p></description><content:encoded><![CDATA[<h2 id="a-quick-disclaimer">A Quick Disclaimer</h2>
<p>This article is primarily a <strong>self-note</strong> that I keep updating as I learn more about Claude Code. The AI tooling landscape evolves rapidly, so some information might be outdated by the time you read this. If you find something that needs updating, feel free to reach out!</p>
<p>Whether you&rsquo;re a <strong>junior developer</strong> just getting started or a <strong>senior developer</strong> looking to boost your workflow, Claude Code has something for everyone.</p>
<hr>
<h2 id="why-should-you-care">Why Should You Care?</h2>
<p>If you&rsquo;ve read my <a href="https://md.eknath.dev/posts/software-development/devfest2025-solutionist-mindset-talk/">Solutionist Mindset talk</a>, you know I believe in <strong>using AI as a copilot, not a pilot</strong>. Claude Code embodies this philosophy perfectly—it&rsquo;s a CLI tool that sits alongside your existing workflow, helping you move faster while keeping you in control.</p>
<p>For Android and <strong>Compose Multiplatform (KMP)</strong> developers like us, having a tool that understands our codebase context is game-changing. Gradle configurations, multi-module architectures, platform-specific implementations—Claude Code can navigate all of this.</p>
<hr>
<h2 id="what-is-claude-code">What is Claude Code?</h2>
<p>Claude Code is Anthropic&rsquo;s <strong>official CLI tool</strong> that brings Claude directly into your terminal. Unlike the web interface, it:</p>
<ul>
<li><strong>Has full access to your codebase</strong> (with your permission)</li>
<li><strong>Can read, write, and edit files</strong> directly</li>
<li><strong>Runs shell commands</strong> for you</li>
<li><strong>Understands project context</strong> across multiple files</li>
<li><strong>Integrates with Git</strong> for version control operations</li>
</ul>
<p>Think of it as having a senior developer sitting next to you who can:</p>
<ul>
<li>Explore your codebase instantly</li>
<li>Write and refactor code</li>
<li>Debug issues by reading logs and stack traces</li>
<li>Create commits and PRs</li>
<li>Explain complex code sections</li>
</ul>
<hr>
<h2 id="latest-updates-january-2026">Latest Updates (January 2026)</h2>
<p><strong>Update available for Claude Code users (v: 2.1.2)!</strong></p>
<h3 id="model-selection">Model Selection</h3>
<p>Opus 4.5 is now available in model selection. While <strong>Sonnet is superior for general coding tasks</strong>, <strong>Opus is amazing for complex features and crazy bugs</strong>. You can use <code>-m</code> flag for selecting a specific model for particular task sessions.</p>
<table>
  <thead>
      <tr>
          <th>Model ID</th>
          <th>Description</th>
          <th>Context Window</th>
          <th>Relative Cost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>claude-sonnet-4-20250514</code></td>
          <td>Sonnet 4 (default, most balanced)</td>
          <td>200K</td>
          <td>$$ (Moderate)</td>
      </tr>
      <tr>
          <td><code>claude-opus-4-20250514</code></td>
          <td>Opus 4 (most capable, slower)</td>
          <td>200K</td>
          <td>$$$$ (Very High)</td>
      </tr>
      <tr>
          <td><code>claude-sonnet-4-5-20250929</code></td>
          <td>Sonnet 4.5 (smartest, efficient)</td>
          <td>500K</td>
          <td>$$$ (High)</td>
      </tr>
      <tr>
          <td><code>claude-haiku-4-5-20251001</code></td>
          <td>Haiku 4.5 (fastest, most economical)</td>
          <td>200K</td>
          <td>$ (Low)</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>[!WARNING]
<strong>Token Usage Warning</strong>: Continuous usage of <strong>Opus</strong> models will consume your rate limits and quota significantly faster (approx. 5-10x) than Sonnet. Use Opus only for complex debugging or architectural tasks.</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Run with a specific model</span>
</span></span><span style="display:flex;"><span>claude -m claude-opus-4-20250514
</span></span></code></pre></div><h3 id="official-plugins">Official Plugins</h3>
<p>Check out the official plugins:</p>
<ul>
<li><a href="https://github.com/anthropics/claude-code/tree/main/plugins">All Plugins</a></li>
<li><a href="https://github.com/anthropics/claude-code/tree/main/plugins/code-review">Code Review Plugin</a></li>
</ul>
<hr>
<h2 id="getting-started">Getting Started</h2>
<h3 id="installation">Installation</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Using npm</span>
</span></span><span style="display:flex;"><span>npm install -g @anthropic-ai/claude-code
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Using Homebrew (macOS)</span>
</span></span><span style="display:flex;"><span>brew install claude-code
</span></span></code></pre></div><p>After installation, run <code>claude</code> in your terminal to start an interactive session. You&rsquo;ll need to authenticate with your Anthropic API key or use the <code>claude --login</code> command to login via the browser.</p>
<h3 id="basic-usage">Basic Usage</h3>
<p>Navigate to your project directory and simply run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>claude
</span></span></code></pre></div><p>This starts an interactive session where you can ask questions, request code changes, or explore your codebase.</p>
<hr>
<h2 id="essential-commands-every-developer-should-know">Essential Commands Every Developer Should Know</h2>
<h3 id="1-slash-commands">1️⃣ Slash Commands</h3>
<p>Claude Code has built-in slash commands that trigger specific workflows:</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>What It Does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/help</code></td>
          <td>Shows available commands and usage tips</td>
      </tr>
      <tr>
          <td><code>/clear</code></td>
          <td>Clears conversation history (starts fresh)</td>
      </tr>
      <tr>
          <td><code>/compact</code></td>
          <td>Compresses the conversation to save context</td>
      </tr>
      <tr>
          <td><code>/cost</code></td>
          <td>Shows token usage and estimated costs</td>
      </tr>
      <tr>
          <td><code>/doctor</code></td>
          <td>Diagnoses installation and configuration issues</td>
      </tr>
      <tr>
          <td><code>/init</code></td>
          <td>Creates a CLAUDE.md file with project context</td>
      </tr>
      <tr>
          <td><code>/review</code></td>
          <td>Triggers code review for recent changes</td>
      </tr>
      <tr>
          <td><code>/commit</code></td>
          <td>Creates a git commit with meaningful message</td>
      </tr>
  </tbody>
</table>
<h3 id="2-the-claudemd-file">2️⃣ The CLAUDE.md File</h3>
<p>One of the most powerful features for <strong>multi-module Android/KMP projects</strong> is the <code>CLAUDE.md</code> file. Create this at your project root:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Project: MyKMPApp
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Architecture
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Multi-module KMP project
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Shared module: commonMain, androidMain, iosMain
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Android app module with Jetpack Compose UI
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> iOS app using SwiftUI
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Key Conventions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Use Koin for dependency injection
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Room for local database (Android), SQLDelight for shared
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Ktor for networking
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Kotlin Coroutines + Flow for async operations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Build Commands
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`./gradlew assembleDebug`</span> - Build Android debug
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`./gradlew :shared:build`</span> - Build shared module only
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`./gradlew connectedAndroidTest`</span> - Run instrumented tests
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Module Structure
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> :app - Android application entry point
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :shared - KMP shared code
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :feature:home - Home feature module
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :feature:settings - Settings feature module
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :core:network - Networking utilities
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :core:database - Database layer
</span></span></code></pre></div><p>Claude reads this file and uses it as <strong>persistent context</strong> for every conversation. This is incredibly useful for:</p>
<ul>
<li><strong>Multi-module navigation</strong> - Claude knows your module structure</li>
<li><strong>Consistent coding patterns</strong> - Follows your conventions</li>
<li><strong>Faster builds</strong> - Knows the right Gradle commands</li>
</ul>
<h3 id="3-vim-style-keybindings">3️⃣ Vim-Style Keybindings</h3>
<p>For terminal enthusiasts, Claude Code supports vim keybindings:</p>
<table>
  <thead>
      <tr>
          <th>Key</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Escape</code></td>
          <td>Enter command mode</td>
      </tr>
      <tr>
          <td><code>i</code></td>
          <td>Return to insert mode</td>
      </tr>
      <tr>
          <td><code>Ctrl+C</code></td>
          <td>Cancel current operation</td>
      </tr>
      <tr>
          <td><code>Ctrl+D</code></td>
          <td>Exit Claude Code</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="practical-use-cases-for-androidkmp-developers">Practical Use Cases for Android/KMP Developers</h2>
<h3 id="use-case-1-exploring-unfamiliar-codebases">Use Case 1: Exploring Unfamiliar Codebases</h3>
<p>When you join a new project or inherit legacy code:</p>
<pre tabindex="0"><code>You: How is the authentication flow implemented in this app?
     Show me the key files involved.
</code></pre><p>Claude will search through your codebase, identify relevant files (ViewModels, Repositories, API services), and explain the flow.</p>
<h3 id="use-case-2-writing-compose-ui-components">Use Case 2: Writing Compose UI Components</h3>
<pre tabindex="0"><code>You: Create a bottom sheet component for filtering products.
     It should have checkboxes for categories and a price range slider.
     Follow our existing design system in :core:designsystem module.
</code></pre><p>Claude will:</p>
<ol>
<li>Look at your existing design system</li>
<li>Match the patterns and naming conventions</li>
<li>Create the component following your architecture</li>
</ol>
<h3 id="use-case-3-debugging-build-issues">Use Case 3: Debugging Build Issues</h3>
<pre tabindex="0"><code>You: I&#39;m getting this Gradle error when building the shared module:
     [paste error here]

     Help me understand and fix it.
</code></pre><p>Claude can read your <code>build.gradle.kts</code> files, understand the dependency graph, and suggest fixes.</p>
<h3 id="use-case-4-writing-platform-specific-implementations">Use Case 4: Writing Platform-Specific Implementations</h3>
<pre tabindex="0"><code>You: I need to implement biometric authentication.
     Create the expect/actual declarations for Android and iOS
     in the :core:auth module.
</code></pre><p>Claude understands KMP&rsquo;s expect/actual mechanism and generates appropriate platform-specific code.</p>
<h3 id="use-case-5-creating-git-commits">Use Case 5: Creating Git Commits</h3>
<pre tabindex="0"><code>You: /commit
</code></pre><p>Claude will:</p>
<ol>
<li>Analyze your staged changes</li>
<li>Understand the context of modifications</li>
<li>Generate a meaningful commit message</li>
<li>Create the commit</li>
</ol>
<h3 id="use-case-6-room-database-migrations">Use Case 6: Room Database Migrations</h3>
<pre tabindex="0"><code>You: I need to add a &#39;lastSyncedAt&#39; column to the UserEntity.
     Create the migration and update the entity.
</code></pre><p>Claude handles the boilerplate of Room migrations, which can be error-prone manually.</p>
<hr>
<h2 id="best-practices-for-effective-usage">Best Practices for Effective Usage</h2>
<h3 id="-do">✅ DO</h3>
<ol>
<li>
<p><strong>Be specific with context</strong> - Instead of &ldquo;fix this bug&rdquo;, say &ldquo;fix the crash in <code>UserRepository.kt</code> when the token expires&rdquo; make sure you add the file and line-number of the function or scope.</p>
</li>
<li>
<p><strong>Review generated code</strong> - Always understand what Claude writes. Don&rsquo;t blindly accept suggestions.</p>
</li>
<li>
<p><strong>Use it for exploration</strong> - Ask Claude to explain complex parts of your codebase or third-party libraries</p>
</li>
<li>
<p><strong>Leverage for boilerplate</strong> - Let Claude handle repetitive tasks like:</p>
<ul>
<li>Creating data classes from API responses</li>
<li>Writing Room entities and DAOs</li>
<li>Setting up Hilt/Koin modules</li>
<li>Creating navigation graphs</li>
<li>Writing unit test boilerplate</li>
</ul>
</li>
<li>
<p><strong>Maintain your CLAUDE.md</strong> - Keep it updated as your project evolves</p>
</li>
<li>
<p><strong>Use the right model for the task</strong> - Haiku for quick questions, Sonnet for general coding, Opus for complex debugging</p>
</li>
</ol>
<h3 id="-dont">❌ DON&rsquo;T</h3>
<ol>
<li>
<p><strong>Don&rsquo;t share sensitive data</strong> - Avoid passing API keys, secrets, or user data through Claude</p>
</li>
<li>
<p><strong>Don&rsquo;t skip the review</strong> - Especially for security-critical code (authentication, payment processing, encryption etc)</p>
</li>
<li>
<p><strong>Don&rsquo;t use it as a crutch</strong> - You should still understand the fundamentals. AI is a multiplier, not a replacement.</p>
</li>
<li>
<p><strong>Don&rsquo;t expect perfection</strong> - Claude can make mistakes. Treat its output as a starting point.</p>
</li>
<li>
<p><strong>Don&rsquo;t ignore Gradle sync</strong> - After Claude modifies <code>build.gradle.kts</code>, sync manually in Android Studio</p>
</li>
</ol>
<hr>
<h2 id="cost-management-tips">Cost Management Tips</h2>
<p>Claude Code uses API tokens, which cost money. Here&rsquo;s how to optimize:</p>
<h3 id="1-choose-the-right-model">1️⃣ Choose the Right Model</h3>
<table>
  <thead>
      <tr>
          <th>Task Type</th>
          <th>Recommended Model</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quick questions, simple edits</td>
          <td>Haiku 4.5</td>
      </tr>
      <tr>
          <td>General development</td>
          <td>Sonnet 4/4.5</td>
      </tr>
      <tr>
          <td>Complex debugging, architecture</td>
          <td>Opus 4/4.5</td>
      </tr>
  </tbody>
</table>
<h3 id="2-use-compact-regularly">2️⃣ Use <code>/compact</code> Regularly</h3>
<p>Long conversations consume more tokens. Use <code>/compact</code> to summarize and reduce context.</p>
<h3 id="3-start-fresh-for-new-tasks">3️⃣ Start Fresh for New Tasks</h3>
<p>Use <code>/clear</code> when switching to unrelated tasks. No need to carry previous context.</p>
<h3 id="4-be-concise">4️⃣ Be Concise</h3>
<p>Instead of:</p>
<pre tabindex="0"><code>&#34;Hey Claude, I was wondering if you could maybe help me
understand how the user authentication works in this app,
like when someone logs in, what happens step by step?&#34;
</code></pre><p>Try:</p>
<pre tabindex="0"><code>&#34;Explain the login flow. Start from LoginViewModel.&#34;
</code></pre><h3 id="6-offload-tasks-to-other-modelstools-save-those-tokens">6️⃣ Offload Tasks to Other Models/Tools (Save those Tokens!)</h3>
<p>Not everything requires Claude Code&rsquo;s deep context awareness. Save your tokens by routing tasks to the right tool:</p>
<ul>
<li><strong>Use Gemini for Quick Concepts</strong>: Need to understand &ldquo;How <code>LruCache</code> works internally&rdquo; or &ldquo;Explain the Builder pattern&rdquo;? Use <strong>Gemini</strong>. It&rsquo;s fast, free/cheap, and great for general knowledge that doesn&rsquo;t need your private codebase context.</li>
<li><strong>Use ChatGPT for High-Level Project Questions</strong>: If you need advice on &ldquo;Best practices for modularizing a KMP project&rdquo; or architecture discussions where providing full code access isn&rsquo;t necessary, <strong>ChatGPT</strong> is a great option.</li>
<li><strong>Use CLI Tools for Quick Answers</strong>: If you&rsquo;re a terminal power user (using <code>tmux</code>, <code>dia</code>, etc.), tools like <strong>ddgr</strong> (DuckDuckGo from terminal) or <strong>Ollama</strong> (local models) are fantastic for quick lookups without leaving your flow.</li>
</ul>
<blockquote>
<p>[!TIP]
<strong>Pro Tip</strong>: Reserve Claude Code for tasks that <em>specifically</em> need to read your files, understand your project structure, or perform edits. For everything else, cheaper (or free) alternatives often work just as well!</p>
</blockquote>
<h3 id="7-check-costs-with-cost">7️⃣ Check Costs with <code>/cost</code></h3>
<p>Regularly run <code>/cost</code> to monitor your usage.</p>
<hr>
<h2 id="integration-with-development-workflow">Integration with Development Workflow</h2>
<h3 id="ide-integration">IDE Integration</h3>
<p>Claude Code works alongside your IDE, not inside it. A typical workflow:</p>
<ol>
<li><strong>Have your IDE open</strong> (Android Studio / Fleet / IntelliJ)</li>
<li><strong>Run Claude in a terminal</strong> (split screen works great)</li>
<li><strong>Ask Claude to make changes</strong></li>
<li><strong>Review changes in IDE</strong> (they appear instantly)</li>
<li><strong>Test and iterate</strong></li>
</ol>
<h3 id="git-workflow-integration">Git Workflow Integration</h3>
<p>Claude Code integrates nicely with Git:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start a session</span>
</span></span><span style="display:flex;"><span>claude
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create meaningful commits</span>
</span></span><span style="display:flex;"><span>You: /commit
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create a pull request</span>
</span></span><span style="display:flex;"><span>You: Create a PR <span style="color:#66d9ef">for</span> these changes. The target branch is develop.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Review code before pushing</span>
</span></span><span style="display:flex;"><span>You: Review the changes in the last commit <span style="color:#66d9ef">for</span> any issues.
</span></span></code></pre></div><hr>
<h2 id="common-gotchas-for-androidkmp-projects">Common Gotchas for Android/KMP Projects</h2>
<h3 id="1-gradle-sync-after-changes">1️⃣ Gradle Sync After Changes</h3>
<p>When Claude modifies <code>build.gradle.kts</code> files, you&rsquo;ll need to sync in Android Studio manually. Claude can&rsquo;t trigger this for you.</p>
<h3 id="2-resource-files">2️⃣ Resource Files</h3>
<p>Claude can create/modify XML resources (layouts, strings, drawables), but be careful with:</p>
<ul>
<li><strong>Generated resources</strong> (R class) - These regenerate on build</li>
<li><strong>Vector drawables</strong> - Complex paths might need manual tweaking</li>
</ul>
<h3 id="3-compose-preview">3️⃣ Compose Preview</h3>
<p>Claude-generated Compose components might need <code>@Preview</code> annotations added for visibility in Android Studio&rsquo;s preview pane.</p>
<h3 id="4-ios-specific-code">4️⃣ iOS-Specific Code</h3>
<p>For KMP projects, Claude can write Swift/Objective-C code for iOS implementations, but:</p>
<ul>
<li>You&rsquo;ll need Xcode to verify it compiles</li>
<li>Swift interop with Kotlin can be tricky</li>
</ul>
<h3 id="5-version-catalog">5️⃣ Version Catalog</h3>
<p>If you use <code>libs.versions.toml</code>, make sure Claude knows about it in your <code>CLAUDE.md</code>. Otherwise, it might use hardcoded versions.</p>
<hr>
<h2 id="my-personal-workflow">My Personal Workflow</h2>
<p>Here&rsquo;s how I typically use Claude Code for Android development:</p>
<ol>
<li>
<p><strong>Morning exploration</strong> - &ldquo;What did I work on yesterday? Show me recent changes.&rdquo;</p>
</li>
<li>
<p><strong>Feature development</strong> - Start with asking Claude to explore existing patterns, then implement following those patterns</p>
</li>
<li>
<p><strong>Code review helper</strong> - &ldquo;Review this ViewModel for potential memory leaks or coroutine issues&rdquo;</p>
</li>
<li>
<p><strong>Documentation</strong> - &ldquo;Generate KDoc comments for the public methods in NetworkClient.kt&rdquo;</p>
</li>
<li>
<p><strong>Refactoring</strong> - &ldquo;Migrate this callback-based API to use Kotlin Coroutines with Flow&rdquo;</p>
</li>
<li>
<p><strong>Test writing</strong> - &ldquo;Write unit tests for UserRepository using MockK&rdquo;</p>
</li>
</ol>
<hr>
<h2 id="advanced-mcp-servers">Advanced: MCP Servers</h2>
<p>Claude Code supports <strong>Model Context Protocol (MCP)</strong> servers, which extend its capabilities. For Android developers, interesting MCPs include:</p>
<ul>
<li><strong>File system access</strong> - Already built-in</li>
<li><strong>Git operations</strong> - Built-in</li>
<li><strong>Web search</strong> - For documentation lookups</li>
<li><strong>Custom tools</strong> - You can create your own MCP servers</li>
</ul>
<p>Check out the <a href="https://modelcontextprotocol.io/">MCP Documentation</a> for more details.</p>
<hr>
<h2 id="useful-one-liners">Useful One-Liners</h2>
<p>Quick commands I use frequently:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start with a specific model for complex tasks</span>
</span></span><span style="display:flex;"><span>claude -m claude-opus-4-20250514
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Resume last conversation</span>
</span></span><span style="display:flex;"><span>claude --resume
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check installation health</span>
</span></span><span style="display:flex;"><span>claude /doctor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Quick code review</span>
</span></span><span style="display:flex;"><span>claude <span style="color:#e6db74">&#34;Review my staged changes for issues&#34;</span>
</span></span></code></pre></div><hr>
<h2 id="whats-next">What&rsquo;s Next?</h2>
<p>This article covers the fundamentals, but Claude Code is constantly evolving. I plan to update this note as I discover:</p>
<ul>
<li>New features and capabilities</li>
<li>Better workflows for KMP development</li>
<li>Integration patterns with CI/CD</li>
<li>Team collaboration strategies</li>
</ul>
<hr>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Claude Code is a <strong>powerful addition to the Android/KMP developer toolkit</strong>. It&rsquo;s not about replacing your skills—it&rsquo;s about <strong>amplifying</strong> them. Use it to handle boilerplate, explore unfamiliar code, and move faster on repetitive tasks.</p>
<p>But remember: <strong>You are still the pilot</strong>. Claude is your copilot. Understand what it generates, review its suggestions, and keep learning the fundamentals.</p>
<p>The developers who thrive in the AI age won&rsquo;t be those who code the fastest—they&rsquo;ll be the ones who <strong>solve problems thoughtfully</strong> while leveraging every tool at their disposal.</p>
<hr>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://docs.anthropic.com/claude-code">Official Claude Code Documentation</a></li>
<li><a href="https://github.com/anthropics/claude-code">Claude Code GitHub</a></li>
<li><a href="https://github.com/anthropics/claude-code/tree/main/plugins">Official Plugins</a></li>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a></li>
<li><a href="https://md.eknath.dev/posts/software-development/devfest2025-solutionist-mindset-talk/">My Solutionist Mindset Talk</a></li>
</ul>
<hr>
<p>Feel free to connect with me on:
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong>
🌍 <strong><a href="https://eknath.dev">Website</a></strong></p>
<p><em>I wish to keep this article as a living document. Last updated: January 2026</em></p>
]]></content:encoded></item><item><title>Clickable Links in Jetpack Compose: AnnotatedString with Custom URL Handler</title><link>https://md.eknath.dev/posts/android/clickable-links/</link><pubDate>Tue, 11 Nov 2025 22:30:28 +0530</pubDate><guid>https://md.eknath.dev/posts/android/clickable-links/</guid><description>&lt;p>While building the onboarding screen for my app, I needed a simple text that read something like this:&lt;/p>
&lt;p>&lt;img alt="Example of the Implementation" loading="lazy" src="https://md.eknath.dev/img/android/clickable-text-android-example.jpeg">
&lt;em>The green hyperlinks are clickable&lt;/em>&lt;/p>
&lt;p>Pretty straightforward, right? Just a bit of text with two clickable links — &lt;code>Privacy Policy&lt;/code> and &lt;code>Terms of Service&lt;/code>.&lt;/p>
&lt;p>But as I started implementing it, I realized: &lt;strong>there isn&amp;rsquo;t a single complete guide&lt;/strong> explaining how to make part of a text clickable using &lt;code>AnnotatedString&lt;/code> in Jetpack Compose — especially with the new &lt;strong>LinkAnnotation API&lt;/strong> and a &lt;strong>custom URL handler&lt;/strong> for better control.&lt;/p></description><content:encoded><![CDATA[<p>While building the onboarding screen for my app, I needed a simple text that read something like this:</p>
<p><img alt="Example of the Implementation" loading="lazy" src="/img/android/clickable-text-android-example.jpeg">
<em>The green hyperlinks are clickable</em></p>
<p>Pretty straightforward, right? Just a bit of text with two clickable links — <code>Privacy Policy</code> and <code>Terms of Service</code>.</p>
<p>But as I started implementing it, I realized: <strong>there isn&rsquo;t a single complete guide</strong> explaining how to make part of a text clickable using <code>AnnotatedString</code> in Jetpack Compose — especially with the new <strong>LinkAnnotation API</strong> and a <strong>custom URL handler</strong> for better control.</p>
<p>So I&rsquo;m writing this as both a reference for myself and for anyone else struggling with this tiny, under-documented detail.</p>
<hr>
<h2 id="why-custom-url-handling">Why Custom URL Handling?</h2>
<p>By default, Compose can open links using the system browser. But what if you want:</p>
<ul>
<li><strong>Better UX with Chrome Custom Tabs</strong> (opens links in-app with smooth transitions)</li>
<li><strong>Graceful fallbacks</strong> (handle cases where browsers aren&rsquo;t installed)</li>
<li><strong>Full control</strong> over how URLs are opened (analytics, deep links, etc.)</li>
</ul>
<p>That&rsquo;s exactly what we&rsquo;ll implement.</p>
<hr>
<h2 id="step-by-step-implementation">Step-by-Step Implementation</h2>
<h3 id="1-add-the-browser-library">1. Add the Browser Library</h3>
<p>We&rsquo;ll use <strong>Android Browser Library</strong> to implement Chrome Custom Tabs for a better link-opening experience.</p>
<p>Add this to your <code>libs.versions.toml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">versions</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">browser</span> = <span style="color:#e6db74">&#34;1.8.0&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">libraries</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">androidx-browser</span> = { <span style="color:#a6e22e">group</span> = <span style="color:#e6db74">&#34;androidx.browser&#34;</span>, <span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;browser&#34;</span>, <span style="color:#a6e22e">version</span>.<span style="color:#a6e22e">ref</span> = <span style="color:#e6db74">&#34;browser&#34;</span> }
</span></span></code></pre></div><p>Then add the dependency to your app&rsquo;s <code>build.gradle.kts</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>dependencies {
</span></span><span style="display:flex;"><span>    implementation(libs.androidx.browser)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Why Chrome Custom Tabs?</strong>
Custom Tabs open web content <strong>inside your app</strong> with a smooth transition, pre-loaded browser engine, and shared cookies/sessions — all without leaving your app&rsquo;s context.</p>
<hr>
<h3 id="2-create-a-custom-url-handler">2. Create a Custom URL Handler</h3>
<p>This function handles opening URLs with <strong>three layers of fallback</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">urlHandler</span>(url: String, context: Context) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// First: Try Chrome Custom Tabs (best UX)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> customTabsIntent = <span style="color:#a6e22e">CustomTabsIntent</span>.Builder().build()
</span></span><span style="display:flex;"><span>        customTabsIntent.launchUrl(context, <span style="color:#a6e22e">Uri</span>.parse(url))
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: ActivityNotFoundException) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Second: Fallback to system browser
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#66d9ef">val</span> intent = Intent(<span style="color:#a6e22e">Intent</span>.ACTION_VIEW, <span style="color:#a6e22e">Uri</span>.parse(url))
</span></span><span style="display:flex;"><span>            context.startActivity(intent)
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Third: Show toast if no browser exists
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>                context,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;Browser not installed! visit: </span><span style="color:#e6db74">$url</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Toast</span>.LENGTH_LONG
</span></span><span style="display:flex;"><span>            ).show()
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>        e.printStackTrace()
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>            context,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;Unable to open link! visit: </span><span style="color:#e6db74">$url</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT
</span></span><span style="display:flex;"><span>        ).show()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>How it works:</strong></p>
<ol>
<li><strong>Chrome Custom Tabs</strong> (preferred): Opens in-app with native feel</li>
<li><strong>System Browser</strong> (fallback): Opens in the default browser if Custom Tabs fail</li>
<li><strong>Toast Message</strong> (last resort): Shows URL if no browser is available</li>
</ol>
<p>This ensures <strong>your app never crashes</strong> when opening links, even on devices without browsers.</p>
<hr>
<h3 id="3-building-the-clickable-text-component">3. Building the Clickable Text Component</h3>
<p>Now let&rsquo;s create the actual composable with clickable links.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@OptIn</span>(ExperimentalTextApi<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">TermsAndCondition</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Define how links should behave when clicked
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> linkInteractionListener = LinkInteractionListener {
</span></span><span style="display:flex;"><span>        urlHandler((<span style="color:#66d9ef">it</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">LinkAnnotation</span>.Url).url, context)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Define link styling (color + underline)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> linkStyle = TextLinkStyles(
</span></span><span style="display:flex;"><span>        SpanStyle(
</span></span><span style="display:flex;"><span>            color = DGreen,
</span></span><span style="display:flex;"><span>            textDecoration = <span style="color:#a6e22e">TextDecoration</span>.Underline
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Create Privacy Policy link annotation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> privacyPolicy = <span style="color:#a6e22e">LinkAnnotation</span>.Url(
</span></span><span style="display:flex;"><span>        url = <span style="color:#a6e22e">URLS</span>.PRIVACY,
</span></span><span style="display:flex;"><span>        styles = linkStyle,
</span></span><span style="display:flex;"><span>        linkInteractionListener = linkInteractionListener
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Create Terms of Service link annotation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> toc = <span style="color:#a6e22e">LinkAnnotation</span>.Url(
</span></span><span style="display:flex;"><span>        url = <span style="color:#a6e22e">URLS</span>.TOC,
</span></span><span style="display:flex;"><span>        styles = linkStyle,
</span></span><span style="display:flex;"><span>        linkInteractionListener = linkInteractionListener
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Helper functions for reusable link text (supports localization!)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">AnnotatedString</span>.<span style="color:#a6e22e">Builder</span>.privacyLink() {
</span></span><span style="display:flex;"><span>        withLink(privacyPolicy) {
</span></span><span style="display:flex;"><span>            append(stringResource(id = <span style="color:#a6e22e">R</span>.string.privacy_policy))
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">AnnotatedString</span>.<span style="color:#a6e22e">Builder</span>.tocLink() {
</span></span><span style="display:flex;"><span>        withLink(toc) {
</span></span><span style="display:flex;"><span>            append(stringResource(id = <span style="color:#a6e22e">R</span>.string.terms_of_service))
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Build the complete annotated string with mixed text + links
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> annotatedString = buildAnnotatedString {
</span></span><span style="display:flex;"><span>        append(<span style="color:#e6db74">&#34;Read &#34;</span>)
</span></span><span style="display:flex;"><span>        privacyLink()  <span style="color:#75715e">// Clickable Privacy Policy
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        append(<span style="color:#e6db74">&#34; and tap on `&#34;</span>)
</span></span><span style="display:flex;"><span>        withStyle(style = SpanStyle(fontWeight = <span style="color:#a6e22e">FontWeight</span>.Bold)) {
</span></span><span style="display:flex;"><span>            append(<span style="color:#e6db74">&#34;Agree and Continue&#34;</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        append(<span style="color:#e6db74">&#34;` to accept the &#34;</span>)
</span></span><span style="display:flex;"><span>        tocLink()  <span style="color:#75715e">// Clickable Terms of Service
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Text(
</span></span><span style="display:flex;"><span>        text = annotatedString,
</span></span><span style="display:flex;"><span>        style = <span style="color:#a6e22e">PannaiTheme</span>.typography.semiBold12.copy(
</span></span><span style="display:flex;"><span>            color = LGray,
</span></span><span style="display:flex;"><span>            textAlign = <span style="color:#a6e22e">TextAlign</span>.Center
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(<span style="color:#ae81ff">0.8f</span>),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="breaking-down-the-code">Breaking Down the Code</h2>
<h3 id="linkinteractionlistener">LinkInteractionListener</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> linkInteractionListener = LinkInteractionListener {
</span></span><span style="display:flex;"><span>    urlHandler((<span style="color:#66d9ef">it</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">LinkAnnotation</span>.Url).url, context)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li>This listener gets triggered when <strong>any link is clicked</strong></li>
<li>It extracts the URL from the <code>LinkAnnotation.Url</code> object</li>
<li>Passes it to our custom <code>urlHandler()</code> for controlled opening</li>
</ul>
<hr>
<h3 id="linkannotationurl">LinkAnnotation.Url</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> privacyPolicy = <span style="color:#a6e22e">LinkAnnotation</span>.Url(
</span></span><span style="display:flex;"><span>    url = <span style="color:#a6e22e">URLS</span>.PRIVACY,
</span></span><span style="display:flex;"><span>    styles = linkStyle,
</span></span><span style="display:flex;"><span>    linkInteractionListener = linkInteractionListener
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li><strong>url</strong>: The actual link destination</li>
<li><strong>styles</strong>: How the link looks (color, underline, etc.)</li>
<li><strong>linkInteractionListener</strong>: What happens when clicked</li>
</ul>
<p>This is the <strong>new Compose way</strong> of handling links (replaces older <code>pushStringAnnotation</code> approach).</p>
<hr>
<h3 id="helper-functions-for-localization">Helper Functions for Localization</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">AnnotatedString</span>.<span style="color:#a6e22e">Builder</span>.privacyLink() {
</span></span><span style="display:flex;"><span>    withLink(privacyPolicy) {
</span></span><span style="display:flex;"><span>        append(stringResource(id = <span style="color:#a6e22e">R</span>.string.privacy_policy))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Why helper functions?</strong>
By using <code>stringResource()</code>, the link text <strong>automatically updates</strong> based on user&rsquo;s language. This makes your code cleaner, reusable, and supports multi-language apps without duplicating logic.</p>
<hr>
<h3 id="building-the-final-text">Building the Final Text</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> annotatedString = buildAnnotatedString {
</span></span><span style="display:flex;"><span>    append(<span style="color:#e6db74">&#34;Read &#34;</span>)
</span></span><span style="display:flex;"><span>    privacyLink()  <span style="color:#75715e">// This part is clickable!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    append(<span style="color:#e6db74">&#34; and tap on `&#34;</span>)
</span></span><span style="display:flex;"><span>    withStyle(style = SpanStyle(fontWeight = <span style="color:#a6e22e">FontWeight</span>.Bold)) {
</span></span><span style="display:flex;"><span>        append(<span style="color:#e6db74">&#34;Agree and Continue&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    append(<span style="color:#e6db74">&#34;` to accept the &#34;</span>)
</span></span><span style="display:flex;"><span>    tocLink()  <span style="color:#75715e">// This part is also clickable!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>This combines <strong>regular text</strong> + <strong>clickable links</strong> + <strong>styled text</strong> in one component. <code>withLink()</code> wraps text and makes it interactive, while <code>withStyle()</code> applies visual formatting without making it clickable.</p>
<hr>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><strong>LinkAnnotation API</strong> is the modern way to create clickable links in Compose</li>
<li><strong>Custom URL handler</strong> gives you full control over how links open</li>
<li><strong>Chrome Custom Tabs</strong> provides better UX than default browser</li>
<li><strong>Graceful fallbacks</strong> prevent app crashes on edge cases</li>
<li><strong>Helper functions + stringResource</strong> make your code localization-ready</li>
</ul>
<hr>
<h2 id="why-this-approach-is-better">Why This Approach is Better</h2>
<p>Compared to older methods using <code>pushStringAnnotation</code>, this approach:</p>
<ul>
<li><strong>Type-safe</strong>: LinkAnnotation is strongly typed</li>
<li><strong>Easier styling</strong>: Direct style application per link</li>
<li><strong>Better separation</strong>: Link logic separate from text building</li>
<li><strong>Localization-friendly</strong>: Works seamlessly with multi-language apps</li>
</ul>
<hr>
<h2 id="related-resources">Related Resources</h2>
<ul>
<li><a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/LinkAnnotation">LinkAnitation Documentation</a></li>
<li><a href="https://developer.chrome.com/docs/android/custom-tabs/guide-get-started">Chrome Custom Tabs Guide</a></li>
</ul>
<hr>
<p><strong>Thanks for reading!</strong> If you found this helpful, feel free to reach out via my social handles.</p>
]]></content:encoded></item><item><title>Mastering Raw Queries in Room: Why, When &amp; When Not to Use Them (#OF06)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-6/</link><pubDate>Sun, 25 May 2025 08:18:39 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-6/</guid><description>&lt;p>In the &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">previous article&lt;/a>, we explored the power and flexibility of DAOs with Room’s built-in query annotations. But what if your query needs don’t fit into Room’s constraints? Enter @RawQuery – the most flexible and dangerous tool in the Room arsenal.&lt;/p>
&lt;p>Let’s dive into what &lt;code>@RawQuery&lt;/code> is, when it shines and shuns.&lt;/p>
&lt;hr>
&lt;h2 id="what-is-rawquery">What is @RawQuery?&lt;/h2>
&lt;p>&lt;code>@RawQuery&lt;/code> allows you to execute &lt;strong>SQL statements that aren’t validated at compile time&lt;/strong>. Unlike &lt;code>@Query&lt;/code>, which Room parses and validates during compilation, raw queries are evaluated at runtime.&lt;/p></description><content:encoded><![CDATA[<p>In the <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">previous article</a>, we explored the power and flexibility of DAOs with Room’s built-in query annotations. But what if your query needs don’t fit into Room’s constraints? Enter @RawQuery – the most flexible and dangerous tool in the Room arsenal.</p>
<p>Let’s dive into what <code>@RawQuery</code> is, when it shines and shuns.</p>
<hr>
<h2 id="what-is-rawquery">What is @RawQuery?</h2>
<p><code>@RawQuery</code> allows you to execute <strong>SQL statements that aren’t validated at compile time</strong>. Unlike <code>@Query</code>, which Room parses and validates during compilation, raw queries are evaluated at runtime.</p>
<p>You typically use <code>@RawQuery</code> with either SupportSQLiteQuery (for fully dynamic queries) or plain String (though the latter is limited and discouraged).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Dao</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ProductDao</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@RawQuery</span> 
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductsWithRawQuery</span>(query: SupportSQLiteQuery): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// This will be called from the repository
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Transaction</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductsAbovePrice</span>(minPrice: Int): List&lt;Product&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> query = SimpleSQLiteQuery(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;SELECT * FROM products WHERE price &gt; ?&#34;</span>,
</span></span><span style="display:flex;"><span>        arrayOf(minPrice)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> productDao.getProductsWithRawQuery(query)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="-when-should-you-use-rawquery">✅ When Should You Use @RawQuery?</h3>
<p>Despite its risks, there are legitimate use cases for <code>@RawQuery</code> which come handy when the app is completely offline and need to do operations similar to server with available data for example sorting, filtering and searching etc.</p>
<p>There are some cases where you need advanced joins, unions, subqueries Room’s <code>@Query</code> might fall short an example i can think of are expense list screen where you need to get associated budgets,tags, users who created and approved them, etc getting this merged data specific filter and sort type will be extremely hard with conventional methods in cases like these <code>@RawQuery</code> are a boon.</p>
<p>This is one of my dao&rsquo;s functions that is triggered when the there applies a filter, since there was no network i apply the filter and show the available data assuming the user already knows he is in offline mode,we have an indicator that should handle conveying of the message. So take a look and the params and tell me can we acheve this using conventional method faster than this ? i don&rsquo;t think so but do share your views.</p>
<p>Lets check out my code for</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@RawQuery</span> <span style="color:#75715e">// Query runner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getExpensesWithParamsQueryRunner</span>(query: SupportSQLiteQuery): List&lt;LocalExpenseWithDetails&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Transaction</span> <span style="color:#75715e">// this will be called as a fallback option when offline
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getExpensesWithParams</span>(
</span></span><span style="display:flex;"><span>        type: String?,
</span></span><span style="display:flex;"><span>        budgetId: Long?,
</span></span><span style="display:flex;"><span>        sessionId: Long?,
</span></span><span style="display:flex;"><span>        filterParam: FilterParams,
</span></span><span style="display:flex;"><span>        sortParam: SortParam,
</span></span><span style="display:flex;"><span>        searchQuery: String?,
</span></span><span style="display:flex;"><span>        page: Int
</span></span><span style="display:flex;"><span>    ): List&lt;LocalExpenseWithDetails&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> typeId = <span style="color:#66d9ef">if</span> (type <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">&amp;&amp;</span> type <span style="color:#f92672">!=</span> <span style="color:#a6e22e">BaseExpenseType</span>.ALL) getExpenseTypeWithName(type.serverKey) <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> getExpensesWithParamsQueryRunner(
</span></span><span style="display:flex;"><span>            query = getExpenseQuery( <span style="color:#75715e">// query generator
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                typeId = typeId,
</span></span><span style="display:flex;"><span>                budgetId = budgetId,
</span></span><span style="display:flex;"><span>                sessionId = sessionId,
</span></span><span style="display:flex;"><span>                filterParam = filterParam,
</span></span><span style="display:flex;"><span>                sortParam = sortParam,
</span></span><span style="display:flex;"><span>                search = searchQuery,
</span></span><span style="display:flex;"><span>                page = page
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div><ol start="3">
<li>Performance Testing / Debugging
During development, you might want to test different raw SQL statements quickly without baking them into DAOs.</li>
</ol>
<h3 id="-when-not-to-use-rawquery">⚠️ When NOT to Use @RawQuery</h3>
<p>Just because you can doesn’t mean you should. Raw queries come with trade-offs:</p>
<ul>
<li>❌ No Compile-Time Safety</li>
</ul>
<p>Room won’t validate your SQL. Typos, wrong column names, or invalid SQL will only fail at runtime – often with vague errors.</p>
<ul>
<li>❌ No Type Inference</li>
</ul>
<p>Unlike @Query, Room won’t know what result type to expect unless you specify it manually and correctly.</p>
<ul>
<li>❌ Risk of SQL Injection
If you’re concatenating SQL strings, you open yourself to SQL injection vulnerabilities. Always use parameterized queries or filter the vulnerable queries.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Bad ❌
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> query = SimpleSQLiteQuery(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE name = &#39;</span><span style="color:#e6db74">$name</span><span style="color:#e6db74">&#39;&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Good ✅
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> query = SimpleSQLiteQuery(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE name = ?&#34;</span>, arrayOf(name))
</span></span></code></pre></div><h3 id="-tips-for-safely-using-raw-queries">📋 Tips for Safely Using Raw Queries</h3>
<ul>
<li>✅ Always use SimpleSQLiteQuery with parameterized arguments.</li>
<li>✅ Keep the query logic isolated and well-documented.</li>
<li>✅ Prefer @Query whenever possible.</li>
<li>✅ Avoid user-generated input directly in SQL strings.</li>
<li>✅ Write tests to validate dynamic query paths.</li>
</ul>
<h3 id="-anti-patterns-to-avoid">🚫 Anti-Patterns to Avoid</h3>
<ul>
<li>❌ Using raw queries as your primary query method.</li>
<li>❌ Skipping query reuse – dynamic doesn’t mean you can’t structure it.</li>
<li>❌ Using @RawQuery when @Query or DAO methods would suffice.</li>
<li>❌ Ignoring test coverage for raw query logic.</li>
</ul>
<p>I’ve even created my own query builder to simplify this process in my codebase. Whether you should use it or not depends on your needs, but I’m sharing it just so you know it exists because it was extremely useful for me. The code for my query builder can be found here: <a href="https://gist.github.com/Eganathan/6692e2ea77fe51daf02f9e34a036f1b5">CustomQueryBuilder</a></p>
<h2 id="-conclusion">🚀 Conclusion</h2>
<p><code>@RawQuery</code> is the escape hatch when Room’s abstraction becomes a cage. Use it when needed, but use it wisely. Think of it like the goto statement of Room – powerful but potentially dangerous if overused or misused.</p>
<p>In most offline-first app cases, well-structured DAOs using Room’s annotations will suffice. But in edge cases where flexibility is key, <code>@RawQuery</code> gives you that last-mile control.</p>
<p><strong>Next up, we’ll explore Query Optimization Tips to keep your Offline-First App lightning fast, even at scale.</strong></p>
<p><strong>Next we will explore Data Access Objects</strong>, Stay tuned for the next article in this series! 🚀</p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>Raw queries are sharp tools — excellent in skilled hands, but risky for the unprepared. If you’ve got a use case or edge case where @RawQuery saved your app or made something possible, I’d love to hear it!</p>
<p>Feel free to connect and share your stories:
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong><br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">Previous Article in this Series</a>
🚀 <strong>Stay tuned for Part 7!</strong> 🚀</p>
<!-- 🔖 [Next Article in this Series](https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-7/) -->]]></content:encoded></item><item><title>DAO: The Backbone of Offline-First Apps (#OF05)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-5/</link><pubDate>Sun, 09 Mar 2025 13:18:39 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-5/</guid><description>&lt;p>In our &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">previous article&lt;/a>, we covered &lt;strong>Entities&lt;/strong> in Room. Now, let’s explore &lt;strong>DAOs (Data Access Objects)&lt;/strong> – the bridge between your app and the database. DAOs allow you to execute queries that allow is to insert, update, and delete records efficiently and thats exactly what we are going to explore today.&lt;/p>
&lt;hr>
&lt;h2 id="what-is-a-dao">What is a DAO?&lt;/h2>
&lt;p>A DAO is an interface annotated with @Dao that defines methods for interacting with the database. The Room compiler generates the necessary implementation for these interfaces and its methods, which enables us to access the database efficient and type safe.&lt;/p></description><content:encoded><![CDATA[<p>In our <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">previous article</a>, we covered <strong>Entities</strong> in Room. Now, let’s explore <strong>DAOs (Data Access Objects)</strong> – the bridge between your app and the database. DAOs allow you to execute queries that allow is to insert, update, and delete records efficiently and thats exactly what we are going to explore today.</p>
<hr>
<h2 id="what-is-a-dao">What is a DAO?</h2>
<p>A DAO is an interface annotated with @Dao that defines methods for interacting with the database. The Room compiler generates the necessary implementation for these interfaces and its methods, which enables us to access the database efficient and type safe.</p>
<p>A typical DAO will look simar as the code below:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Dao</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ProductDao</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAllProducts</span>(): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE id = :productId&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductById</span>(productId: Long): Product?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProducts</span>(products: List&lt;Product&gt;)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">updateProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsertProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>While the <code>@Dao</code> interface is created by the developer the instance/Implementation of the DAO&rsquo;s are created by the compiler when the Database is created and the generated implementation file can be looked at the following path:</p>
<blockquote>
<p><code>app/build/generated/source/kapt/debug/YOUR_PACKAGE_NAME/database/ProductDao_Impl.java</code></p>
</blockquote>
<p>The file will contain the instance with <code>impl</code> as post fix and all the corresponding functions will have its own implementation accessing the SQLLite Database for example the <strong>select query</strong> annotated with <code>@Query</code> on the above <strong>DAO</strong> will generate an implementation code that is shared below, <strong>if you are to use SQLLite Directly this is the code you would have to write manually for each query</strong>:</p>
<p>The implementation of select query will look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#a6e22e">@Override</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> Object <span style="color:#a6e22e">getAllProducts</span>(Continuation<span style="color:#f92672">&lt;?</span> <span style="color:#66d9ef">super</span> List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;&gt;</span> continuation) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">final</span> String _sql <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;SELECT * FROM products&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">final</span> RoomSQLiteQuery _statement <span style="color:#f92672">=</span> RoomSQLiteQuery.<span style="color:#a6e22e">acquire</span>(_sql, 0);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> CoroutinesRoom.<span style="color:#a6e22e">execute</span>(__db, <span style="color:#66d9ef">false</span>, continuation, <span style="color:#66d9ef">new</span> Callable<span style="color:#f92672">&lt;</span>List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;&gt;</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Override</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">call</span>() <span style="color:#66d9ef">throws</span> Exception {
</span></span><span style="display:flex;"><span>            Cursor _cursor <span style="color:#f92672">=</span> DBUtil.<span style="color:#a6e22e">query</span>(__db, _statement, <span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>                List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;</span> _result <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> ArrayList<span style="color:#f92672">&lt;&gt;</span>();
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">while</span> (_cursor.<span style="color:#a6e22e">moveToNext</span>()) {
</span></span><span style="display:flex;"><span>                    Product _item <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Product();
</span></span><span style="display:flex;"><span>                    _item.<span style="color:#a6e22e">id</span> <span style="color:#f92672">=</span> _cursor.<span style="color:#a6e22e">getLong</span>(_cursor.<span style="color:#a6e22e">getColumnIndexOrThrow</span>(<span style="color:#e6db74">&#34;id&#34;</span>));
</span></span><span style="display:flex;"><span>                    _item.<span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> _cursor.<span style="color:#a6e22e">getString</span>(_cursor.<span style="color:#a6e22e">getColumnIndexOrThrow</span>(<span style="color:#e6db74">&#34;name&#34;</span>));
</span></span><span style="display:flex;"><span>                    _item.<span style="color:#a6e22e">price</span> <span style="color:#f92672">=</span> _cursor.<span style="color:#a6e22e">getFloat</span>(_cursor.<span style="color:#a6e22e">getColumnIndexOrThrow</span>(<span style="color:#e6db74">&#34;price&#34;</span>));
</span></span><span style="display:flex;"><span>                    _result.<span style="color:#a6e22e">add</span>(_item);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> _result;
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">finally</span> {
</span></span><span style="display:flex;"><span>                _cursor.<span style="color:#a6e22e">close</span>();
</span></span><span style="display:flex;"><span>                _statement.<span style="color:#a6e22e">release</span>();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Isn&rsquo;t it so nice how <strong>instead of writing 24 lines we just have to write 1</strong></p>
<p>🙏 <strong>Thank You, Android and Google Team for making this extremely robust and performant.</strong></p>
<p>Now that we got an intro on <strong>Dao</strong> and what happens behind the scenes, let&rsquo;s go through the supported query annotations.</p>
<h2 id="supported-query-annotations">Supported Query Annotations</h2>
<p>In the previous section of DAO example code, you might have noticed the annotations (<code>@Insert</code>,<code>@Delete</code>&hellip;) on each functions, these annotations help the compiler to generate query object on your behalf so we can smoothly interact with the database, Let&rsquo;s go through each of them to understand how and when to use them effectively.</p>
<h3 id="insert---insert-data-into-the-database">@Insert - Insert Data into the Database</h3>
<p>Used for <strong>inserting single or multiple entries to a table</strong>, this will <strong>skip validation of checking if a duplicate entry with the same primary id exists</strong>, so it&rsquo;s faster for insertion when you are clear that duplicate entries are impossible. In case <strong>you inset a duplicate entry the app will crash</strong> with the exception primary key already exists.</p>
<p>Here is an example of how the <code>@Insert</code> can be used:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Inserting single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Insert</span>(onConflict = <span style="color:#a6e22e">OnConflictStrategy</span>.REPLACE)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProduct</span>(product: Product)     
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Inserting multiple items
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProducts</span>(products: List&lt;Product&gt;)     
</span></span><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span></code></pre></div><h4 id="conflict-strategy">Conflict Strategy</h4>
<p>Defining the conflict strategy on the query tells the room how to handle the conflict effectively by default it <code>NONE</code>:</p>
<ul>
<li><code>NONE</code>: Default strategy when you insert duplicate it crashes the app.</li>
<li><code>REPLACE</code>: Keep the old data and ignore the new data.</li>
<li><code>ABORT</code>: Aborts the transaction entirely so none of the entries will be updated.</li>
<li><code>IGNORE</code>: Skip the entry and proceed to next</li>
</ul>
<p>You can set your preference as per your requirement, but if you want the behavior of <code>REPLACE</code> hold on there is another annotation that we will learn about soon that is a better option.</p>
<p>⚠️ Beware: inserting duplicate entries will crash the app if you don&rsquo;t provide a conflict strategy.</p>
<h3 id="update---modify-existing-data">@Update - Modify Existing Data</h3>
<p>Used for updating all columns of an existing row where the primary key matches, If the table does not contain a matching primary key, the update fails silently (no error or no exception) but the unmatched entries remain unchanged (they are not inserted or modified).</p>
<p>Like the <code>@insert</code> you can <code>@update</code> single or multiple entries, a sample code will look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Updating single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">updateProduct</span>(product: Product)   
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Updating multiple items
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">updateProducts</span>(products: List&lt;Product&gt;)     
</span></span><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span></code></pre></div><p>The update overrides the provided value to matched entries so if you accidentally provide a null value, that is exactly what the matched row&rsquo;s column will be, for example, if you want to mark the product as sold-out you will think you have to send only a product object with <code>id</code> and the <code>sold</code> value but if you do that all other values like name, date, and other values will be set as null so ensure you provide all values.</p>
<p>⚠️ You are overriding all values, so ensure you provide all values in each entry. <br>
⚠️ Unmatched rows will not be inserted, no errors or exceptions will be thrown</p>
<h3 id="upsert---insert-or-update-in-one-call">@Upsert - Insert or Update in One Call</h3>
<p><code>@Upsert</code> is a combination of <code>@Insert</code> and <code>@Update</code>, if the entry matches the <strong>primary-key</strong> it updates them, otherwise it inserts them smoothly, you could use <code>@Insert</code> with <code>REPLACE</code> if your table has no primary key otherwise this is better.</p>
<p>This is an example of how the ``@Upsert` will be used in a dao</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// upsert single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsertProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// upsert bulk Item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsertProducts</span>(products: List&lt;Product&gt;)
</span></span></code></pre></div><p>One thing to note is that the conflict is handled only for the primary key, if your table has other columns that are set as unique the upsert may still fail if that rule is ever broken due to new data.</p>
<p>⚠️ Requires the primary key  <br>
⚠️ If your table has unique constraints on other columns, @Upsert may still fail.</p>
<h3 id="delete---remove-entries-from-the-database">@Delete - Remove Entries from the Database</h3>
<p>Enables you to delete single and multiple entities blissfully, if the row does not exist it fails silently which means that the entries that exist will be deleted and the other will not since it does not exist, here is how typical delete functions will look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// delete single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// delete bulk Item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProducts</span>(products: List&lt;Product&gt;)
</span></span></code></pre></div><p>I don&rsquo;t like to pass the entire entity for deletions so I prefer to use <code>@Query</code> for it so I can just pass the primary keys for deletion, but for simplicity, you can use the <code>@Delete</code> as well.</p>
<h3 id="query---the-most-powerful-annotation">@Query - The Most Powerful Annotation</h3>
<p>The <code>@Query</code> Swiss knife allows you to write custom SQL queries to interact with your database. It provides more flexibility than other annotations (<code>@Insert</code>, <code>@Update</code>, <code>@Delete</code>) and is essential for performing simple/complex operations depending on our use cases.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span><span style="color:#75715e">// bulk get
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAllProducts</span>(): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// single get 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE id = :productId&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductById</span>(productId: Long): Product?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// search Query
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE name LIKE &#39;%&#39; || :searchQuery || &#39;%&#39;&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">searchProducts</span>(searchQuery: String): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// deleting item with ID
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;DELETE FROM products WHERE id = :productId&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProductById</span>(productId: Long)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// delete multiple items
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;DELETE FROM products WHERE id IN (:productIds)&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProductById</span>(productIds: List&lt;Long&gt;)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// deleting all entries (truncates)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;DELETE FROM products&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteAllProducts</span>()
</span></span><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span></code></pre></div><p>Swiss Knife it is, you can do all kinds of operations with it, its gives us more flexibility but as we know &ldquo;With Great Power comes to Great Responsibility&rdquo;. Although <code>@Query</code> is powerful, it can fail in various ways due to syntax errors, missing data, type mismatches, database constraints, etc.. so use it cautiously and expect exceptions and app crashes.</p>
<h3 id="rawquery---fully-dynamic-sql-execution">@RawQuery - Fully Dynamic SQL Execution</h3>
<p>The <code>@RawQuery</code> annotation in Room allows you to write and execute fully dynamic SQL queries, will will explore this later in this series it breaks the simplicity boundary because there is no compile-time safety on this.</p>
<h2 id="-conclusion">🚀 Conclusion</h2>
<p>Data Access Objects <strong>(DAOs) are the backbone of efficient database operations in Room</strong>. They provide a clean, structured way to interact with your database while leveraging compile-time safety and reducing boilerplate SQL code. With annotations like @Insert, @Update, @Delete, @Upsert, and @Query, DAOs make it easier to perform CRUD operations while maintaining performance and maintainability.</p>
<p>By using DAOs, you ensure that your Offline-First App has a seamless data layer, enabling a smooth and efficient user experience.</p>
<p><strong>Next, we will discuss about @RawQuery and its pro&rsquo;s and cons&hellip;</strong></p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong><br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">Previous Article in this Series</a>
🚀 <strong>Stay tuned for Part 6!</strong> 🚀</p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Room Entity and its intricacies (#OF04)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-4/</link><pubDate>Sun, 02 Mar 2025 15:39:05 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-4/</guid><description>&lt;p>In our &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">previous article&lt;/a>, we explored the key components of Room. Now, let’s take a deep dive into Room Entities, their importance, and the various ways to customize them.&lt;/p>
&lt;p>&lt;strong>Entities&lt;/strong> are the foundation of Room—they define how your data is stored in the database. Properly structuring your entity ensures efficient querying, maintainability, and scalability. Let&amp;rsquo;s break it down! 🛠️&lt;/p>
&lt;h2 id="-what-is-an-entity">🏗️ What is an Entity?&lt;/h2>
&lt;p>An &lt;strong>Entity in Room represents a table in the database&lt;/strong>. Each instance of the entity corresponds to a row in the table, Room generates corresponding SQL table schema based on how you create an entity class.&lt;/p></description><content:encoded><![CDATA[<p>In our <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">previous article</a>, we explored the key components of Room. Now, let’s take a deep dive into Room Entities, their importance, and the various ways to customize them.</p>
<p><strong>Entities</strong> are the foundation of Room—they define how your data is stored in the database. Properly structuring your entity ensures efficient querying, maintainability, and scalability. Let&rsquo;s break it down! 🛠️</p>
<h2 id="-what-is-an-entity">🏗️ What is an Entity?</h2>
<p>An <strong>Entity in Room represents a table in the database</strong>. Each instance of the entity corresponds to a row in the table, Room generates corresponding SQL table schema based on how you create an entity class.</p>
<p><strong>Defining an Entity:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p><strong>Breaking It Down:</strong></p>
<ul>
<li><code>@Entity</code> tells Room that this class is a database table.</li>
<li><code>@PrimaryKey</code> is used to uniquely identify each row.</li>
<li><code>autoGenerate = true</code> ensures Room generates unique IDs automatically.</li>
<li><code>@Ignore</code> → Excludes a field from being stored in the database.</li>
</ul>
<p>Before we procced further, Lets check out the sql query generated by the room</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> LocalHabitTracker (
</span></span><span style="display:flex;"><span>    id INTEGER <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> AUTOINCREMENT,
</span></span><span style="display:flex;"><span>    associatedHabitId INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    positionX INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    positionY INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    note TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>Since its a simple entity it might look workable but think of adding indexes, relations and other complicated relations it can quickly get complicated and prone errors. That being said this is just to show you how room is doing most of the heavy lifting for you, lets move on to customizing our entities.</p>
<hr>
<h2 id="-customizing-entities">🔄 Customizing Entities</h2>
<p>Room provides several ways to customize entities to fit your data structure requirements. Let&rsquo;s explore these!</p>
<h3 id="1-custom-table-and-column-names">1️⃣ Custom Table and Column Names</h3>
<p>By default, Room uses the <strong>class name as the table name</strong> and <strong>variable names as column names</strong>. You can override this using annotations:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;habit_id&#34;</span>) <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;pos_x&#34;</span>) <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;pos_y&#34;</span>) <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>tableName changes the SQL table name while keeping the DataClass name as per your requiremnt.</li>
<li>@ColumnInfo(name = &ldquo;custom_name&rdquo;) changes the column name in the table.</li>
</ul>
<p>⚠️ <strong>These customizations are for the DataBase Tables which means when writing the query you must use the customized name for the query to work properly.</strong></p>
<p>This is extremely useful when you want to Name your tables and columns differently, in kotlin we use camelcase but on sql its common to use underscore to split worlds like <strong>habit_tracker</strong> instead of <strong>LocalHabitTracker</strong> it simplifies the query and expedites debugging process.</p>
<p>Using custom name for table and column is optional so if you are comfortable with the existing name you can use just that, next up lets see how can we ensure the entities can be indexed faster.</p>
<h3 id="2-indexing-for-faster-queries">2️⃣ Indexing for Faster Queries</h3>
<p>Indexes speed up query performance, especially for large datasets. Use @Index for frequently queried columns, for example here the <code>associated_habit_id</code> will be used most frequently by me in different queries so i am adding <code>indices</code> property on the <code>@Table</code> annotation, here is an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(
</span></span><span style="display:flex;"><span>    tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>,
</span></span><span style="display:flex;"><span>    indices = [Index(<span style="color:#66d9ef">value</span> = [<span style="color:#e6db74">&#34;associated_habit_id&#34;</span>])]
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;associated_habit_id&#34;</span>)<span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>Indexing <code>habit_id</code> speeds up lookup queries on this column.</li>
</ul>
<h4 id="when-should-you-use-an-index">When Should You Use an Index?</h4>
<p><strong>✅ Use indexing if:</strong></p>
<ul>
<li>The column is used frequently in WHERE, JOIN, or ORDER BY.</li>
<li>The column is a foreign key linking to another table.</li>
</ul>
<p><strong>🚫 Avoid indexing if:</strong></p>
<ul>
<li>The table is small (indexing overhead isn’t worth it).</li>
<li>The column has many unique values (like id, which is already indexed as PRIMARY KEY).</li>
</ul>
<h4 id="what-actually-happens">What actually happens?</h4>
<p>When you add an index to a column in Room, Room <strong>creates a separate data structure called a B-tree index</strong>. This makes queries that search for specific values in the indexed column much faster.</p>
<p>If you want to know more about <code>B-tree Index</code>: check this out <a href="https://en.wikipedia.org/wiki/B-tree">B-Tree Index</a></p>
<p>Now we have learned about indexing lets checkout how to ensure the columns stays unique</p>
<h3 id="3-unique-constraints">3️⃣ Unique Constraints</h3>
<p>Preventing duplicate values on an indexed column is sometimes necessary and we can achieve that by setting the property <code>unique = true</code> if the Index class, lets checkout an example to see how to apply this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(
</span></span><span style="display:flex;"><span>    tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>,
</span></span><span style="display:flex;"><span>    indices = [
</span></span><span style="display:flex;"><span>        Index(<span style="color:#66d9ef">value</span> = [<span style="color:#e6db74">&#34;entry_data&#34;</span>], unique = <span style="color:#66d9ef">true</span>),
</span></span><span style="display:flex;"><span>        Index(<span style="color:#66d9ef">value</span> = [<span style="color:#e6db74">&#34;associated_habit_id&#34;</span>])
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;associated_habit_id&#34;</span>)<span style="color:#66d9ef">val</span> associatedHabitId: Long,  
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;entry_data&#34;</span>)<span style="color:#66d9ef">val</span> entryDate: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>Ensures <code>habit_id</code> remains unique across all rows.</li>
</ul>
<p>The <code>unique = true</code> ensures there is no duplicate value in that particular indexed row, this can be extremely helpful in some cases in our case we are building a habit tracking app where users log their habits daily. We want to ensure that a user cannot log the same habit more than once per day so this property help us to achieve that blissfully.</p>
<p>Lets move on to Keys and Relationships,i meant between the Entities 😜</p>
<h3 id="4-foreign-keys-for-relationships-foreignkey">4️⃣ Foreign Keys for Relationships (@ForeignKey)</h3>
<p>Define relationships between tables using <code>@ForeignKey</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(
</span></span><span style="display:flex;"><span>    tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>,
</span></span><span style="display:flex;"><span>    foreignKeys = [
</span></span><span style="display:flex;"><span>        ForeignKey(
</span></span><span style="display:flex;"><span>            entity = Habit<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>,
</span></span><span style="display:flex;"><span>            parentColumns = [<span style="color:#e6db74">&#34;id&#34;</span>],
</span></span><span style="display:flex;"><span>            childColumns = [<span style="color:#e6db74">&#34;habit_id&#34;</span>],
</span></span><span style="display:flex;"><span>            onDelete = <span style="color:#a6e22e">ForeignKey</span>.CASCADE
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> habit_id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>🔷 habit_id references id from the Habit table.
🔷 onDelete = CASCADE ensures that deleting a Habit deletes all related LocalHabitTracker records.</p>
<p>The <code>@ForeignKey</code> is a primary key of an associated entity in the context of current entity the associated entity is referred as parent entity, using <code>@ForeignKey</code> help us to create relations and trigger changes depending on the event triggered, most commonly used trigger event is <code>onDelete</code> the other one is <code>onUpdate</code> the actions supported are as follows:</p>
<h4 id="supported-actions"><strong>Supported Actions:</strong></h4>
<table>
  <thead>
      <tr>
          <th>Action</th>
          <th>Behavior on Delete/Update</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CASCADE</td>
          <td>Deletes/updates child rows automatically when the parent row is deleted/updated.</td>
      </tr>
      <tr>
          <td>SET NULL</td>
          <td>Sets the foreign key column in child rows to NULL when the parent row is deleted/updated.</td>
      </tr>
      <tr>
          <td>SET DEFAULT</td>
          <td>Sets the foreign key column in child rows to its default value when the parent row is deleted/updated.</td>
      </tr>
      <tr>
          <td>RESTRICT</td>
          <td>Prevents deletion or update of the parent row if child rows exist (throws an error).</td>
      </tr>
      <tr>
          <td>NO ACTION</td>
          <td>Similar to RESTRICT, but the check happens after the statement executes.</td>
      </tr>
  </tbody>
</table>
<p>This table should have given you a clear picture of what each action&rsquo;s behavior and next lets checkout how to select them and when to use them effectively:</p>
<h4 id="when-to-use-each-action"><strong>When to Use Each Action</strong></h4>
<table>
  <thead>
      <tr>
          <th>Action</th>
          <th>When to Use?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CASCADE</td>
          <td>When child records must be removed/updated along with the parent.</td>
      </tr>
      <tr>
          <td>SET NULL</td>
          <td>When child records should remain but lose their reference to the parent.</td>
      </tr>
      <tr>
          <td>SET DEFAULT</td>
          <td>When child records should be assigned a default foreign key value (useful for fallback behaviors).</td>
      </tr>
      <tr>
          <td>RESTRICT</td>
          <td>When you want to prevent deletion or modification of a parent that still has child records.</td>
      </tr>
      <tr>
          <td>NO ACTION</td>
          <td>Similar to RESTRICT, but checks only after execution (rarely used).</td>
      </tr>
  </tbody>
</table>
<p>You can add any number of foreign keys so make use of it for simplifying and automate your preferred action when the parent entity is modified.</p>
<h3 id="5-embedded-objects-embedded">5️⃣ Embedded Objects (@Embedded)</h3>
<p>Instead of creating separate tables, you can embed objects inside an entity, this does not mean that the table will hold your embedded object as is, it basically flattens the properties into the Entity, first lets see how to use <code>@Embedded</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Position</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> x: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> y: Int
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Embedded</span> <span style="color:#66d9ef">val</span> position: Position,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>The Position object is embedded as separate columns (x, y) in LocalHabitTracker,</li>
</ul>
<p>This helps with <strong>structuring data efficiently without creating a separate table for nested objects</strong>, It is primarily used to flatten objects into separate columns within the same table this helps us to avoid using <code>TypeConverters</code>, extra Table, faster queries as we don&rsquo;t have to deal with JOINS and other complications.</p>
<p><strong>When to Use @Embedded:</strong></p>
<ol>
<li>You have small nested objects (e.g., Position, Address, Metadata).</li>
<li>You don’t need a separate table for the object.</li>
<li>You want to avoid TypeConverters for simple objects.</li>
<li>The embedded object is always used with the parent (it has no independent existence).</li>
</ol>
<p><strong>When to Avoid it:</strong></p>
<ol>
<li>The embedded object is large or frequently updated → Use a separate table.</li>
<li>The object has relationships with other entities → Use @Relation instead.</li>
<li>The object needs to be referenced from multiple entities → Use Foreign Keys.</li>
</ol>
<p>I hope this made sense, simply put the <code>@Embedded</code> is only a output structure, while creating the SQL query room will flatten the properties into the entity table, Now if you want to store complex objects like lets say List<String> event that is possible, lets see how to achieve it.</p>
<hr>
<h3 id="6-storing-objects-as-is-typeconverters">6️⃣ Storing Objects as is (TypeConverters)</h3>
<p>Room only supports primitive data types (Int, Long, String, Boolean, etc.) by default. If you want to store complex data types (e.g., List, Date, Enum), you need Type Converters to convert them into a format Room understands ie String, lets look at an example of @TypeConverter:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> androidx.room.TypeConverter
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.gson.Gson
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.gson.reflect.TypeToken
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> java.util.Date
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Converters</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert Date -&gt; Long (for storage)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">fromDate</span>(date: Date?): Long? {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> date<span style="color:#f92672">?.</span>time
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert Long -&gt; Date (for retrieval)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">toDate</span>(timestamp: Long?): Date? {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> timestamp<span style="color:#f92672">?.</span>let { Date(<span style="color:#66d9ef">it</span>) }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert List&lt;String&gt; -&gt; JSON String (for storage)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">fromStringList</span>(list: List&lt;String&gt;?): String {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Gson().toJson(list)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert JSON String -&gt; List&lt;String&gt; (for retrieval)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">toStringList</span>(json: String?): List&lt;String&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Gson().fromJson(json, <span style="color:#66d9ef">object</span> <span style="color:#960050;background-color:#1e0010">: </span><span style="color:#a6e22e">TypeToken</span>&lt;List&lt;String&gt;&gt;() {}.type)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Once a converter is defined it must be added to the DataBase class,here is an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Database</span>(entities = [User<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>], version = <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@TypeConverters</span>(Converters<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>)  <span style="color:#75715e">// Register converters
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AppDatabase</span> : RoomDatabase() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">userDao</span>(): UserDao
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>now you can add it to the entity by annotating it with the the <code>@TypeConverter</code>, here is an example for it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(tableName = <span style="color:#e6db74">&#34;users&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> name: String,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@TypeConverters</span>(Converters<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>) <span style="color:#75715e">// Apply TypeConverter here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> hobbies: List&lt;String&gt;
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The best use-case for this is when you want to store Enums, Dates and List of items, do note the values in the object will be stored but cannot be queried which means you can&rsquo;t search, sort or filter the values so ensure you use them only when absolutely need to like for Storing simple objects (e.g., <code>Address</code>, <code>Date</code>, <code>Enum</code>)</p>
<p>These cover the most important aspects of the <code>@Entity</code> and its properties and how to use them effectively, if you think i have missed any please feel free to add comment and ill definitely add them here so it can be helpful for me and others, before you go lets check the best practices.</p>
<hr>
<h2 id="-best-practices">🔍 Best Practices</h2>
<p>✅ Use <code>autoGenerate = true</code> for <code>@PrimaryKey</code> to avoid conflicts if you don&rsquo;t have a server that provides it.<br>
✅ Optimize query performance with <code>@Index</code>.<br>
✅ Define <code>@ForeignKey</code> relationships to maintain integrity but optional for complex cases.<br>
✅ Use <code>@Ignore</code> for transient fields that shouldn&rsquo;t be stored in the database.<br>
✅ Keep entity classes small and focused—avoid unnecessary logic inside them.<br>
✅ Ensure Proper Indexing: Index frequently queried columns to improve performance. <br>
✅ Use Primitive Types: Room doesn’t support custom objects directly. Use <code>@TypeConverter</code> if needed.</p>
<hr>
<h2 id="-conclusion">🚀 Conclusion</h2>
<p>Room Entities form the foundation of Android&rsquo;s database layer, allowing structured and efficient data management. By following best practices, you ensure scalable and maintainable database architectures.</p>
<p><strong>Next we will explore Data Access Objects</strong>, Stay tuned for the next article in this series! 🚀</p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong><br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">Previous Article in this Series</a>
🚀 <strong>Stay tuned for Part 5!</strong> 🚀</p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Key Components of Room &amp; Manually Creating A Database Instance(#OF03)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-3/</link><pubDate>Sat, 22 Feb 2025 11:23:17 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-3/</guid><description>&lt;h2 id="-intro">👋 Intro&lt;/h2>
&lt;p>On the &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">previous article&lt;/a> we have set-up the Room dependencies and plugins, Now lets get into the primary key components of a room library.&lt;/p>
&lt;p>There are three important components: &lt;strong>Entity&lt;/strong>, &lt;strong>DAO&lt;/strong>&amp;rsquo;s, and &lt;strong>Database&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Entity&lt;/strong> represents a single row of a table. (Table structure)&lt;/li>
&lt;li>&lt;strong>DAO&lt;/strong>&amp;rsquo;s (Data Access Objects) are interfaces to write queries and define operations.&lt;/li>
&lt;li>&lt;strong>Database&lt;/strong> is where you associate entities and include the DAO&amp;rsquo;s you&amp;rsquo;d like to access from outside.&lt;/li>
&lt;/ul>
&lt;p>Let&amp;rsquo;s check it out one by one! 🔍&lt;/p></description><content:encoded><![CDATA[<h2 id="-intro">👋 Intro</h2>
<p>On the <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">previous article</a> we have set-up the Room dependencies and plugins, Now lets get into the primary key components of a room library.</p>
<p>There are three important components: <strong>Entity</strong>, <strong>DAO</strong>&rsquo;s, and <strong>Database</strong>.</p>
<ul>
<li><strong>Entity</strong> represents a single row of a table. (Table structure)</li>
<li><strong>DAO</strong>&rsquo;s (Data Access Objects) are interfaces to write queries and define operations.</li>
<li><strong>Database</strong> is where you associate entities and include the DAO&rsquo;s you&rsquo;d like to access from outside.</li>
</ul>
<p>Let&rsquo;s check it out one by one! 🔍</p>
<hr>
<h2 id="-entities-defining-your-data-structure">🏗️ Entities: Defining Your Data Structure</h2>
<p>Any class annotated with <code>@Entity</code> is called a entity, it represents a table in a database, while designing the entity ensure you follow <a href="https://www.geeksforgeeks.org/normal-forms-in-dbms/">Normalizations Rules</a> to keep it simple and future proof. Let&rsquo;s look checkout a sample entity in a Habit Tracker app.</p>
<p><strong>Example:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p><strong>Key Points:</strong></p>
<p>✅ An entity <strong>must have</strong> at least one parameter annotated with @PrimaryKey. <br>
✅ The <strong>primary key</strong> must be unique the <code>primaryKey</code> column can&rsquo;t have a duplicate key. <br>
✅ If you want Room to <strong>auto-generate</strong> the primary key, set autoGenerate = true otherwise to false. <br>
✅ Stick to <strong>primitive types</strong> like Long or Int as PrimaryKey.</p>
<p>The entity can be further customized with <strong>table names, indexing, custom column names, keys and more</strong>, which we’ll cover in next article. 🎯</p>
<h2 id="-daos-your-data-gateway">🔗 DAOs: Your Data Gateway</h2>
<p>DAOs or Data Access Objects serve as the interface between your app and the database, providing an abstraction layer. This is a bridge between you the developer and the Database all your interactions you intend to have table must be defined here later at compile time the compiler will generate the concrete classes for these at compile time, lets see an example how to define these interactions.</p>
<p><strong>✍️ Example:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Dao</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">HabitTrackerDao</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Query to get all habit trackers
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM LocalHabitTracker&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAll</span>(): List&lt;LocalHabitTracker&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Query to get a habit tracker by ID
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM LocalHabitTracker WHERE id = :id&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getById</span>(id: Long): LocalHabitTracker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Insert a new row into the table (fails if duplicate)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insert</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Update an existing row
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">update</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Upsert: If exists, update; otherwise, insert
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsert</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Delete a row from the table
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">delete</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Best Practices:</strong></p>
<p>✅ <strong>One entity per DAO:</strong> It’s best to separate interaction with each table into its own DAO for maintainability.  <br>
✅ <strong>Keep queries optimized</strong> to avoid performance issues as your app scales.  <br>
✅ <strong>Inheritance</strong> if some of the interactions are common to other Dao&rsquo;s create a new Dao with <code>Common</code> as prefix for example<code>CommonHabitTrackerDao</code>.</p>
<p>We’ll cover complex cases like @RawQuery, Junctions, and TableViews in later articles as to not over-complicate this! 🔮</p>
<h2 id="-database-the-central-hub">🏛️ Database: The Central Hub</h2>
<p>Now that we have defined the entities and DAOs, we need to create a database to integrate thee entity and Dao&rsquo;s into its domain. Now the room can effectively generate its contents when instantiated along with this the database also manages the integrity validation, migrations and works as a central hub of control for our interactions with this database and its entities.</p>
<p>Here is an <strong>example</strong> of a database class:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Database</span>(
</span></span><span style="display:flex;"><span>    entities = [LocalHabitTracker<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>], <span style="color:#75715e">// Add all your entities here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    version = <span style="color:#ae81ff">1</span> <span style="color:#75715e">// Update this when modifying the schema
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HabitTrackerDataBase</span> : RoomDatabase() {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Room will auto-implement this function and return the DAO implementation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">habitTrackerDao</span>(): HabitTrackerDao
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Important Notes:</strong></p>
<p>✅ Always increase the version when modifying entities, it enables room to validate the integrity of tables and run migrations effectively. <br>
✅ Room doesn’t know how to handle schema changes unless you define a migration strategy. <br>
✅ Multiple databases can exist in an app, so this class serves as an entry point of the particular database.</p>
<p>Migrations and schema updates are crucial for real-world apps to prevent data loss. We’ll cover on later articles. 🔄</p>
<h2 id="-manually-creating-and-using-the-database">🎛️ Manually Creating and Using the Database</h2>
<p>Now, let’s see how to manually create and interact with the database in our app.</p>
<p><strong>✍️ Example:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MainActivity</span> : AppCompatActivity() {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">onCreate</span>(savedInstanceState: Bundle?) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">super</span>.onCreate(savedInstanceState)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Creating the database manually
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> habitTrackerDB = <span style="color:#a6e22e">Room</span>.databaseBuilder(
</span></span><span style="display:flex;"><span>            context = <span style="color:#66d9ef">this</span>, <span style="color:#75715e">// ApplicationContext
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            klass = HabitTrackerDataBase<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>.java, <span style="color:#75715e">// Database abstract class
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            name = <span style="color:#e6db74">&#34;habbit_tracker&#34;</span> <span style="color:#75715e">// Custom database name
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        ).build() 
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Get the DAO implementation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> habitTrackerDao = habitTrackerDB.habitTrackerDao()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        setContent {
</span></span><span style="display:flex;"><span>            AppTheme {
</span></span><span style="display:flex;"><span>                AppNav(activity = <span style="color:#66d9ef">this</span>, habitLocalService = habitTrackerDao)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>⚡ Pro Tips:</strong></p>
<p>✅ Don’t use the UI thread for database operations; always use coroutines or background threads. <br>
✅ In real-world apps, use Dependency Injection (DI) for managing database instances efficiently.  <br>
✅ Avoid creating multiple database instances, as it can lead to memory leaks and performance issues.</p>
<p>Mostly this is enough for creating simple CRUD apps, though it looks simple the entities will be converted into a query to create tables, A concrete classes will be created for each Dao&rsquo;s and other essential tasks will be carried by room it self easing our development and debug process.</p>
<h2 id="-whats-next">🚀 What’s Next?</h2>
<p>This was a quick run through of some of the key components of room, on the next article we will dive a little deeper into <strong>@Entity</strong> its keys and customizations,as we have a larger purpose to explore them each in detail.</p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">Previous Article in this Series</a></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Setting-up Room Library and its dependencies(#OF02)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-2/</link><pubDate>Mon, 10 Feb 2025 10:02:29 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-2/</guid><description>&lt;p>Before we proceed with the setup, let’s quickly recap &lt;strong>why&lt;/strong> we chose Room:&lt;/p>
&lt;p>✅ &lt;strong>Compile-time SQL Validation&lt;/strong> – Catches errors early by verifying SQL queries at compile time.&lt;br>
✅ &lt;strong>Kotlin-first Approach&lt;/strong> – Supports coroutines, Flow, and LiveData natively.&lt;br>
✅ &lt;strong>Less Boilerplate Code&lt;/strong> – Simplifies database interactions while maintaining SQLite’s power.&lt;br>
✅ &lt;strong>Seamless Migration Handling&lt;/strong> – Built-in support for database migrations.&lt;/p>
&lt;p>For a more detailed explanation, refer to the &lt;a href="https://developer.android.com/training/data-storage/room">official Android documentation&lt;/a>.&lt;/p></description><content:encoded><![CDATA[<p>Before we proceed with the setup, let’s quickly recap <strong>why</strong> we chose Room:</p>
<p>✅ <strong>Compile-time SQL Validation</strong> – Catches errors early by verifying SQL queries at compile time.<br>
✅ <strong>Kotlin-first Approach</strong> – Supports coroutines, Flow, and LiveData natively.<br>
✅ <strong>Less Boilerplate Code</strong> – Simplifies database interactions while maintaining SQLite’s power.<br>
✅ <strong>Seamless Migration Handling</strong> – Built-in support for database migrations.</p>
<p>For a more detailed explanation, refer to the <a href="https://developer.android.com/training/data-storage/room">official Android documentation</a>.</p>
<hr>
<h2 id="official-documentation">Official Documentation</h2>
<p>Google keeps its documentation up to date with a simple setup guide. However, you might wonder why we need this article if the documentation is available.</p>
<p>Google’s documentation caters to <strong>both Java and Kotlin</strong> developers, but since most of us have migrated to <strong>Kotlin</strong>, we don’t need Java-specific instructions. This article focuses on a <strong>Kotlin-first approach</strong> to setting up Room.</p>
<p>📖 <a href="https://developer.android.com/jetpack/androidx/releases/room">Official Guide on Setting up Room Dependencies and Plugins</a>.</p>
<hr>
<h2 id="setting-up-dependencies">Setting up Dependencies</h2>
<h3 id="1-configure-project-level-buildgradlekts">1️⃣ Configure Project-Level <code>build.gradle.kts</code></h3>
<p>Ensure your project-level <code>build.gradle.kts</code> file includes <strong>Google’s Maven repository</strong> and <strong>Maven Central</strong>, as these are where Room dependencies are hosted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>buildscript {
</span></span><span style="display:flex;"><span>    repositories {
</span></span><span style="display:flex;"><span>        google()
</span></span><span style="display:flex;"><span>        mavenCentral()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="2-add-room-dependencies">2️⃣ Add Room Dependencies</h3>
<p>Head to your module-level build.gradle.kts file and add only the required Room dependencies inside the dependencies block:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>dependencies {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Other dependencies 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Room Dependencies
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> room_version = <span style="color:#e6db74">&#34;2.6.1&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Room Database Core
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    implementation(<span style="color:#e6db74">&#34;androidx.room:room-runtime:</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Annotation Processor (KSP)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    ksp(<span style="color:#e6db74">&#34;androidx.room:room-compiler:</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Kotlin Extensions and Coroutines support for Room (Optional)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    implementation(<span style="color:#e6db74">&#34;androidx.room:room-ktx:</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>🔎 Check for Latest Versions
Always use the latest stable version of Room and KSP:</p>
<p>📌 <a href="https://mvnrepository.com/artifact/androidx.room/room-runtime">Maven Central - Room Runtime</a>
📌 <a href="https://github.com/google/ksp/releases">kotlin-ksp-releases</a></p>
<h3 id="3-add-the-room-plugin">3️⃣ Add the Room Plugin</h3>
<p><strong>Project-Level build.gradle.kts</strong>
Add the Room plugin reference to the project-level build.gradle.kts file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>plugins {
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;androidx.room&#34;</span>) version <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span> apply <span style="color:#66d9ef">false</span> 
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;com.google.devtools.ksp&#34;</span>) version <span style="color:#e6db74">&#34;2.0.21-1.0.27&#34;</span> apply <span style="color:#66d9ef">false</span> 
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Other plugins...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p><strong>Module-Level build.gradle.kts</strong>
Add this to the module-level <code>build.gradle</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>plugins {
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;androidx.room&#34;</span>)
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;com.google.devtools.ksp&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="4-configure-schema-directory">4️⃣ Configure Schema Directory</h3>
<p>Room allows schema export for better version control and database migrations. Let’s specify a schema directory inside the android {} block:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>android {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span>    room {
</span></span><span style="display:flex;"><span>        schemaDirectory(<span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$projectDir</span><span style="color:#e6db74">/schemas&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="sync--verify-installation">Sync &amp; Verify Installation</h3>
<p>Once you’ve added the dependencies and plugin, sync your Gradle project. If everything is set up correctly, you should have Room ready for use in our project! 🎉</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./gradlew build 
</span></span></code></pre></div><hr>
<h3 id="sample-code-for-a-quick-reference">Sample Code for a quick reference</h3>
<p>I hope you had no issues setting up the dependencies, if you need a sample app to compare the change-set feel free to checkout this <a href="https://github.com/Eganathan/jotters-space-android/commit/92613b0b3f1e08709b94752a5276d6031466e16a">commit</a>.</p>
<hr>
<h2 id="whats-next"><strong>What’s Next?</strong></h2>
<p>This short article we have learned how to set-up the room dependencies to our projects and hopefully this was helpful:
📌 <strong>In the next part of this series, we’ll dive into:</strong></p>
<p>1️⃣ <strong>Key Components of Room</strong></p>
<ul>
<li><strong>@Entity</strong>: Defines how <strong>data is stored</strong> in the table.</li>
<li><strong>@Dao</strong>: Specifies <strong>how to interact</strong> with the table (CRUD operations).</li>
<li><strong>@Database</strong>: Configures the database and <strong>associates Entities and DAOs</strong>.</li>
</ul>
<p>2️⃣ <strong>How to create, access and interact with a simple database</strong>.</p>
<hr>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong>    <br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-1/">Previous Article in this Series</a></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Upgrading Your App to Offline First With Room (#OF01)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-1/</link><pubDate>Sun, 09 Feb 2025 09:23:57 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-1/</guid><description>&lt;h2 id="why-should-you-care">Why Should You Care?&lt;/h2>
&lt;p>Making your app &lt;strong>offline-first&lt;/strong> is essential if you want to provide the &lt;strong>best user experience&lt;/strong>. It makes your app significantly &lt;strong>faster&lt;/strong> by reducing the number of network calls, which in turn also &lt;strong>reduces server costs&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="what-does-offline-first-mean">What does Offline-First mean?&lt;/h2>
&lt;p>The core idea is to &lt;strong>persist/store remote-fetched data on the device&lt;/strong>. This allows users to access the data &lt;strong>instantly&lt;/strong>, skipping unnecessary network requests—until the data expires or is invalidated.&lt;/p></description><content:encoded><![CDATA[<h2 id="why-should-you-care">Why Should You Care?</h2>
<p>Making your app <strong>offline-first</strong> is essential if you want to provide the <strong>best user experience</strong>. It makes your app significantly <strong>faster</strong> by reducing the number of network calls, which in turn also <strong>reduces server costs</strong>.</p>
<hr>
<h2 id="what-does-offline-first-mean">What does Offline-First mean?</h2>
<p>The core idea is to <strong>persist/store remote-fetched data on the device</strong>. This allows users to access the data <strong>instantly</strong>, skipping unnecessary network requests—until the data expires or is invalidated.</p>
<p>If your app has this feature then your app is offline-first if it does not then lets make it one.</p>
<hr>
<p>While offline-first apps come with some complexities, the benefits far outweigh the challenges. but before we start a small disclaimer.</p>
<h2 id="a-quick-disclaimer">A Quick Disclaimer</h2>
<p>There are <strong>multiple ways</strong> to approach offline-first implementation. What I discuss here is <strong>my approach</strong>, tailored to my requirements. There might be <strong>better methods</strong> for different scenarios so take these as <strong>suggestions and not rules</strong>.</p>
<p>If you have suggestions, feedback, or alternative approaches, I’d love to discuss them! Let’s learn and refine this together. As I explore further, I will <strong>actively update</strong> these articles with new insights/approaches/strategies.</p>
<p>Now, let’s get started. 🚀</p>
<hr>
<h2 id="offline-strategies-partial-vs-full-offline-mode">Offline Strategies: Partial vs. Full Offline Mode</h2>
<p>There are <strong>two main strategies</strong> for implementing offline-first functionality:</p>
<ol>
<li><strong>Fully Offline Mode</strong></li>
<li><strong>Partial Offline Mode</strong></li>
</ol>
<p>The <strong>main difference</strong> between them is whether the user can modify their data offline.</p>
<h3 id="1-fully-offline-mode-crud-operations-offline">1️⃣ Fully Offline Mode (CRUD Operations Offline)</h3>
<p>In this approach, users can:<br>
✅ Create, Read, Update, and Delete (CRUD) <strong>without an internet connection</strong>.<br>
✅ Sync data to the cloud <strong>when online</strong>.</p>
<p>🔴 <strong>Major Challenge</strong>:</p>
<ul>
<li>If a user <strong>forgets to sync</strong> data from one device (e.g., a tablet) and later accesses their account from another device (e.g., a phone), they might think the data is lost.</li>
<li>This can <strong>frustrate users</strong>, leading to complaints or abandoning our app.</li>
</ul>
<p>🔵 <strong>Idea Use case</strong>:</p>
<p>Best option for apps that does not support multi user and has single device login, but ⚠️ The data loss % is still significantly high if user forgets to connect online after the initial login and loses his device etc, but thats a tradeoff you have to live with.</p>
<p>While this approach offers <strong>true offline functionality</strong>, it introduces <strong>complex synchronization issues</strong>, making it harder to maintain <strong>data consistency across devices</strong>.</p>
<h3 id="2-partial-offline-mode-read-only-when-offline">2️⃣ Partial Offline Mode (Read-Only When Offline)</h3>
<p>In this approach, users can:<br>
✅ <strong>View/Read data</strong> offline.<br>
❌ <strong>Modify data (Create, Update, Delete) only when online</strong>.</p>
<p>🔹 <strong>Why We Chose This Approach</strong>:</p>
<ul>
<li><strong>Multi-device users won’t face sync issues</strong> since data is always available in cloud.</li>
<li><strong>Less complexity &amp; better error handling &amp; almost no possibility of data loss</strong>.</li>
<li><strong>Data remains clean and consistent</strong> with the server.</li>
</ul>
<p>🔵 <strong>Idea Use case</strong>:</p>
<p>Best option for most use-cases simple or complex or extremely complex data set apps, with or without <strong>multi-user</strong> or <strong>multiple-device-logins</strong> and what not.</p>
<p>This is most preferred due to its <strong>data-consistency</strong> point and much simpler to implement, Now checkout the next one.</p>
<h3 id="3-hybrid-offline-mode-partially-restricted-write-operations">3️⃣ Hybrid Offline Mode (Partially-restricted Write operations)</h3>
<p>The Hybrid mode is using both discussed strategies depending on the use cases, for example you may allow the user to update their profile details or personal notes etc but not the public contents that can cause issue with other uses on the same org/team.</p>
<p>✅ <strong>View/Read data</strong> offline. <br>
✅ <strong>Modify some data even if offline</strong>.<br>
❌ <strong>Modify most data only if online</strong>.</p>
<p>🔵 <strong>Idea Use case:</strong></p>
<p>Best option for most simple or complex apps, with or without multi-user but <strong>strictly allow only single-device-login</strong>, this enables the user to do write-operations to user specific data while restricting the same for other parts of the app, there are tradeoffs here as well but only a particular user will be affected in the org/team.</p>
<p>For me the <strong>partial offline mode</strong> stands out as best option and would suggest the same for most use-cases, we will be sticking with this through out this journey.</p>
<p>Now, let’s dive into <strong>how we fetch and synchronize data</strong>.</p>
<hr>
<h2 id="data-synchronization-strategies"><strong>Data Synchronization Strategies</strong></h2>
<p>Once we are decided on <strong>partial offline mode</strong>, the next question is:<br>
👉 <strong>How do we fetch data from the server and keep it updated?</strong></p>
<p>There are two main synchronization approaches:</p>
<h3 id="1-on-demand-pull-based-synchronization">1️⃣ On-Demand (Pull-Based Synchronization)</h3>
<ul>
<li><strong>Data is fetched only when the user requests it.</strong></li>
<li>Data is <strong>invalidated</strong> when it <strong>expires</strong> or is <strong>manually deleted</strong>.</li>
<li><strong>Lightweight &amp; user-centric</strong>—fetches only relevant data.</li>
</ul>
<h3 id="2-clone-push-based-synchronization">2️⃣ Clone (Push-Based Synchronization)</h3>
<ul>
<li>The local database <strong>mirrors the entire remote database</strong>.</li>
<li><strong>Auto-updates</strong> when the server sends a flag indicating it was modified.</li>
<li><strong>Heavy &amp; resource-intensive</strong>, as it may download <strong>unnecessary</strong> data.</li>
</ul>
<p>🔹 <strong>Which One Did We Pick?</strong><br>
We chose <strong>On-Demand Synchronization</strong> since it’s:<br>
✅ <strong>Efficient</strong> (fetches only what’s needed).<br>
✅ <strong>Faster &amp; lightweight</strong> (minimizes unnecessary data transfers).</p>
<p>However, we use mox of both lets call it a <strong>hybrid approach</strong>, we loaded some data that user will need on the navigating screen to ensure a better user experience.  After that, everything is <strong>strictly on-demand</strong> the data is synced only based on user.</p>
<hr>
<h2 id="which-local-database-library-should-you-choose"><strong>Which Local Database Library Should You Choose?</strong></h2>
<p>There are several <strong>local database solutions</strong> available, but we chose <strong>Google’s Room Persistence Library</strong>. Here’s why:</p>
<p>✅ <strong>Built on SQLite</strong> – but with a modern, developer-friendly API.<br>
✅ <strong>Works seamlessly with Kotlin</strong> (supports data classes).<br>
✅ <strong>Compile-time SQL query verification</strong> (reduces errors).<br>
✅ <strong>Built-in support for LiveData, Flow, and Paging</strong>.<br>
✅ <strong>Faster development</strong> (less boilerplate, easy migrations).</p>
<p>Using <strong>Room</strong> significantly <strong>accelerates development</strong> while maintaining a structured and reliable database. It’s my <strong>go-to solution</strong>, and I highly recommend it for <strong>Android and Kotlin Multiplatform (KMP) projects</strong>.</p>
<hr>
<h2 id="whats-next"><strong>What’s Next?</strong></h2>
<p>This article laid the foundation for an <strong>offline-first architecture</strong> and explained <strong>why we chose partial offline mode &amp; on-demand synchronization</strong>.</p>
<p>📌 <strong>In the next part of this series, we’ll dive into:</strong></p>
<ul>
<li><strong>Setting up Room in an Android</strong>.</li>
<li><strong>Exploring Key Components of Room</strong>.</li>
</ul>
<hr>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong>   <br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong>     <br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">Next Article in this Series</a></p>
]]></content:encoded></item></channel></rss>