<?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>Kotlin on MangoDriod</title><link>https://md.eknath.dev/tags/kotlin/</link><description>Recent content in Kotlin 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/kotlin/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></channel></rss>