<?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>MangoDriod</title><link>https://md.eknath.dev/</link><description>Recent content on MangoDriod</description><generator>Hugo -- 0.141.0</generator><language>en-us</language><lastBuildDate>Sun, 26 Apr 2026 10:25:15 +0530</lastBuildDate><atom:link href="https://md.eknath.dev/index.xml" rel="self" type="application/rss+xml"/><item><title>AI Command Center to manage multiple projects [Expriment]</title><link>https://md.eknath.dev/posts/software-development/ai-command-center-expriment/</link><pubDate>Sun, 26 Apr 2026 10:25:15 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/ai-command-center-expriment/</guid><description>&lt;p>a team of 20+ handed over a project to our team of 5. i ended up owning all the native clients: iOS, Android, Windows by mostly bymyself. There are multiple repos, multiple languages and the hardship of understanding thier 7 year codebase.&lt;/p>
&lt;p>initially the thought of this was pretty stressful, but being a good solutionist i proceeded with dicecting the issues one by one and presistent on finding a solition for them all.&lt;/p></description><content:encoded><![CDATA[<p>a team of 20+ handed over a project to our team of 5. i ended up owning all the native clients: iOS, Android, Windows by mostly bymyself. There are multiple repos, multiple languages and the hardship of understanding thier 7 year codebase.</p>
<p>initially the thought of this was pretty stressful, but being a good solutionist i proceeded with dicecting the issues one by one and presistent on finding a solition for them all.</p>
<p>The first thing is prioritizing platoforms, The stats were clear hence shared them with my manager, got iOS, Android prioritized in order and parked Windows for later as usage is extremly low. now the priority is set to the next thing.</p>
<p>The problem was not the workload. it was the cognitive overhead. i can barely understand my own code after a few months. here i am inheriting code from 6-7 people who clearly took shortcuts to ship fast. folder structure was a dump. files everywhere. and on top of reverse-engineering three codebases i had to track what is done, what is next, what is blocked: simultaneously, across all platforms this task alone without AI would be a real pain and would def take more than few months.</p>
<p>I opted to the $20 Claude subscription. so every session i was wasting the first chunk of context re-explaining the same codebase to Claude, re-orienting it to where i left off, re-answering questions it should already know. by lunch i was running out of tokens i had to use my colleague&rsquo;s accounts to do whole thing again (Thanks Arun!)</p>
<p>out of this mess i needed a single place. one terminal. one browser tab. one AI session that already knows everything and picks up exactly where we left off, an interface for me and claude to read/update/learn about the project im working and it has to be highly structured, organized and prioritized.</p>
<p>so i built one. i call it the <strong>Command Center</strong>. if you have any other name to suggest, my inbox is open. this post is the full breakdown of what it is, how it works, and how you can shape it for your your own structure if you ever are in that spot, a little future pridction i think by the end of 2026 we all might have to work in this kind of set-up working on multiple projects simultaniously or atleast we will be capabble of that level of productivity.</p>
<hr>
<h2 id="what-is-this-ai-command-center">What is this AI-Command Center</h2>
<p><img alt="The Command Center&rsquo;s Dashboard Home Screen — This is the primary human interface rendered in the browser tab with everything: repo status, today&rsquo;s focus, quick links, docs and more" loading="lazy" src="/img/command-center-images/cce-home.png"></p>
<p>the idea is simple. instead of having our docs, tasks, changelogs in multiple apps and your terminal tabs scattered across screens for different platforms wokfing with different stages of the tasks or even totally different task altogether: and your daily runner claude-code has no means to know about all these instead you put a single shared operational layer above all your repos. not inside them. above them hence the term command center.</p>
<pre tabindex="0"><code>MyWorkspace/
├── ios/               ← own git repo, untouched
├── ios-dataKit/       ← own git repo, untouched
├── android/           ← own git repo, untouched
├── win/               ← own git repo, untouched
└── .ops/              ← Command Center (its own git repo)
    ├── docs/          ← documentation per project
    ├── todo/          ← task files per project + daily focus
    ├── memo/          ← decisions, KT notes, research
    ├── diagrams/      ← architecture diagrams + companion notes
    ├── changelog/     ← per-project ship history
    ├── scripts/       ← automation scripts
    └── dashboard/     ← local web dashboard
</code></pre><p>Don&rsquo;t worry the project folders stay completely independent. <code>git -C ios/ status</code> never bleeds into <code>git -C android/ status</code>. you can add or remove a project folder without touching anything else, this is very important sepearation for corporate repositories where the restrictions are tight.</p>
<p>almost everything in <code>.ops/</code> is a markdown, here is where it might looks like the WiKi pattern shared by the Andrej Karpathy. it is readable by me and the AI, diffs cleanly, and has zero dependencies you can stop here is you want a bare simple command-center but if you are like me this is not enough and there are many flaws here like doc&rsquo;s going stale as we make changes so lets get to the text stage.</p>
<hr>
<h2 id="the-scripts-are-the-real-mvp">The scripts are the real MVP</h2>
<p>As i said earlier i still use the 20$ subscription so for me tokens are really valuable running out of limit means one less productive day that might gift me a day of guilt so making use of scripts to save some routine commands that im sure will be helpful for claude to not be too dependent on remote calls, it might be confusting so here is example:</p>
<p>when i ask Claude &ldquo;what is the git status across all my repos?&rdquo;  Claude tries to figure it out by calling tools one at a time burning tokens on reasoning and multiple tool calls to give me accurate and proper response</p>
<p>but if you have a script that does it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># .ops/scripts/git_status_all.sh</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> dir in android ios ios-dataKit ios-textEditor win; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    branch<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>git -C <span style="color:#e6db74">&#34;</span>$ROOT<span style="color:#e6db74">/</span>$dir<span style="color:#e6db74">&#34;</span> branch --show-current 2&gt;/dev/null<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>    changes<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>git -C <span style="color:#e6db74">&#34;</span>$ROOT<span style="color:#e6db74">/</span>$dir<span style="color:#e6db74">&#34;</span> status --porcelain | wc -l | tr -d <span style="color:#e6db74">&#39; &#39;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$changes<span style="color:#e6db74">&#34;</span> -gt <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#e6db74">&#34;● </span>$dir<span style="color:#e6db74"> — </span>$branch<span style="color:#e6db74"> (</span>$changes<span style="color:#e6db74"> uncommitted)&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>        echo <span style="color:#e6db74">&#34;● </span>$dir<span style="color:#e6db74"> — </span>$branch<span style="color:#e6db74"> (clean)&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>Claude runs the script. gets the answer in one shot. no reasoning, no guessing, no tool call loop you don&rsquo;t have to write this manually you can just ask it to do, just make sure you get the code though.</p>
<p>the scripts i have accumulated up over time:</p>
<table>
  <thead>
      <tr>
          <th>Script</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>briefing.sh</code></td>
          <td>morning snapshot: repo status, high-priority tasks, daily focus, doc staleness — all in one output</td>
      </tr>
      <tr>
          <td><code>session_context.sh</code></td>
          <td>generates a JSON briefing injected into Claude context at session start via a hook</td>
      </tr>
      <tr>
          <td><code>daily_reset.sh</code></td>
          <td>resets <code>daily.md</code> to today, carries over incomplete items, pulls high-priority tasks from project todos</td>
      </tr>
      <tr>
          <td><code>git_status_all.sh</code></td>
          <td>git status across all repos in one command</td>
      </tr>
      <tr>
          <td><code>git_pull_all.sh</code></td>
          <td>pull latest on all repos</td>
      </tr>
      <tr>
          <td><code>git_branch_all.sh</code></td>
          <td>current branch per repo</td>
      </tr>
      <tr>
          <td><code>doc_sync.js</code></td>
          <td>diffs each doc&rsquo;s last-verified commit against HEAD, flags stale docs</td>
      </tr>
      <tr>
          <td><code>pre-push-codecheck.sh</code></td>
          <td>pre-push validation — lint, build check, etc. per platform</td>
      </tr>
      <tr>
          <td><code>log_token_saving.py</code></td>
          <td>PostToolUse hook — logs each local MCP call with estimated tokens saved, reminds Claude to tag responses with <code>[local-command-center-mcp]</code></td>
      </tr>
  </tbody>
</table>
<p>the pattern is always same: take something that would require Claude to do many tool calls or make assumptions, turn it into one script, let Claude just run it and read the output. you get a more reliable answer and you spend a fraction of the tokens.</p>
<p>the <code>session_context.sh</code> one is worth explaining. it runs as a session-start hook and injects the project context automatically before i type anything:</p>
<pre tabindex="0"><code>=== SESSION BRIEFING (2026-04-26) ===

REPOS
  ios          dev_eganathan   clean
  android      dev_eganathan   clean
  ios-dataKit  dev_eganathan   14 uncommitted  ← needs attention

HIGH PRIORITY (ios)
  - sessionId hardcoded as &#34;&#34; (TIBConverseInteractor.swift:83)
  - Localization migration uncommitted

LAST SESSION: 2026-04-22 — ios folder restructure, Settings flatten done
DOC SYNC: 8 stale docs — run /sync
=====================================
</code></pre><p>this replaces re-explaining the codebase every session. the entire briefing is generated from actual file state and costs about 100 tokens to inject i shared only a small portion of this but it basically added more relavant contexts that im sure will help claude so compare that to the 500–1000 tokens you&rsquo;d spend manually orienting Claude each time.</p>
<hr>
<h2 id="documentation-that-doesnt-go-stale">Documentation that doesn&rsquo;t go stale</h2>
<p><img alt="Docs folder — each file has watches frontmatter so the sync script knows exactly which code changes make it stale." loading="lazy" src="/img/command-center-images/cce-doc-folder.png"></p>
<p>the biggest problem with docs is they go stale the moment you stop actively maintaining them. and the honest truth is most people stop maintaining them pretty quickly.</p>
<p>so instead of relying on discipline, i wired it into the workflow. every doc has frontmatter that declares what code it describes:</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-yaml" data-lang="yaml"><span style="display:flex;"><span>---
</span></span><span style="display:flex;"><span><span style="color:#f92672">project</span>: <span style="color:#ae81ff">ios</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">watches</span>: <span style="color:#ae81ff">ios/Features/Inbox/**, ios/Core/Network/**</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">lastVerified</span>: <span style="color:#ae81ff">a3f9c12</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">verifiedDate</span>: <span style="color:#e6db74">2026-04-20</span>
</span></span><span style="display:flex;"><span>---
</span></span></code></pre></div><p><code>watches</code> is a glob pattern over source paths. <code>lastVerified</code> is the git commit hash when i last checked this doc.</p>
<p><code>doc_sync.js</code> runs at session start: diffs <code>lastVerified</code> against HEAD per project, filtered by <code>watches</code>. if watched files changed, the doc is flagged stale. you get a list of exactly which docs need attention — not all of them, just the ones where the underlying code actually changed.</p>
<p>the workflow: read the diff, update the doc if needed, run <code>--mark-current</code> to stamp it with the new HEAD. stale docs are a session-start action item, not a quarterly effort.</p>
<p>one more thing worth building: a <code>doc_sync_prompt.md</code> template. when <code>doc_sync.js</code> flags a doc as stale, you need to give Claude a consistent prompt for reviewing it. the template fills in <code>{{DOC_PATH}}</code>, <code>{{GIT_DIFF_STAT}}</code>, <code>{{CHANGED_FILES}}</code>, and <code>{{DOC_CONTENT}}</code> — Claude reads the diff, decides what changed, updates only what is wrong or outdated, and preserves the frontmatter format. without a template you end up writing a different prompt every time and the quality of the review varies. one template file in <code>scripts/</code>, referenced whenever <code>/sync</code> runs.</p>
<hr>
<h2 id="task-management-across-platforms">Task management across platforms</h2>
<p><img alt="Per-platform todo files with priority buckets — high, medium, low, and completed. Daily.md pulls from these each morning." loading="lazy" src="/img/command-center-images/cce-todos.png"></p>
<p>one file per platform. <code>ios_todos.md</code>, <code>android_todos.md</code>, <code>win_todos.md</code>. same structure in all of them:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span><span style="color:#75715e">## High Priority
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">- [ ]</span> Fix session ID bug in TIBConverseInteractor.swift:83
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Medium Priority
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">- [ ]</span> Add unit test target
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Low Priority
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">- [ ]</span> Refactor legacy auth flow
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Completed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">- [x]</span> 2026-04-20 — Migrated Localizable strings
</span></span></code></pre></div><p>there is also <code>daily.md</code> — today&rsquo;s focus, separate from the long-running backlogs. <code>daily_reset.sh</code> resets it each morning, carries over anything incomplete from yesterday, and pulls the top items from each project&rsquo;s High Priority section so you are never starting from blank. at end of day you move done items back to the project file and carry over the rest.</p>
<p>it is not rocket science but it works because everything is in one place and Claude can read all of it directly — no context switching, no &ldquo;go check Linear&rdquo;, no copy-pasting.</p>
<hr>
<h2 id="memos-kt-notes-and-changelogs">Memos, KT notes, and changelogs</h2>
<p>these three are the most underrated parts of the system. they get skipped when people think about &ldquo;what does an AI need to know&rdquo; but they are exactly what the AI is missing when it gives you advice that misses context.</p>
<p><strong>Memos (<code>memo/</code>)</strong> are for anything that does not fit in a doc or a todo. decision logs, architecture choices, research, migration plans, meeting outputs. the key thing about a memo is it captures the <em>why</em>. a doc says &ldquo;the auth middleware works like this&rdquo;. a memo says &ldquo;we rewrote the auth middleware because legal flagged the session token storage in April&rdquo;. without the memo Claude treats every piece of code as a deliberate, still-valid decision. with the memo it knows what is intentional and what is technical debt inherited from a compliance scramble.</p>
<p><strong>KT notes (<code>memo/kt/&lt;platform&gt;/YYYY-MM-DD_topic.md</code>)</strong> are specifically for inheriting a codebase. when someone does a knowledge transfer session, you write it up here. when you figure out something non-obvious about how the code works, you write it up here. these are the things that would take a new person weeks to discover by reading code — undocumented conventions, &ldquo;we don&rsquo;t touch that file because&rdquo;, quirks of the build system, context behind a weird architectural choice. writing them down once means Claude knows them forever.</p>
<p><strong>Changelogs (<code>changelog/&lt;project&gt;.md</code>)</strong> are append-only per-project ship logs:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>| 2026-04-15 | Migrated inbox to VIPER          | PR #441 |
</span></span><span style="display:flex;"><span>| 2026-04-08 | Upgraded to AGP 8.3              | PR #438 |
</span></span><span style="display:flex;"><span>| 2026-03-22 | Added push notification handling | PR #421 |
</span></span></code></pre></div><p>one row per notable change. the practical use: &ldquo;did we ship X on all platforms yet?&rdquo; — you check the changelog instead of grepping git history across five repos. also useful at standup when someone asks what shipped last week.</p>
<hr>
<h2 id="the-local-web-dashboard">The local web dashboard</h2>
<p><img alt="Quick links panel — one-click access to docs, scripts, and frequently used paths across all projects." loading="lazy" src="/img/command-center-images/cce-home-quicklinks.png"></p>
<p><img alt="Keyboard shortcuts reference — configured per-project so you never forget the exact command flags." loading="lazy" src="/img/command-center-images/cce-shortcuts.png"></p>
<p>the dashboard is a local Node.js server. no framework, no build step, runs offline, starts in two seconds.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>node .ops/dashboard/server.js
</span></span><span style="display:flex;"><span><span style="color:#75715e"># opens at localhost:3000</span>
</span></span></code></pre></div><p><strong>File browser</strong> — tree sidebar over all docs, todos, memos. files render as Markdown. todo files have live checkboxes — clicking one calls <code>POST /api/toggle</code> and writes the change directly to disk.</p>
<p><strong>Git status panel</strong> — polls all repos in parallel. branch, last commit, time ago, dirty file count.</p>
<p><strong>Full-text search</strong> — index built at startup from every <code>.md</code>, <code>.txt</code>, <code>.sh</code> file. AND-matched with scoring. returns results with line-number snippets. useful when you remember something exists but can not remember which doc it is in.</p>
<p><strong>Doc staleness indicators</strong> — green/yellow/red dots next to each doc in the sidebar based on <code>doc_sync.js</code> output. a resync button re-runs the script and refreshes the cache.</p>
<p><strong>Task creation</strong> — a form on every page to add a todo at any priority level to any project. finds the right heading and inserts the item, updates the <code>Last updated:</code> stamp.</p>
<hr>
<h2 id="shape-it-to-your-problems--the-cmd-tabs-and-rss-feeds">Shape it to your problems — the CMD tabs and RSS feeds</h2>
<p>here is the thing: everyone&rsquo;s pain is different. the folder structure and scripts above are the common base. but the reason this actually works day-to-day is that you can add whatever else you actually need on top.</p>
<p>for me, the two things i added that made the biggest difference:</p>
<p><img alt="Embedded terminal with per-project tabs — each one opens a PTY in that project&rsquo;s directory. Quick-command buttons above run the commands you&rsquo;d otherwise forget." loading="lazy" src="/img/command-center-images/cce-terminal.png"></p>
<p><strong>CMD view</strong> — <code>xterm.js</code> + <code>node-pty</code> over WebSocket. a real terminal embedded in the browser, one tab per project. each tab opens a PTY session in that project&rsquo;s directory. per-project quick-command buttons — you configure a label and a shell command, they appear as buttons above the terminal. so <code>Build Debug</code> runs <code>./gradlew assembleDebug</code> in the Android tab. <code>Sync Pods</code> runs <code>pod install</code> in the iOS tab. i click once, watch it run. no switching windows, no remembering the exact command flags.</p>
<p>before this i was constantly switching terminal windows and losing track of which one was which. now everything is in one browser tab.</p>
<p><img alt="RSS feeds grouped by platform — no algorithm, no app, just the dev blogs and release channels you actually want to follow." loading="lazy" src="/img/command-center-images/cce-rss.png"></p>
<p><strong>RSS feeds / newsletters</strong> — each platform has its own dev newsletter and release channel. iOS dev forum, Android releases, Kotlin blog. i added an RSS tab. feed URLs live in a <code>feeds.json</code> file, keyed by platform:</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;ios&#34;</span>: [
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;iOS Dev Weekly&#34;</span>, <span style="color:#f92672">&#34;url&#34;</span>: <span style="color:#e6db74">&#34;https://iosdevweekly.com/issues.rss&#34;</span> },
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;Swift Blog&#34;</span>, <span style="color:#f92672">&#34;url&#34;</span>: <span style="color:#e6db74">&#34;https://swift.org/atom.xml&#34;</span> }
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;android&#34;</span>: [
</span></span><span style="display:flex;"><span>    { <span style="color:#f92672">&#34;label&#34;</span>: <span style="color:#e6db74">&#34;Android Developers Blog&#34;</span>, <span style="color:#f92672">&#34;url&#34;</span>: <span style="color:#e6db74">&#34;https://feeds.feedburner.com/blogspot/hsDu&#34;</span> }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>the server fetches them server-side (handles redirects, CDATA stripping), renders as cards per platform. i stop by when i want to catch up. no separate app, no subscriptions, no algorithm deciding what i see — just the feeds i actually want, inside the same tab i already have open.</p>
<p>neither of these exist in anyone else&rsquo;s command center because they are solving my specific workflow pain. the point is the base layer gives you the foundation. the top layer is yours to build.</p>
<p>other ideas i have seen or thought about that i haven&rsquo;t built yet: a meeting notes tab that auto-stamps today&rsquo;s date, a platform-specific analytics panel, a PR review queue for when you work with a team.</p>
<hr>
<h2 id="the-two-tier-ai-model">The two-tier AI model</h2>
<p><img alt="Local AI chat powered by Mistral 7B via Ollama — answers questions about your codebase from the RAG index, free and fully offline." loading="lazy" src="/img/command-center-images/cce-local-ai-chat.png"></p>
<p>the local web dashboard has a built-in AI chat powered by Mistral 7B via Ollama — not Claude. this is the layer that routes cheap questions away from the cloud.</p>
<p>the RAG pipeline:</p>
<pre tabindex="0"><code>question
   │
   ▼
embed ──► LanceDB vector search ──► top-k doc chunks
                                          │
                                          ▼
                             Mistral 7B ◄── context + question
                                          │
                                          ▼
                                    streamed answer
                               (with source file citations)
</code></pre><p>at startup the server walks all docs, chunks them, embeds them and stores the index in LanceDB. conversation history (last 12 turns) is passed with each request. topics can be filtered per platform.</p>
<p>but here is the part that matters more — the local model is also wired directly into Claude as an MCP tool. not just the dashboard chat, but Claude itself can call it:</p>
<pre tabindex="0"><code>search_docs(query, project?)  — semantic search over the LanceDB index
ask_local(question, project?) — full RAG query to Mistral 7B
</code></pre><p>so when you ask Claude &ldquo;where is the WebSocket manager?&rdquo; Claude does not think about it, does not run three tool calls, does not burn tokens. it calls <code>ask_local</code>, gets the answer back from the local model as a tool response, and continues. the routing decision is encoded in <code>CLAUDE.md</code> as explicit rules so it happens automatically:</p>
<pre tabindex="0"><code>You
 │
 ├── Complex reasoning ──────────────────► Claude (API tokens)
 │    debugging, architecture,
 │    multi-file refactoring, codegen
 │
 └── Lookups + summaries ──► local MCP ──► Mistral 7B (free, local)
      &#34;where is X?&#34;, todos,
      file summaries, templates
</code></pre><p>questions that go local: &ldquo;where is ClassName defined&rdquo;, &ldquo;what files are in folder X&rdquo;, &ldquo;summarise this file&rdquo;, &ldquo;what todos are open for Android&rdquo;, &ldquo;what is the VIPER template for a new scene&rdquo;, anything answerable by reading the existing docs.</p>
<p>questions that use Claude: multi-file refactoring, debugging across call chains, cross-platform analysis with real reasoning, writing new features.</p>
<p>in practice around 60–70% of session queries route to the local model. those run free on my machine. Claude gets the work that actually needs it.</p>
<h3 id="tracking-what-you-saved">Tracking what you saved</h3>
<p>there is one more hook worth adding: a PostToolUse hook on the local MCP calls. every time Claude calls <code>ask_local</code> or <code>search_docs</code>, a small Python script logs the call to a <code>token_savings.jsonl</code> file with a timestamp, the tool used, and an estimated token count saved:</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-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2026-04-26T10:32:11&#34;</span>, <span style="color:#f92672">&#34;tool&#34;</span>: <span style="color:#e6db74">&#34;ask_local&#34;</span>,    <span style="color:#f92672">&#34;estTokensSaved&#34;</span>: <span style="color:#ae81ff">400</span>}
</span></span><span style="display:flex;"><span>{<span style="color:#f92672">&#34;ts&#34;</span>: <span style="color:#e6db74">&#34;2026-04-26T10:33:45&#34;</span>, <span style="color:#f92672">&#34;tool&#34;</span>: <span style="color:#e6db74">&#34;search_docs&#34;</span>,  <span style="color:#f92672">&#34;estTokensSaved&#34;</span>: <span style="color:#ae81ff">250</span>}
</span></span></code></pre></div><p>the same hook also outputs a reminder back into Claude&rsquo;s context: <em>&ldquo;you just used the local model — append <code>[tib-mcp-info]</code> to the sentence in your response that came from this result.&rdquo;</em></p>
<p>that <code>[tib-mcp-info]</code> tag in the response is how you know which parts Claude answered from local knowledge vs the local model. it is easy to skip but worth keeping — after a few weeks you can look at the JSONL and get a rough sense of how many tokens the routing saved. it also keeps you honest about whether the local model is actually being used or whether Claude is quietly doing everything itself.</p>
<hr>
<h2 id="the-session-cache--picking-up-exactly-where-you-left-off">The session cache — picking up exactly where you left off</h2>
<p>the session briefing knows &ldquo;what was worked on last session&rdquo; because <code>/callitaday</code> writes a <code>session_summary.json</code> at the end of each day. the structure:</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;date&#34;</span>: <span style="color:#e6db74">&#34;2026-04-22&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;platform&#34;</span>: <span style="color:#e6db74">&#34;ios&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;done&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Settings flatten complete — SettingsSUI/NewUI/Settings_base merged into Features/Settings&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Helpers restructured — Contacts moved to Features/Contacts&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;carriedOver&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Android: edge to edge mandate for Play Store — not checked yet&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;decisions&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;Contacts gets Features/Contacts/ not buried in Helpers&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;URLSchemeAnalyser lives in Core/DeepLinker/&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;openQuestions&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;CustomContextMenu still needs to move to Features/Inbox/Actions/&#34;</span>
</span></span><span style="display:flex;"><span>  ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;keyFiles&#34;</span>: [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;native/TeamInbox.xcodeproj/project.pbxproj&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;native/TeamInbox/Features/Settings/&#34;</span>
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>next morning, <code>session_context.sh</code> reads this file and includes the <code>done</code>, <code>carriedOver</code>, <code>decisions</code>, and <code>openQuestions</code> fields in the briefing. Claude starts the session knowing exactly where things were left — no re-reading git log, no &ldquo;what were we working on?&rdquo;. the <code>keyFiles</code> field is useful if you want Claude to immediately orient to the relevant parts of the code.</p>
<p><code>/callitaday</code> is the slash command that writes this. it wraps the session: moves done items from <code>daily.md</code> back to the project todo files, carries over incomplete items, writes the JSON. it is the last thing you run before closing the terminal.</p>
<hr>
<h2 id="setting-this-up-yourself">Setting this up yourself</h2>
<p>the minimum version of this is five files and an afternoon:</p>
<ol>
<li>create a parent workspace folder above all your repos</li>
<li>add a <code>.ops/</code> folder with <code>docs/</code>, <code>todo/</code>, <code>memo/</code>, <code>scripts/</code></li>
<li>write a <code>CLAUDE.md</code> at the workspace root — folder layout, git command prefixes, working conventions, routing rules</li>
<li>write <code>session_context.sh</code> or a simple briefing script that runs <code>git -C &lt;project&gt;/ status</code> across all repos and prints a summary — hook it to session start</li>
<li>write one doc per project covering the folder structure and architecture, add the <code>watches</code> frontmatter</li>
</ol>
<p>that is the base. that alone kills the &ldquo;re-explain everything every session&rdquo; problem and the &ldquo;docs live in Notion somewhere&rdquo; problem.</p>
<p>layer on top in order of payoff:</p>
<ul>
<li><code>daily_reset.sh</code> and the <code>daily.md</code> workflow — probably adds the most to day-to-day sanity</li>
<li><code>doc_sync.js</code> — if you are writing docs and want them to stay honest</li>
<li>the dashboard — once you want a visual layer over all of it</li>
<li>the CMD tabs — if you are constantly switching terminal windows for the same commands</li>
<li>the local model + MCP — when token pressure is real and your doc library is large enough to justify a RAG pipeline</li>
</ul>
<p>do not build all of it at once. start with the folder structure and the <code>CLAUDE.md</code>. add the rest as you actually feel the pain they solve.</p>
<hr>
<h2 id="what-changed">What changed</h2>
<p>before: open the right repo in Xcode, find the right Notion page, check Slack for where i left off, re-explain the codebase to Claude, watch the first 30% of my context window fill with boilerplate before writing a single line of actual code.</p>
<p>after: one terminal, one browser tab, one Claude session. the Command Center has the state of every repo, every doc, every task, every recent decision — and the cheap questions never reach the cloud.</p>
<p>the whole system is about 1,500 lines of Node.js, a handful of shell scripts, and Markdown files. no heavy dependencies, no cloud services, runs entirely offline.</p>
<p>if you are managing more than two active repos and you are constantly re-orienting your AI every session — this pattern is worth trying. you do not have to build the whole thing. start with the folder structure and a <code>CLAUDE.md</code>. that alone will change how your sessions feel.</p>
<p>the rest you will figure out as you go, shaped around whatever is actually slowing you down. that is the point.</p>
<hr>
<p><em>a note on this post — the ideas, the frustration, the architecture, the decisions are all mine. i used Claude to help structure and articulate things i already knew but was too lazy to write out properly. felt right to mention it given the whole post is about working with AI. use your tools.</em></p>
<p><em>after writing this i came across andrej karpathy&rsquo;s wiki pattern — same instinct around plain files, single source of truth, readable by humans and machines. worth looking up if this resonated.</em></p>
]]></content:encoded></item><item><title>To or not to use AI Tools - A Self Reflection</title><link>https://md.eknath.dev/posts/ai-ml/to-or-not-to-use-ai-tools/</link><pubDate>Sat, 07 Mar 2026 21:47:13 +0530</pubDate><guid>https://md.eknath.dev/posts/ai-ml/to-or-not-to-use-ai-tools/</guid><description>&lt;p>This is more of self reflection than some sort of an ai generate slop that i wish to shove it on to your face, Less than 5% of my friend circle has enough patient&amp;rsquo;s to actually read an any article let alone debate or discuss on the topics, im trying to get into that learned circle because they seems to have more clarity on topics, thankfully at work i was blessed with a friend who shared and discuess on the things he shares, i mostly defend the values/lessons/reflections shared by the articles (Thanks aditya!) One such article is the reason for this reflective thougts.&lt;/p></description><content:encoded><![CDATA[<p>This is more of self reflection than some sort of an ai generate slop that i wish to shove it on to your face, Less than 5% of my friend circle has enough patient&rsquo;s to actually read an any article let alone debate or discuss on the topics, im trying to get into that learned circle because they seems to have more clarity on topics, thankfully at work i was blessed with a friend who shared and discuess on the things he shares, i mostly defend the values/lessons/reflections shared by the articles (Thanks aditya!) One such article is the reason for this reflective thougts.</p>
<p>before we go further i humbly request you to read this article, don&rsquo;t stop reading it after the first few paragraph(Again, Thanks Aditya), keep reading till the end there are twists and turns so please try to read it and get back here on this reflection journey.</p>
<p>Recently, a colleague shared an article with me titled <a href="https://www.scottsmitelli.com/articles/you-dont-have-to/">&ldquo;You Don&rsquo;t Have To&rdquo;</a> by Scott Smitelli. I highly recommend reading it. It really makes you think about how we might slowly be losing our edge due to the heavy use of Large Language Models (LLMs), like how i stoped memorizing phone numbers coz its available on my phone book except mine ;0 and find it hard to calculate in head with large numbers continuesly like the guy in typical village shop guy does quickly.</p>
<p>With ai there are so many techniques i still experiment with and i continiously work on. It&rsquo;s great to use AI to brainstorm, validate, or debate your thoughts, but don&rsquo;t let it run the show for you. If you do, you&rsquo;ll slowly vanish into the background.</p>
<p>Reading this article—and later debating it with my manager—sent me down a rabbit hole of self-reflection. It sparked a deep, sometimes controversial, conversation between us. Is AI destroying the art of coding? Or is it simply the next step in our evolution? After stepping back to clear my thoughts, here are my reflections on where I stand with AI tools today.</p>
<h2 id="the-zero-to-one-myth">The &ldquo;Zero to One&rdquo; Myth</h2>
<p>There&rsquo;s a romanticized idea in our industry that we are still building things &ldquo;from zero.&rdquo; The truth is, that hasn&rsquo;t been true for years. We went from machine code, to assembly, to C, to Java, and Kotlin. Today, 95% of developers rely heavily on layers of abstractions like frameworks, APIs, and libraries that they didn&rsquo;t build and often don&rsquo;t fully understand under the hood.</p>
<p>AI isn&rsquo;t some alien invasion; it is merely the newest layer of abstraction in this ongoing shift.</p>
<p>Also, AI isn&rsquo;t a &ldquo;magic button.&rdquo; Unless you are simply cloning a well-documented repository, AI cannot build a full-fledged, custom solution from start to finish with a single prompt. Real engineering still needs architecture, logic, and deep orchestration—all of which must come from a human.</p>
<h2 id="value-over-the-craft">Value over &ldquo;The Craft&rdquo;</h2>
<p>Many developers treat coding as an art form. They pride themselves on writing the perfect, zero-dependency code. But let&rsquo;s be practical: in a professional setting, we aren&rsquo;t here just to write poetry in code. We are here to solve problems, deliver value to our clients, and ultimately earn a living.</p>
<p>All I care about are the humans using the tools I build. The exact ingredients or the &ldquo;purity&rdquo; of the code doesn&rsquo;t matter nearly as much as the end-user&rsquo;s experience. Getting too caught up in digging through abstractions or coding like a purist feels unnecessary if it stands in the way of delivering a working, valuable product.</p>
<h2 id="the-danger-of-ai-slop">The Danger of &ldquo;AI Slop&rdquo;</h2>
<p>That being said, my manager raised a very valid point: the sheer ease of generating content with AI easily leads to garbage. Generative AI can produce 10x more content in a single day than humans have produced in a lifetime. When people generate and publish content without any personal input, refinement, or real intent, we get what he accurately called &ldquo;AI slop.&rdquo;</p>
<p>I agree completely. Outputting anything publicly without real personal meaning—just creating for the sake of creating without adding value—is wrong. It pollutes the internet and human conversations. The tool is only as good as the intent behind it.</p>
<h2 id="the-axe-and-the-chainsaw">The Axe and the Chainsaw</h2>
<p>Think of AI as an electric chainsaw, and traditional coding as an axe (an example from the article shared above).</p>
<p>Knowing how to use an axe remains extremely important even when a chainsaw is available. The chainsaw can break or fail. More importantly, <strong>a lumberjack who has cut down trees with an axe a thousand times knows exactly how to angle the cut so the tree falls right where he wants it to</strong>.</p>
<p>Using AI tools doesn&rsquo;t turn your brain to mush, <strong>provided</strong> you actually understand the changes it makes to your creation. Let me be real here THIS IS EXTREMLU HARD! That&rsquo;s actually why I&rsquo;m thankful for Claude&rsquo;s token limitations sometimes. It forces me to stay involved. AI shifts your role from pure execution to orchestration. You cannot outsource your JUDGEMENT, EXPERIENCE, or PERSONALITY. If you rely entirely on AI without applying these human traits, the quality of your work will drop, and you will eventually become redundant.</p>
<h2 id="an-equalizer-for-communication">An Equalizer for Communication</h2>
<p>Beyond just writing code, AI is an incredible equalizer for communication. Not everyone is a natural wordsmith. For those of us who are less vocal or struggle to find the right phrasing, AI is a tool that helps us communicate our technical intent clearly. It&rsquo;s not about faking a voice; it&rsquo;s about polishing it. It helps take the raw intent inside my head and words it rightly for the world to understand, let me say its not easy to get it the way we have in mind there are limitations like token limit, it tries to end it very quickly by saying the crux of things or to draggy but never, or almost never the right proprotion.</p>
<h2 id="wielding-the-future">Wielding the Future</h2>
<p>My philosophy in this rapidly changing landscape is simple:</p>
<ul>
<li>When we only had machine code, we coded in assembly.</li>
<li>When we got high-level languages like Java and Kotlin, we used them.</li>
<li>Now we have AI, and we should use it extensively.</li>
<li>If AI becomes inaccessible or too expensive tomorrow, we simply go back to coding in Kotlin. We should be prepared for that.</li>
</ul>
<p>To make sure I&rsquo;m prepared, I&rsquo;ve actually started &ldquo;AI fasting.&rdquo; As silly as it might sound, taking a break from AI helps me make sure I can still build or think differently without these tools. I&rsquo;m not sure how every employer feels about this, but for me, spending three days coding without AI is a healthy practice. If feature demands increase, I might have to tweak this schedule, but I try not to be too rigid about it, so far looks like we are going all in this new era.</p>
<p>Recently, I was scrolling through Instagram and saw a reel where a developer made a great point. He said: &ldquo;Before AI, you delivered a project in a week. Now, even with AI, it still takes a week—why is that?&rdquo; His answer was that while AI generates code quickly, fixing the bugs or tweeking the code it creates and also reviewing it has become extremely harder.</p>
<p>That really stood out to me. I still have some lingering doubts about all of this, but what matters most is finding the right balance,keeping it away is not going to help me, im going to embrance it tight find its strenth and weekness and use it to my advantage.</p>
<p>I don&rsquo;t wish to nitpick on philosophies people have, that have no real meaning to the end user. Coding like a &ldquo;caveman&rdquo; shouldn&rsquo;t be a badge of honor. Utilizing tools like MCP, plugins, and advanced prompting is how we stay forward-thinking and effective in this day and age, especially in the phase we are moving in right now.</p>
<p>Ultimately, machines should take the drudgery out of life. AI brings that promise to our doorsteps. It&rsquo;s up to us to embrace it, orchestrate it responsibly, and ensure that we are using it to add real value, rather than just contributing to the noise of confusions around.</p>
<p>I wrote this just to clear some thoughts in my head regarding the excesive ai tool usage, i myself fell into this trap of vibe working for a short period of time, i regret it coz i lost the chance to learn what happend in that part of the code, i thought skipping that was okey but that above article opens by eyes. This is also a phase of learning but don&rsquo;t ever do that on work/personal projects that you truly care, thats a big no no, Blinde Vibecoding is okey for prototying some personal productivity tools, features and such silly thing never on any real product that you truely care.</p>
]]></content:encoded></item><item><title>Visualizing Ideas with Claude: Setting Up the Official Draw.io MCP Server</title><link>https://md.eknath.dev/posts/ai-ml/drawio-mcp-server-setup/</link><pubDate>Tue, 17 Feb 2026 12:00:00 +0530</pubDate><guid>https://md.eknath.dev/posts/ai-ml/drawio-mcp-server-setup/</guid><description>&lt;h2 id="tldr---quick-setup">TL;DR - Quick Setup&lt;/h2>
&lt;p>Want to generate diagrams directly in Claude? Here is the fast track configuration for your &lt;code>claude_desktop_config.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;mcpServers&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;drawio&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;command&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;npx&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;args&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;-y&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;@drawio/mcp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Restart Claude Desktop, and then ask: &lt;em>&amp;ldquo;Create a flowchart for a user login system.&amp;rdquo;&lt;/em>&lt;/p>
&lt;hr>
&lt;h2 id="why-drawio-with-claude">Why Draw.io with Claude?&lt;/h2>
&lt;p>If you are like me, explaining architecture or complex flows in text can get wordy and confusing. &amp;ldquo;Component A talks to B, which then signals C&amp;hellip;&amp;rdquo; is much harder to parse than a simple arrow connecting boxes.&lt;/p></description><content:encoded><![CDATA[<h2 id="tldr---quick-setup">TL;DR - Quick Setup</h2>
<p>Want to generate diagrams directly in Claude? Here is the fast track configuration for your <code>claude_desktop_config.json</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;mcpServers&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;drawio&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;npx&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;args&#34;</span>: [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;-y&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;@drawio/mcp&#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></code></pre></div><p>Restart Claude Desktop, and then ask: <em>&ldquo;Create a flowchart for a user login system.&rdquo;</em></p>
<hr>
<h2 id="why-drawio-with-claude">Why Draw.io with Claude?</h2>
<p>If you are like me, explaining architecture or complex flows in text can get wordy and confusing. &ldquo;Component A talks to B, which then signals C&hellip;&rdquo; is much harder to parse than a simple arrow connecting boxes.</p>
<p>The <strong>Official Draw.io MCP Server</strong> (<code>@drawio/mcp</code>) bridges this gap. It allows Claude to:</p>
<ol>
<li><strong>Generate Diagrams</strong>: Create flowcharts, sequence diagrams, and system architectures from scratch.</li>
<li><strong>Edit Existing Diagrams</strong>: Update diagrams based on new requirements.</li>
<li><strong>Render Visuals</strong>: See the diagram directly in the chat interface (depending on the client support).</li>
</ol>
<p>This is a game-changer for documentation, brainstorming, and technical specs.</p>
<hr>
<h2 id="prerequisites">Prerequisites</h2>
<p>Before we start, ensure you have the following:</p>
<ul>
<li><strong>Claude Desktop App</strong>: Installed on your Mac or Linux machine.</li>
<li><strong>Node.js</strong>: Version 18 or higher.
<ul>
<li>Check your version: <code>node -v</code></li>
<li>If missing, I recommend using <code>nvm</code> (Node Version Manager) to install it.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="step-by-step-setup-guide">Step-by-Step Setup Guide</h2>
<h3 id="1-locate-configuration-file">1. Locate Configuration File</h3>
<p>You need to edit the highly specific <code>claude_desktop_config.json</code> file.</p>
<p><strong>On macOS:</strong>
Open your terminal and run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>code ~/Library/Application<span style="color:#ae81ff">\ </span>Support/Claude/claude_desktop_config.json
</span></span></code></pre></div><p><em>(Or use <code>nano</code>, <code>vim</code>, or <code>open -e</code> if you don&rsquo;t use VS Code)</em></p>
<p><strong>On Linux:</strong>
The file is typically located at:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>~/.config/Claude/claude_desktop_config.json
</span></span></code></pre></div><h3 id="2-add-the-drawio-server">2. Add the Draw.io Server</h3>
<p>Add the following entry to the <code>mcpServers</code> object in your config file. If the file is empty, wrap it in curly braces.</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;mcpServers&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;drawio&#34;</span>: {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;npx&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;args&#34;</span>: [
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;-y&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;@drawio/mcp&#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></code></pre></div><p><strong>What is this doing?</strong></p>
<ul>
<li>It tells Claude to run the <code>npx</code> command.</li>
<li>The <code>-y</code> flag automatically creates the environment without prompting.</li>
<li><code>@drawio/mcp</code> is the official package containing the server logic.</li>
</ul>
<h3 id="3-restart-claude">3. Restart Claude</h3>
<p>For the changes to take effect:</p>
<ol>
<li>Close the Claude Desktop interface completely.</li>
<li>Re-open it.</li>
</ol>
<p>You should see a generic &ldquo;MCP&rdquo; icon or indicator (depending on your version) showing that tools are loaded.</p>
<hr>
<h2 id="alternative-setup-with-claude-code-cli">Alternative: Setup with Claude Code CLI</h2>
<p>If you prefer using the <strong>Claude Code CLI</strong> (command line interface) instead of the Desktop app, you can add the server directly using the <code>claude mcp add</code> command.</p>
<p>This is particularly useful if you want to scope the tool to a specific project or your user account without editing JSON files manually.</p>
<h3 id="one-line-setup">One-Line Setup</h3>
<p>Run this command in your terminal:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>claude mcp add drawio --scope user -- npx -y @drawio/mcp
</span></span></code></pre></div><p><strong>Breakdown of the command:</strong></p>
<ul>
<li><code>claude mcp add drawio</code>: Tells Claude to add a new MCP server named &ldquo;drawio&rdquo;.</li>
<li><code>--scope user</code>: Installs it globally for your user account (use <code>--scope project</code> to install for the current folder only).</li>
<li><code>--</code>: Separator indicating the start of the actual server command.</li>
<li><code>npx -y @drawio/mcp</code>: The command to run the Draw.io server.</li>
</ul>
<p>Once added, you can verify it with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>claude mcp list
</span></span></code></pre></div><hr>
<h2 id="how-to-use-it">How to Use It</h2>
<p>Once connected, you can converse with Claude naturally about diagrams.</p>
<h3 id="creating-a-new-diagram">Creating a New Diagram</h3>
<p><strong>Prompt:</strong></p>
<blockquote>
<p>&ldquo;Create a sequence diagram for an OAuth 2.0 authentication flow involving a User, Client App, Authorization Server, and Resource Server.&rdquo;</p>
</blockquote>
<p>Claude will generate the XML or specific format required for Draw.io and often provide a link or a rendered view.</p>
<h3 id="editing-a-diagram">Editing a Diagram</h3>
<p>If you have a diagram file (e.g., XML) in your project context, you can ask Claude to modify it.</p>
<p><strong>Prompt:</strong></p>
<blockquote>
<p>&ldquo;Update the attached architecture diagram to include a Redis cache layer between the API and the Database.&rdquo;</p>
</blockquote>
<h3 id="complex-visualizations">Complex Visualizations</h3>
<p>You aren&rsquo;t limited to simple boxes. You can ask for:</p>
<ul>
<li><strong>Mind Maps</strong>: &ldquo;Create a mind map for a marketing strategy.&rdquo;</li>
<li><strong>ER Diagrams</strong>: &ldquo;Generate an Entity-Relationship diagram for an e-commerce database schema.&rdquo;</li>
<li><strong>Network Topologies</strong>: &ldquo;Draw a high-availability AWS network setup with public and private subnets.&rdquo;</li>
</ul>
<hr>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="command-not-found-npx">&ldquo;Command not found: npx&rdquo;</h3>
<p>If Claude complains it can&rsquo;t find <code>npx</code>, you might need to provide the absolute path.</p>
<ol>
<li>Run <code>which npx</code> in your terminal. (e.g., <code>/usr/local/bin/npx</code>)</li>
<li>Update your config:
<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-json" data-lang="json"><span style="display:flex;"><span><span style="color:#e6db74">&#34;drawio&#34;</span><span style="color:#960050;background-color:#1e0010">:</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;command&#34;</span>: <span style="color:#e6db74">&#34;/usr/local/bin/npx&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;args&#34;</span>: [<span style="color:#e6db74">&#34;-y&#34;</span>, <span style="color:#e6db74">&#34;@drawio/mcp&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div></li>
</ol>
<h3 id="server-error--disconnection">Server Error / Disconnection</h3>
<p>If the server crashes, check your Node.js version. The Draw.io MCP server requires a modern Node environment. Ensure <code>node -v</code> returns <code>v18.x.x</code> or newer.</p>
<hr>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/jgraph/drawio-mcp">Official Draw.io MCP GitHub Repository</a></li>
<li><a href="https://modelcontextprotocol.io">Model Context Protocol Documentation</a></li>
<li><a href="https://www.draw.io">Draw.io Website</a></li>
</ul>
]]></content:encoded></item><item><title>Building a Reusable Speech-to-Text Component in Jetpack Compose</title><link>https://md.eknath.dev/posts/jetpack-compose-speech-to-text-implementation/</link><pubDate>Thu, 12 Feb 2026 21:30:00 +0530</pubDate><guid>https://md.eknath.dev/posts/jetpack-compose-speech-to-text-implementation/</guid><description>&lt;h2 id="tldr---why-you-should-add-voice-input">TL;DR - Why You Should Add Voice Input&lt;/h2>
&lt;p>&lt;strong>Voice input can dramatically improve UX, yet most apps don&amp;rsquo;t use it.&lt;/strong> Here&amp;rsquo;s why you should:&lt;/p>
&lt;p>✅ &lt;strong>Zero app size increase&lt;/strong> - Uses Android&amp;rsquo;s native speech recognition (no libraries!)
✅ &lt;strong>No permissions required&lt;/strong> - Works out of the box
✅ &lt;strong>3-5x faster input&lt;/strong> - Users can speak 150+ words/min vs typing 40 words/min
✅ &lt;strong>Better accessibility&lt;/strong> - Essential for users with motor impairments
✅ &lt;strong>Reduces friction&lt;/strong> - One tap vs multiple keyboard interactions
✅ &lt;strong>Professional polish&lt;/strong> - Shows attention to UX details&lt;/p></description><content:encoded><![CDATA[<h2 id="tldr---why-you-should-add-voice-input">TL;DR - Why You Should Add Voice Input</h2>
<p><strong>Voice input can dramatically improve UX, yet most apps don&rsquo;t use it.</strong> Here&rsquo;s why you should:</p>
<p>✅ <strong>Zero app size increase</strong> - Uses Android&rsquo;s native speech recognition (no libraries!)
✅ <strong>No permissions required</strong> - Works out of the box
✅ <strong>3-5x faster input</strong> - Users can speak 150+ words/min vs typing 40 words/min
✅ <strong>Better accessibility</strong> - Essential for users with motor impairments
✅ <strong>Reduces friction</strong> - One tap vs multiple keyboard interactions
✅ <strong>Professional polish</strong> - Shows attention to UX details</p>
<p><strong>The catch?</strong> It requires network connectivity and device support. But with proper availability checks, you can gracefully hide the feature when unavailable—making it a <strong>pure win</strong> when present.</p>
<hr>
<h2 id="why-most-apps-skip-this-feature">Why Most Apps Skip This Feature</h2>
<p>Despite being a <strong>native Android capability since API 8</strong>, many developers overlook voice input because:</p>
<ol>
<li><strong>Assumed complexity</strong> - Developers think it requires heavy ML libraries</li>
<li><strong>Unclear implementation</strong> - Documentation is scattered</li>
<li><strong>Network dependency concerns</strong> - Fear of handling edge cases</li>
<li><strong>Device fragmentation worries</strong> - Uncertainty about availability</li>
<li><strong>&ldquo;The keyboard already has it&rdquo;</strong> - The most common misconception</li>
</ol>
<p><strong>The truth?</strong> It&rsquo;s simpler than adding a date picker, and this guide shows you how to handle all edge cases properly.</p>
<hr>
<h2 id="but-users-have-voice-input-on-their-keyboard-already">&ldquo;But Users Have Voice Input on Their Keyboard Already!&rdquo;</h2>
<p>This is the <strong>most common objection</strong> developers raise. Yes, most mobile keyboards (Gboard, SwiftKey, Samsung Keyboard) have a mic button. <strong>But here&rsquo;s why in-app voice input is still essential:</strong></p>
<h3 id="the-reality-of-keyboard-voice-input-usage"><strong>The Reality of Keyboard Voice Input Usage</strong></h3>
<p>📊 <strong>Usage statistics show a problem:</strong></p>
<ul>
<li>Most users <strong>don&rsquo;t even know</strong> the keyboard mic button exists</li>
<li>Many <strong>forget about it</strong> after the initial setup</li>
<li>Some <strong>disable it accidentally</strong> during keyboard customization</li>
<li>The keyboard mic is <strong>visually small</strong> and easy to miss</li>
<li>Users must <strong>actively look for it</strong> among other keyboard buttons</li>
</ul>
<h3 id="why-in-app-voice-input-is-superior"><strong>Why In-App Voice Input is Superior</strong></h3>
<h4 id="1-discoverability"><strong>1. Discoverability</strong></h4>
<pre tabindex="0"><code>❌ Keyboard mic: Hidden among 30+ keyboard keys, looks like any other button
✅ In-app mic: Prominent, contextual, right next to the input field
</code></pre><p><strong>Example:</strong> A text field with a mic icon in the trailing position is <strong>immediately obvious</strong>. The keyboard mic? Users have to open the keyboard, scan for it, and remember it exists.</p>
<h4 id="2-context-aware-ux"><strong>2. Context-Aware UX</strong></h4>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// In-app voice can be contextual
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>TextField(
</span></span><span style="display:flex;"><span>    label = { Text(<span style="color:#e6db74">&#34;Product Review&#34;</span>) },
</span></span><span style="display:flex;"><span>    trailingIcon = { MicIcon() }  <span style="color:#75715e">// Clear purpose: &#34;Speak your review&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>)
</span></span></code></pre></div><p>The keyboard mic has <strong>no context</strong> - it&rsquo;s the same button whether you&rsquo;re entering an email, a password, or a product review. In-app voice input can show <strong>field-specific prompts</strong> like &ldquo;Describe your issue&rdquo; or &ldquo;Speak your address&rdquo;.</p>
<h4 id="3-user-intent-and-flow"><strong>3. User Intent and Flow</strong></h4>
<ul>
<li>
<p><strong>Keyboard mic</strong>: Requires users to:</p>
<ol>
<li>Tap the input field</li>
<li>Wait for keyboard to appear</li>
<li>Look for the mic button among keyboard keys</li>
<li>Tap the mic</li>
<li>Speak</li>
</ol>
</li>
<li>
<p><strong>In-app mic</strong>: Simplified flow:</p>
<ol>
<li>Tap the mic icon (no keyboard needed!)</li>
<li>Speak</li>
</ol>
</li>
</ul>
<p><strong>Result:</strong> <strong>2 fewer steps</strong> and <strong>no keyboard lag</strong>.</p>
<h4 id="4-visual-prominence"><strong>4. Visual Prominence</strong></h4>
<table>
  <thead>
      <tr>
          <th>Keyboard Mic</th>
          <th>In-App Mic</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>5-6mm size typical</td>
          <td>Can be 24-32dp (12-16mm)</td>
      </tr>
      <tr>
          <td>Gray/neutral color</td>
          <td>App-themed, stands out</td>
      </tr>
      <tr>
          <td>Among 30+ keys</td>
          <td>Isolated, clear purpose</td>
      </tr>
      <tr>
          <td>Same across all apps</td>
          <td>Consistent with your app design</td>
      </tr>
  </tbody>
</table>
<h4 id="5-accessibility-considerations"><strong>5. Accessibility Considerations</strong></h4>
<p>Users with <strong>motor impairments</strong> or <strong>visual limitations</strong> benefit significantly:</p>
<ul>
<li>Larger, easier-to-tap target</li>
<li>Better contrast and visibility</li>
<li>Screen readers can announce it contextually</li>
<li>Doesn&rsquo;t require precise keyboard navigation</li>
</ul>
<h4 id="6-user-psychology"><strong>6. User Psychology</strong></h4>
<p><strong>Explicit invitation &gt; Hidden capability</strong></p>
<p>When users see a mic icon next to a text field, it:</p>
<ul>
<li><strong>Signals</strong> that voice input is encouraged</li>
<li><strong>Reduces friction</strong> - they don&rsquo;t need to hunt for it</li>
<li><strong>Increases adoption</strong> - visible features get used more</li>
<li><strong>Feels intentional</strong> - the app <em>wants</em> them to use voice</li>
</ul>
<p>The keyboard mic feels like a <strong>generic fallback</strong>. The in-app mic feels like a <strong>first-class feature</strong>.</p>
<h3 id="real-world-data-points"><strong>Real-World Data Points</strong></h3>
<p>While specific metrics vary by app, general patterns show:</p>
<ul>
<li>📈 <strong>5-10x higher voice input usage</strong> with prominent in-app mic icons</li>
<li>🎯 <strong>New user discovery</strong> - many users don&rsquo;t realize keyboard voice exists</li>
<li>♿ <strong>Accessibility gains</strong> - significant usage increase among users with disabilities</li>
<li>📱 <strong>Mobile-first users</strong> especially benefit (small screen, fat fingers)</li>
</ul>
<h3 id="the-hybrid-approach-best-of-both-worlds"><strong>The Hybrid Approach: Best of Both Worlds</strong></h3>
<p>The ideal solution is <strong>not either/or</strong>, but <strong>both</strong>:</p>
<p>✅ <strong>In-app mic</strong> for discoverability and context
✅ <strong>Keyboard mic</strong> still works as a fallback</p>
<p>Users get:</p>
<ul>
<li>A prominent, obvious voice input option</li>
<li>Fallback if they prefer keyboard mic</li>
<li>Contextual prompts and better UX</li>
<li>No downsides!</li>
</ul>
<h3 id="when-just-use-the-keyboard-fails"><strong>When &ldquo;Just Use the Keyboard&rdquo; Fails</strong></h3>
<p>Some scenarios where keyboard voice input is insufficient:</p>
<ol>
<li><strong>Custom keyboards</strong> - Not all keyboards have voice input</li>
<li><strong>Enterprise devices</strong> - Some organizations disable keyboard voice for security</li>
<li><strong>Locked-down keyboards</strong> - Educational or restricted environments</li>
<li><strong>Non-Google keyboards</strong> - Third-party keyboards may lack voice features</li>
<li><strong>Disabled by user</strong> - Some users disable keyboard permissions</li>
</ol>
<p>Your <strong>in-app implementation</strong> works regardless of keyboard choice.</p>
<hr>
<h2 id="the-bottom-line">The Bottom Line</h2>
<p><strong>&ldquo;Users have voice on their keyboard&rdquo;</strong> is like saying:</p>
<ul>
<li>&ldquo;Don&rsquo;t add a search icon, users can use Ctrl+F&rdquo;</li>
<li>&ldquo;Don&rsquo;t add a share button, users can copy-paste&rdquo;</li>
<li>&ldquo;Don&rsquo;t add undo, users can manually fix mistakes&rdquo;</li>
</ul>
<p><strong>Just because a capability exists somewhere doesn&rsquo;t mean it&rsquo;s discoverable or convenient.</strong></p>
<p>In-app voice input is about <strong>removing friction</strong> and <strong>guiding users</strong> toward better UX. The fact that keyboard voice exists is great - your in-app implementation makes it <strong>more likely to actually be used</strong>.</p>
<hr>
<p>Ever wanted to add voice input to your Android app with minimal effort? <strong>Speech-to-Text</strong> functionality can dramatically improve user experience, especially for note-taking, messaging, or search features.</p>
<p>In this guide, we&rsquo;ll build a <strong>clean, reusable Speech-to-Text component</strong> using <strong>Jetpack Compose</strong> that wraps Android&rsquo;s native speech recognition API.</p>
<h2 id="-what-were-building">🎯 What We&rsquo;re Building</h2>
<p>A composable speech recognition system with:</p>
<ul>
<li>✅ <strong>Simple API</strong> - One composable function to handle everything</li>
<li>✅ <strong>Lifecycle-aware</strong> - Properly managed with Activity Result API</li>
<li>✅ <strong>Locale support</strong> - Respects app language settings</li>
<li>✅ <strong>Availability checking</strong> - Gracefully handles devices without speech recognition</li>
<li>✅ <strong>Reusable state</strong> - Clean separation of concerns</li>
</ul>
<hr>
<h2 id="-architecture-overview">🏗️ Architecture Overview</h2>
<p>Our implementation consists of three main components:</p>
<ol>
<li><strong><code>SystemSpeechToTextHelper</code></strong> - A utility object that handles Android&rsquo;s RecognizerIntent</li>
<li><strong><code>SpeechToTextState</code></strong> - A state holder that manages the speech recognition launcher</li>
<li><strong><code>rememberSpeechToText()</code></strong> - A composable function that creates and remembers the state</li>
<li><strong><code>SpeechToTextButton</code></strong> (Bonus) - A ready-to-use UI component</li>
</ol>
<hr>
<h2 id="-implementation">📝 Implementation</h2>
<h3 id="1-the-helper-object"><strong>1️⃣ The Helper Object</strong></h3>
<p>First, let&rsquo;s create a helper object to encapsulate all Android-specific speech recognition logic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">object</span> <span style="color:#a6e22e">SystemSpeechToTextHelper</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAppLocale</span>(): Locale {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Locale</span>.forLanguageTag(<span style="color:#a6e22e">Language</span>.currentLocale.<span style="color:#66d9ef">value</span>.code)
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Locale</span>.getDefault()
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">createRecognitionIntent</span>(
</span></span><span style="display:flex;"><span>        languageModel: String = <span style="color:#a6e22e">RecognizerIntent</span>.LANGUAGE_MODEL_FREE_FORM,
</span></span><span style="display:flex;"><span>        locale: Locale = getAppLocale(),
</span></span><span style="display:flex;"><span>        prompt: String? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>        maxResults: Int = <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    ): Intent {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Intent(<span style="color:#a6e22e">RecognizerIntent</span>.ACTION_RECOGNIZE_SPEECH).apply {
</span></span><span style="display:flex;"><span>            putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_LANGUAGE_MODEL, languageModel)
</span></span><span style="display:flex;"><span>            putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_LANGUAGE, locale.toLanguageTag())
</span></span><span style="display:flex;"><span>            putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_MAX_RESULTS, maxResults)
</span></span><span style="display:flex;"><span>            prompt<span style="color:#f92672">?.</span>let { putExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_PROMPT, <span style="color:#66d9ef">it</span>) }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">extractSpokenText</span>(result: ActivityResult): String? {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">if</span> (result.resultCode <span style="color:#f92672">==</span> <span style="color:#a6e22e">Activity</span>.RESULT_OK) {
</span></span><span style="display:flex;"><span>            result.<span style="color:#66d9ef">data</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">?.</span>getStringArrayListExtra(<span style="color:#a6e22e">RecognizerIntent</span>.EXTRA_RESULTS)
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">?.</span>firstOrNull()
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">?.</span>takeIf { <span style="color:#66d9ef">it</span>.isNotBlank() }
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">isRecognitionAvailable</span>(context: Context): Boolean {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> pm = context.packageManager
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> activities = pm.queryIntentActivities(
</span></span><span style="display:flex;"><span>            Intent(<span style="color:#a6e22e">RecognizerIntent</span>.ACTION_RECOGNIZE_SPEECH),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">PackageManager</span>.MATCH_DEFAULT_ONLY
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> activities.isNotEmpty()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Key Features:</strong></p>
<ul>
<li>🌍 <strong>Locale handling</strong> - Automatically uses your app&rsquo;s current language</li>
<li>🎤 <strong>Flexible configuration</strong> - Customize prompt, language model, and result count</li>
<li>✅ <strong>Validation</strong> - Ensures speech recognition is available on the device</li>
<li>🧹 <strong>Clean extraction</strong> - Filters out blank results</li>
</ul>
<hr>
<h3 id="2-the-state-holder"><strong>2️⃣ The State Holder</strong></h3>
<p>Next, we create a state class that manages the speech recognition lifecycle:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Stable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SpeechToTextState</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> launcher: ManagedActivityResultLauncher&lt;Intent, ActivityResult&gt;,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> prompt: String?,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> isAvailable: Boolean
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">launch</span>(
</span></span><span style="display:flex;"><span>        customPrompt: String? = prompt,
</span></span><span style="display:flex;"><span>        customLocale: Locale? = <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> intent = <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.createRecognitionIntent(
</span></span><span style="display:flex;"><span>            prompt = customPrompt,
</span></span><span style="display:flex;"><span>            locale = customLocale <span style="color:#f92672">?:</span> <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.getAppLocale()
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        launcher.launch(intent)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Why <code>@Stable</code>?</strong>
The <code>@Stable</code> annotation tells Compose that this class follows specific stability contracts, allowing for better recomposition optimizations.</p>
<hr>
<h3 id="3-the-composable-function"><strong>3️⃣ The Composable Function</strong></h3>
<p>Now comes the magic - a composable that ties everything together:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">rememberSpeechToText</span>(
</span></span><span style="display:flex;"><span>    prompt: String? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    onResult: (String) <span style="color:#f92672">-&gt;</span> Unit
</span></span><span style="display:flex;"><span>): SpeechToTextState {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> launcher = rememberLauncherForActivityResult(
</span></span><span style="display:flex;"><span>        contract = <span style="color:#a6e22e">ActivityResultContracts</span>.StartActivityForResult()
</span></span><span style="display:flex;"><span>    ) { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.extractSpokenText(result)<span style="color:#f92672">?.</span>let { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            onResult(spokenText)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> isAvailable = remember {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">SystemSpeechToTextHelper</span>.isRecognitionAvailable(context)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> remember(launcher, prompt, isAvailable) {
</span></span><span style="display:flex;"><span>        SpeechToTextState(
</span></span><span style="display:flex;"><span>            launcher = launcher,
</span></span><span style="display:flex;"><span>            prompt = prompt,
</span></span><span style="display:flex;"><span>            isAvailable = isAvailable
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Key Points:</strong></p>
<ul>
<li>🔄 <strong>Activity Result API</strong> - Modern way to handle activity results</li>
<li>💾 <strong>Remembered state</strong> - Survives recompositions</li>
<li>🎯 <strong>Callback pattern</strong> - Clean result handling via lambda</li>
</ul>
<hr>
<h3 id="4-bonus-ready-to-use-button-component"><strong>4️⃣ Bonus: Ready-to-Use Button Component</strong></h3>
<p>For convenience, here&rsquo;s a pre-built button component:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">SpeechToTextButton</span>(
</span></span><span style="display:flex;"><span>    speechToTextState: SpeechToTextState,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier,
</span></span><span style="display:flex;"><span>    enabled: Boolean = <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    iconSize: Dp = <span style="color:#ae81ff">24.</span>dp,
</span></span><span style="display:flex;"><span>    tint: Color = <span style="color:#a6e22e">Color</span>.Unspecified,
</span></span><span style="display:flex;"><span>    contentDescription: String? = <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    IconButton(
</span></span><span style="display:flex;"><span>        onClick = speechToTextState<span style="color:#f92672">::</span>launch,
</span></span><span style="display:flex;"><span>        enabled = enabled <span style="color:#f92672">&amp;&amp;</span> speechToTextState.isAvailable,
</span></span><span style="display:flex;"><span>        modifier = modifier
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        Icon(
</span></span><span style="display:flex;"><span>            painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>            contentDescription = contentDescription,
</span></span><span style="display:flex;"><span>            tint = tint,
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.size(iconSize)
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="-real-world-implementation-examples">🚀 Real-World Implementation Examples</h2>
<h3 id="example-1-textfield-with-voice-input-production-ready"><strong>Example 1: TextField with Voice Input (Production-Ready)</strong></h3>
<p>Here&rsquo;s how to properly integrate voice input with a text field, including validation and network checking:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">SmartTextField</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">value</span>: String,
</span></span><span style="display:flex;"><span>    onValueChange: (String) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier,
</span></span><span style="display:flex;"><span>    label: String = <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    placeholder: String = <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>    isError: Boolean = <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    errorMessage: String? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    maxLength: Int? = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    singleLine: Boolean = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> showNetworkWarning <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#66d9ef">false</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Check network connectivity
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> isNetworkAvailable = remember {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> cm = context.getSystemService(<span style="color:#a6e22e">Context</span>.CONNECTIVITY_SERVICE) <span style="color:#66d9ef">as</span> ConnectivityManager
</span></span><span style="display:flex;"><span>        cm.activeNetwork <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText(
</span></span><span style="display:flex;"><span>        prompt = <span style="color:#e6db74">&#34;Speak </span><span style="color:#e6db74">$label</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    ) { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Handle max length validation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> newText = <span style="color:#66d9ef">if</span> (maxLength <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>            spokenText.take(maxLength)
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            spokenText
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        onValueChange(newText)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(modifier = modifier) {
</span></span><span style="display:flex;"><span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = <span style="color:#66d9ef">value</span>,
</span></span><span style="display:flex;"><span>            onValueChange = { newValue <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Enforce max length on manual input too
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">val</span> sanitized = <span style="color:#66d9ef">if</span> (maxLength <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>                    newValue.take(maxLength)
</span></span><span style="display:flex;"><span>                } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>                    newValue
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                onValueChange(sanitized)
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            label = { Text(label) },
</span></span><span style="display:flex;"><span>            placeholder = { Text(placeholder) },
</span></span><span style="display:flex;"><span>            isError = isError,
</span></span><span style="display:flex;"><span>            singleLine = singleLine,
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(),
</span></span><span style="display:flex;"><span>            trailingIcon = {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Only show mic icon if speech recognition is available
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>                    IconButton(
</span></span><span style="display:flex;"><span>                        onClick = {
</span></span><span style="display:flex;"><span>                            <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>                                speechToText.launch()
</span></span><span style="display:flex;"><span>                            } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>                                showNetworkWarning = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>                            }
</span></span><span style="display:flex;"><span>                        }
</span></span><span style="display:flex;"><span>                    ) {
</span></span><span style="display:flex;"><span>                        Icon(
</span></span><span style="display:flex;"><span>                            painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>                            contentDescription = <span style="color:#e6db74">&#34;Voice input for </span><span style="color:#e6db74">$label</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                            tint = <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>                                <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.primary
</span></span><span style="display:flex;"><span>                            } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>                                <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.onSurface.copy(alpha = <span style="color:#ae81ff">0.38f</span>)
</span></span><span style="display:flex;"><span>                            }
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            supportingText = {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">when</span> {
</span></span><span style="display:flex;"><span>                    errorMessage <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">&amp;&amp;</span> isError <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                        Text(
</span></span><span style="display:flex;"><span>                            text = errorMessage,
</span></span><span style="display:flex;"><span>                            color = <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.error
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                    maxLength <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                        Text(
</span></span><span style="display:flex;"><span>                            text = <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${value.length}</span><span style="color:#e6db74">/</span><span style="color:#e6db74">$maxLength</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(),
</span></span><span style="display:flex;"><span>                            textAlign = <span style="color:#a6e22e">TextAlign</span>.End
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Network warning
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">if</span> (showNetworkWarning) {
</span></span><span style="display:flex;"><span>            Text(
</span></span><span style="display:flex;"><span>                text = <span style="color:#e6db74">&#34;Voice input requires internet connection&#34;</span>,
</span></span><span style="display:flex;"><span>                color = <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.error,
</span></span><span style="display:flex;"><span>                style = <span style="color:#a6e22e">MaterialTheme</span>.typography.bodySmall,
</span></span><span style="display:flex;"><span>                modifier = <span style="color:#a6e22e">Modifier</span>.padding(start = <span style="color:#ae81ff">16.</span>dp, top = <span style="color:#ae81ff">4.</span>dp)
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>            LaunchedEffect(Unit) {
</span></span><span style="display:flex;"><span>                delay(<span style="color:#ae81ff">3000</span>)
</span></span><span style="display:flex;"><span>                showNetworkWarning = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Usage:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">FeedbackForm</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> userName <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> feedback <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> maxFeedbackLength = <span style="color:#ae81ff">500</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(modifier = <span style="color:#a6e22e">Modifier</span>.padding(<span style="color:#ae81ff">16.</span>dp)) {
</span></span><span style="display:flex;"><span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = userName,
</span></span><span style="display:flex;"><span>            onValueChange = { userName = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Your Name&#34;</span>,
</span></span><span style="display:flex;"><span>            placeholder = <span style="color:#e6db74">&#34;John Doe&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = <span style="color:#ae81ff">50</span>,
</span></span><span style="display:flex;"><span>            singleLine = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = feedback,
</span></span><span style="display:flex;"><span>            onValueChange = { feedback = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Feedback&#34;</span>,
</span></span><span style="display:flex;"><span>            placeholder = <span style="color:#e6db74">&#34;Tell us what you think...&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = maxFeedbackLength,
</span></span><span style="display:flex;"><span>            singleLine = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="example-2-search-bar-with-voice-input"><strong>Example 2: Search Bar with Voice Input</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">VoiceEnabledSearchBar</span>(
</span></span><span style="display:flex;"><span>    query: String,
</span></span><span style="display:flex;"><span>    onQueryChange: (String) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    onSearch: () <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText(
</span></span><span style="display:flex;"><span>        prompt = <span style="color:#e6db74">&#34;What are you looking for?&#34;</span>
</span></span><span style="display:flex;"><span>    ) { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        onQueryChange(spokenText)
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Auto-search after voice input
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        onSearch()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    SearchBar(
</span></span><span style="display:flex;"><span>        query = query,
</span></span><span style="display:flex;"><span>        onQueryChange = onQueryChange,
</span></span><span style="display:flex;"><span>        onSearch = { onSearch() },
</span></span><span style="display:flex;"><span>        active = <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>        onActiveChange = {},
</span></span><span style="display:flex;"><span>        modifier = modifier,
</span></span><span style="display:flex;"><span>        leadingIcon = {
</span></span><span style="display:flex;"><span>            Icon(
</span></span><span style="display:flex;"><span>                imageVector = <span style="color:#a6e22e">Icons</span>.<span style="color:#a6e22e">Default</span>.Search,
</span></span><span style="display:flex;"><span>                contentDescription = <span style="color:#e6db74">&#34;Search&#34;</span>
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        trailingIcon = {
</span></span><span style="display:flex;"><span>            Row {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Clear button
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">if</span> (query.isNotEmpty()) {
</span></span><span style="display:flex;"><span>                    IconButton(onClick = { onQueryChange(<span style="color:#e6db74">&#34;&#34;</span>) }) {
</span></span><span style="display:flex;"><span>                        Icon(
</span></span><span style="display:flex;"><span>                            imageVector = <span style="color:#a6e22e">Icons</span>.<span style="color:#a6e22e">Default</span>.Close,
</span></span><span style="display:flex;"><span>                            contentDescription = <span style="color:#e6db74">&#34;Clear&#34;</span>
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Voice input button (only if available)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>                    IconButton(onClick = { speechToText.launch() }) {
</span></span><span style="display:flex;"><span>                        Icon(
</span></span><span style="display:flex;"><span>                            painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>                            contentDescription = <span style="color:#e6db74">&#34;Voice search&#34;</span>
</span></span><span style="display:flex;"><span>                        )
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        placeholder = { Text(<span style="color:#e6db74">&#34;Search products...&#34;</span>) }
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Search suggestions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="example-3-multi-line-text-input-with-append-mode"><strong>Example 3: Multi-line Text Input with Append Mode</strong></h3>
<p>Perfect for note-taking or messaging apps:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">VoiceNoteEditor</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> noteContent <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> isRecording <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#66d9ef">false</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText(
</span></span><span style="display:flex;"><span>        prompt = <span style="color:#e6db74">&#34;Speak your note&#34;</span>
</span></span><span style="display:flex;"><span>    ) { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Intelligently append or replace
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        noteContent = <span style="color:#66d9ef">when</span> {
</span></span><span style="display:flex;"><span>            noteContent.isEmpty() <span style="color:#f92672">-&gt;</span> spokenText
</span></span><span style="display:flex;"><span>            noteContent.endsWith(<span style="color:#e6db74">&#34;.&#34;</span>) <span style="color:#f92672">||</span> noteContent.endsWith(<span style="color:#e6db74">&#34;!&#34;</span>) <span style="color:#f92672">||</span> noteContent.endsWith(<span style="color:#e6db74">&#34;?&#34;</span>) <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$noteContent</span><span style="color:#e6db74"> </span><span style="color:#e6db74">$spokenText</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span> <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$noteContent</span><span style="color:#e6db74">. </span><span style="color:#e6db74">$spokenText</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        isRecording = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(
</span></span><span style="display:flex;"><span>        modifier = Modifier
</span></span><span style="display:flex;"><span>            .fillMaxSize()
</span></span><span style="display:flex;"><span>            .padding(<span style="color:#ae81ff">16.</span>dp)
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = noteContent,
</span></span><span style="display:flex;"><span>            onValueChange = { noteContent = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            modifier = Modifier
</span></span><span style="display:flex;"><span>                .fillMaxWidth()
</span></span><span style="display:flex;"><span>                .weight(<span style="color:#ae81ff">1f</span>),
</span></span><span style="display:flex;"><span>            placeholder = {
</span></span><span style="display:flex;"><span>                Text(<span style="color:#e6db74">&#34;Start typing or tap the mic to speak...&#34;</span>)
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            textStyle = <span style="color:#a6e22e">MaterialTheme</span>.typography.bodyLarge
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Row(
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(),
</span></span><span style="display:flex;"><span>            horizontalArrangement = <span style="color:#a6e22e">Arrangement</span>.SpaceBetween,
</span></span><span style="display:flex;"><span>            verticalAlignment = <span style="color:#a6e22e">Alignment</span>.CenterVertically
</span></span><span style="display:flex;"><span>        ) {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Word count
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            Text(
</span></span><span style="display:flex;"><span>                text = <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${noteContent.split(&#34;\\s+&#34;.toRegex()).size}</span><span style="color:#e6db74"> words&#34;</span>,
</span></span><span style="display:flex;"><span>                style = <span style="color:#a6e22e">MaterialTheme</span>.typography.bodySmall,
</span></span><span style="display:flex;"><span>                color = <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.onSurfaceVariant
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Voice input button
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>                FilledTonalButton(
</span></span><span style="display:flex;"><span>                    onClick = {
</span></span><span style="display:flex;"><span>                        isRecording = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>                        speechToText.launch()
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                ) {
</span></span><span style="display:flex;"><span>                    Icon(
</span></span><span style="display:flex;"><span>                        painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>                        contentDescription = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>                        modifier = <span style="color:#a6e22e">Modifier</span>.size(<span style="color:#ae81ff">20.</span>dp)
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                    Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.width(<span style="color:#ae81ff">8.</span>dp))
</span></span><span style="display:flex;"><span>                    Text(<span style="color:#66d9ef">if</span> (isRecording) <span style="color:#e6db74">&#34;Listening...&#34;</span> <span style="color:#66d9ef">else</span> <span style="color:#e6db74">&#34;Add Voice Note&#34;</span>)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="example-4-form-with-conditional-voice-input"><strong>Example 4: Form with Conditional Voice Input</strong></h3>
<p>Shows how to conditionally enable voice input based on field type:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">UserRegistrationForm</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> name <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> email <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> bio <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(modifier = <span style="color:#a6e22e">Modifier</span>.padding(<span style="color:#ae81ff">16.</span>dp)) {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Name field - voice input enabled
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = name,
</span></span><span style="display:flex;"><span>            onValueChange = { name = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Full Name&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = <span style="color:#ae81ff">50</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Email field - voice input disabled (too error-prone)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = email,
</span></span><span style="display:flex;"><span>            onValueChange = { email = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = { Text(<span style="color:#e6db74">&#34;Email&#34;</span>) },
</span></span><span style="display:flex;"><span>            keyboardOptions = KeyboardOptions(
</span></span><span style="display:flex;"><span>                keyboardType = <span style="color:#a6e22e">KeyboardType</span>.Email
</span></span><span style="display:flex;"><span>            ),
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth()
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// No voice input for email - typing is more accurate
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.height(<span style="color:#ae81ff">16.</span>dp))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Bio field - voice input enabled
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        SmartTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = bio,
</span></span><span style="display:flex;"><span>            onValueChange = { bio = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = <span style="color:#e6db74">&#34;Bio&#34;</span>,
</span></span><span style="display:flex;"><span>            placeholder = <span style="color:#e6db74">&#34;Tell us about yourself...&#34;</span>,
</span></span><span style="display:flex;"><span>            maxLength = <span style="color:#ae81ff">200</span>,
</span></span><span style="display:flex;"><span>            singleLine = <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="-critical-implementation-guidelines">⚠️ Critical Implementation Guidelines</h2>
<h3 id="1-always-check-availability"><strong>1. Always Check Availability</strong></h3>
<p><strong>Never show the mic icon if speech recognition is unavailable.</strong> This creates a poor UX when users tap it and nothing happens.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ✅ GOOD - Only show when available
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">if</span> (speechToText.isAvailable) {
</span></span><span style="display:flex;"><span>    SpeechToTextButton(speechToTextState = speechToText)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ❌ BAD - Shows disabled button (confusing UX)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>SpeechToTextButton(
</span></span><span style="display:flex;"><span>    speechToTextState = speechToText,
</span></span><span style="display:flex;"><span>    enabled = speechToText.isAvailable  <span style="color:#75715e">// Don&#39;t do this!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>)
</span></span></code></pre></div><p><strong>Why?</strong> On devices without Google services (some custom ROMs, enterprise devices), the feature won&rsquo;t work. Hiding it entirely is cleaner than showing a permanently disabled button.</p>
<hr>
<h3 id="2-handle-network-connectivity"><strong>2. Handle Network Connectivity</strong></h3>
<p>Speech recognition requires <strong>active internet connection</strong>. Check before launching:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">isNetworkAvailable</span>(context: Context): Boolean {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> cm = context.getSystemService(<span style="color:#a6e22e">Context</span>.CONNECTIVITY_SERVICE) <span style="color:#66d9ef">as</span> ConnectivityManager
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> cm.activeNetwork <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Usage
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> isNetworkAvailable = remember {
</span></span><span style="display:flex;"><span>    isNetworkAvailable(context)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>IconButton(
</span></span><span style="display:flex;"><span>    onClick = {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>            speechToText.launch()
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Show snackbar or toast
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">Toast</span>.makeText(context, <span style="color:#e6db74">&#34;Voice input requires internet&#34;</span>, <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT).show()
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    Icon(
</span></span><span style="display:flex;"><span>        painter = painterResource(id = <span style="color:#a6e22e">R</span>.drawable.ic_mic),
</span></span><span style="display:flex;"><span>        tint = <span style="color:#66d9ef">if</span> (isNetworkAvailable) {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.primary
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MaterialTheme</span>.colorScheme.onSurface.copy(alpha = <span style="color:#ae81ff">0.38f</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Best Practice:</strong> Show the mic icon in a dimmed state when offline, and display a brief message when tapped.</p>
<hr>
<h3 id="3-input-validation-after-voice-input"><strong>3. Input Validation After Voice Input</strong></h3>
<p>Always validate voice input just like you would keyboard input:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> speechToText = rememberSpeechToText { spokenText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Sanitize and validate
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> sanitized = spokenText
</span></span><span style="display:flex;"><span>        .trim()
</span></span><span style="display:flex;"><span>        .take(maxLength)
</span></span><span style="display:flex;"><span>        .filter { <span style="color:#66d9ef">it</span>.isLetterOrDigit() <span style="color:#f92672">||</span> <span style="color:#66d9ef">it</span>.isWhitespace() }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Check if valid
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">if</span> (sanitized.isNotEmpty()) {
</span></span><span style="display:flex;"><span>        inputValue = sanitized
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>        showError(<span style="color:#e6db74">&#34;Invalid input received&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Common validations:</p>
<ul>
<li><strong>Length limits</strong> - Trim to max length</li>
<li><strong>Character filtering</strong> - Remove special chars if needed</li>
<li><strong>Empty checks</strong> - Handle blank results</li>
<li><strong>Format validation</strong> - Email, phone, etc.</li>
</ul>
<hr>
<h3 id="4-permissions---none-required"><strong>4. Permissions - None Required!</strong></h3>
<p><strong>Good news:</strong> No runtime permissions needed! Android&rsquo;s speech recognition uses Google&rsquo;s cloud service, which handles all the heavy lifting.</p>
<p>This is a <strong>huge advantage</strong> over custom speech recognition libraries that require <code>RECORD_AUDIO</code> permission.</p>
<hr>
<h3 id="5-locale-and-language-support"><strong>5. Locale and Language Support</strong></h3>
<p>By default, the implementation respects your app&rsquo;s current locale:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAppLocale</span>(): Locale {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Locale</span>.forLanguageTag(<span style="color:#a6e22e">Language</span>.currentLocale.<span style="color:#66d9ef">value</span>.code)
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Locale</span>.getDefault()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>For multilingual apps</strong>, you can override the locale per-field:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Spanish input for a specific field
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>speechToText.launch(customLocale = Locale(<span style="color:#e6db74">&#34;es&#34;</span>, <span style="color:#e6db74">&#34;ES&#34;</span>))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// French input
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>speechToText.launch(customLocale = <span style="color:#a6e22e">Locale</span>.FRANCE)
</span></span></code></pre></div><hr>
<h3 id="6-when-not-to-use-voice-input"><strong>6. When NOT to Use Voice Input</strong></h3>
<p>Some fields are better suited for keyboard input:</p>
<p>❌ <strong>Email addresses</strong> - Punctuation and special characters are error-prone
❌ <strong>Passwords</strong> - Security risk + poor accuracy
❌ <strong>Credit card numbers</strong> - High error rate + security concerns
❌ <strong>URLs</strong> - Complex syntax not recognized well
❌ <strong>Code snippets</strong> - Special characters and formatting issues</p>
<p>✅ <strong>Good use cases:</strong>
✔ Names, addresses, descriptions
✔ Search queries
✔ Notes and messages
✔ Feedback and reviews
✔ Long-form text content</p>
<hr>
<h2 id="-ux-impact-the-numbers">📊 UX Impact: The Numbers</h2>
<p>Why voice input matters for your app&rsquo;s user experience:</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Typing</th>
          <th>Voice Input</th>
          <th>Improvement</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Average Speed</strong></td>
          <td>40 words/min</td>
          <td>150+ words/min</td>
          <td><strong>3.75x faster</strong></td>
      </tr>
      <tr>
          <td><strong>Error Rate</strong></td>
          <td>2-3%</td>
          <td>5-8% (but faster to correct)</td>
          <td>Context dependent</td>
      </tr>
      <tr>
          <td><strong>User Effort</strong></td>
          <td>High (small keyboards)</td>
          <td>Low (hands-free)</td>
          <td><strong>Significantly lower</strong></td>
      </tr>
      <tr>
          <td><strong>Accessibility</strong></td>
          <td>Difficult for some users</td>
          <td>Easy for most users</td>
          <td><strong>Universal access</strong></td>
      </tr>
  </tbody>
</table>
<p><strong>Real-world impact:</strong></p>
<ul>
<li>📝 A 100-word product review takes <strong>2.5 minutes typing</strong> vs <strong>40 seconds speaking</strong></li>
<li>🔍 Voice search feels instantaneous vs typing lag</li>
<li>♿ Critical for users with motor impairments, RSI, or visual limitations</li>
<li>🌍 Easier for non-native keyboard users</li>
</ul>
<hr>
<h2 id="-why-this-implementation-is-superior">✅ Why This Implementation is Superior</h2>
<h3 id="compared-to-keyboard-input"><strong>Compared to Keyboard Input:</strong></h3>
<p>✔ <strong>3-5x faster input</strong> for long text
✔ <strong>Lower cognitive load</strong> - speaking is more natural than typing
✔ <strong>Better mobile experience</strong> - no tiny keyboard frustration
✔ <strong>Hands-free operation</strong> - can be used while multitasking</p>
<h3 id="compared-to-third-party-libraries"><strong>Compared to Third-Party Libraries:</strong></h3>
<p>✔ <strong>Zero app size increase</strong> - uses system APIs
✔ <strong>No permissions required</strong> - no <code>RECORD_AUDIO</code> prompt
✔ <strong>Always up-to-date</strong> - Google maintains the recognition engine
✔ <strong>No API keys or quotas</strong> - completely free
✔ <strong>Better privacy</strong> - uses Google&rsquo;s standard speech service (same as Gboard)</p>
<h3 id="compared-to-custom-ml-models"><strong>Compared to Custom ML Models:</strong></h3>
<p>✔ <strong>No model training needed</strong>
✔ <strong>No storage for ML models</strong> (models can be 50MB+)
✔ <strong>Supports 100+ languages</strong> out of the box
✔ <strong>Continuously improving</strong> - benefits from Google&rsquo;s updates</p>
<hr>
<h2 id="-when-voice-input-makes-sense">🎯 When Voice Input Makes Sense</h2>
<h3 id="perfect-use-cases"><strong>Perfect Use Cases:</strong></h3>
<ul>
<li>📝 <strong>Note-taking and memos</strong> - Natural dictation flow</li>
<li>💬 <strong>Messaging and chat</strong> - Quick voice-to-text messages</li>
<li>🔍 <strong>Search queries</strong> - Faster than typing</li>
<li>📋 <strong>Long-form content</strong> - Reviews, feedback, descriptions</li>
<li>♿ <strong>Accessibility features</strong> - Essential for many users</li>
<li>🚗 <strong>Hands-free scenarios</strong> - When typing isn&rsquo;t safe</li>
</ul>
<h3 id="skip-voice-input-for"><strong>Skip Voice Input For:</strong></h3>
<ul>
<li>🔒 <strong>Sensitive data</strong> - Passwords, PINs, SSNs</li>
<li>📧 <strong>Format-specific fields</strong> - Emails, URLs, credit cards</li>
<li>🔢 <strong>Numeric codes</strong> - OTPs, account numbers</li>
<li>💻 <strong>Technical input</strong> - Code, command-line syntax</li>
</ul>
<hr>
<h2 id="-performance-considerations">🚀 Performance Considerations</h2>
<p><strong>App Size Impact:</strong> <strong>0 KB</strong> - Uses system APIs only</p>
<p><strong>Runtime Performance:</strong></p>
<ul>
<li>Minimal memory usage</li>
<li>Lazy initialization (only when needed)</li>
<li>No background processes</li>
<li>Network call only during active recognition</li>
</ul>
<p><strong>Battery Impact:</strong></p>
<ul>
<li>Negligible - recognition happens on Google&rsquo;s servers</li>
<li>No continuous listening (only when user taps mic)</li>
<li>Automatic cleanup after recognition</li>
</ul>
<hr>
<h2 id="-quick-implementation-checklist">🎁 Quick Implementation Checklist</h2>
<p>Before shipping voice input to production, verify:</p>
<ul>
<li><input disabled="" type="checkbox"> ✅ Mic icon only shows when <code>isAvailable == true</code></li>
<li><input disabled="" type="checkbox"> 🌐 Network connectivity is checked before launching</li>
<li><input disabled="" type="checkbox"> ✍️ Input validation applied to voice results</li>
<li><input disabled="" type="checkbox"> 📏 Max length limits enforced</li>
<li><input disabled="" type="checkbox"> 🌍 Proper locale configuration</li>
<li><input disabled="" type="checkbox"> ⚠️ User feedback for network errors</li>
<li><input disabled="" type="checkbox"> 📱 Tested on devices without Google services</li>
<li><input disabled="" type="checkbox"> ♿ Content descriptions added for accessibility</li>
<li><input disabled="" type="checkbox"> 🎨 Visual feedback when mic is active (if custom UI)</li>
</ul>
<hr>
<h2 id="-related-resources">🔗 Related Resources</h2>
<ul>
<li><a href="https://developer.android.com/reference/android/speech/RecognizerIntent">Android Speech Recognition Guide</a></li>
<li><a href="https://developer.android.com/training/basics/intents/result">Jetpack Compose Activity Result API</a></li>
<li><a href="https://m3.material.io/foundations/interaction/input">Material Design Voice Input Guidelines</a></li>
</ul>
<hr>
<h2 id="-final-thoughts">💡 Final Thoughts</h2>
<p>Voice input is a <strong>low-effort, high-impact feature</strong> that most apps overlook. With zero dependencies, no permissions, and minimal code, there&rsquo;s little reason not to add it where appropriate.</p>
<p><strong>The key differentiators:</strong></p>
<ol>
<li><strong>Always check availability</strong> - hide the feature gracefully when unavailable</li>
<li><strong>Validate network state</strong> - provide feedback when offline</li>
<li><strong>Apply proper validation</strong> - treat voice input like any other input</li>
<li><strong>Choose appropriate fields</strong> - not everything needs voice input</li>
</ol>
<p>By following these guidelines, you&rsquo;ll provide a professional, polished experience that sets your app apart.</p>
<hr>
<p><strong>That&rsquo;s it!</strong> You now have a fully functional, production-ready speech-to-text component for Jetpack Compose. 🎉</p>
<p>Feel free to customize this implementation to fit your app&rsquo;s specific needs. If you have questions or suggestions, reach out via my social handles! 😊</p>
<p><strong>Happy coding!</strong> 🚀</p>
]]></content:encoded></item><item><title>Android ML Kit Document Scanner: Stop Using Camera Capture for Documents</title><link>https://md.eknath.dev/posts/android-ml-kit-document-scanner/</link><pubDate>Tue, 10 Feb 2026 21:30:00 +0530</pubDate><guid>https://md.eknath.dev/posts/android-ml-kit-document-scanner/</guid><description>&lt;h2 id="tldr---why-ml-kit-document-scanner-changes-everything">TL;DR - Why ML Kit Document Scanner Changes Everything&lt;/h2>
&lt;p>Stop building custom camera UIs for document capture. &lt;strong>ML Kit Document Scanner&lt;/strong> gives you a professional, AI-powered document scanning experience with just a few lines of code:&lt;/p>
&lt;p>✅ &lt;strong>Automatic edge detection&lt;/strong> - AI finds document boundaries instantly
✅ &lt;strong>Perspective correction&lt;/strong> - Automatically straightens skewed documents
✅ &lt;strong>Shadow removal&lt;/strong> - Intelligent lighting correction
✅ &lt;strong>Multi-page support&lt;/strong> - Scan multiple pages in one session
✅ &lt;strong>Quality enhancement&lt;/strong> - Auto-adjusts contrast and brightness
✅ &lt;strong>Minimal code&lt;/strong> - 10 lines vs 500+ for custom implementation
✅ &lt;strong>Small library size&lt;/strong> - ~3MB vs building from scratch&lt;/p></description><content:encoded><![CDATA[<h2 id="tldr---why-ml-kit-document-scanner-changes-everything">TL;DR - Why ML Kit Document Scanner Changes Everything</h2>
<p>Stop building custom camera UIs for document capture. <strong>ML Kit Document Scanner</strong> gives you a professional, AI-powered document scanning experience with just a few lines of code:</p>
<p>✅ <strong>Automatic edge detection</strong> - AI finds document boundaries instantly
✅ <strong>Perspective correction</strong> - Automatically straightens skewed documents
✅ <strong>Shadow removal</strong> - Intelligent lighting correction
✅ <strong>Multi-page support</strong> - Scan multiple pages in one session
✅ <strong>Quality enhancement</strong> - Auto-adjusts contrast and brightness
✅ <strong>Minimal code</strong> - 10 lines vs 500+ for custom implementation
✅ <strong>Small library size</strong> - ~3MB vs building from scratch</p>
<p><strong>The result?</strong> Professional document scanning that rivals dedicated scanner apps, with 95% less code and effort.</p>
<hr>
<h2 id="why-developers-still-use-basic-camera-capture">Why Developers Still Use Basic Camera Capture</h2>
<p>Despite ML Kit Document Scanner being available since 2022, most apps still use basic camera capture for documents. Here&rsquo;s why:</p>
<ol>
<li><strong>Lack of awareness</strong> - Many developers don&rsquo;t know it exists</li>
<li><strong>&ldquo;Camera is good enough&rdquo;</strong> - Until users complain about quality</li>
<li><strong>Custom UI preference</strong> - Wanting full control (unnecessary)</li>
<li><strong>Assumed complexity</strong> - Thinking it requires ML expertise</li>
<li><strong>Library size concerns</strong> - Actually very reasonable (~3MB)</li>
</ol>
<p><strong>The reality:</strong> Basic camera capture for documents creates <strong>terrible UX</strong> compared to proper document scanning.</p>
<hr>
<h2 id="the-problem-with-basic-camera-capture">The Problem with Basic Camera Capture</h2>
<h3 id="what-happens-with-regular-camera">What Happens with Regular Camera:</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Typical camera implementation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> cameraIntent = Intent(<span style="color:#a6e22e">MediaStore</span>.ACTION_IMAGE_CAPTURE)
</span></span><span style="display:flex;"><span>launcher.launch(cameraIntent)
</span></span><span style="display:flex;"><span><span style="color:#75715e">// User gets: blurry, skewed, shadowy image
</span></span></span></code></pre></div><p><strong>User experience issues:</strong></p>
<ul>
<li>📸 No guidance on document boundaries</li>
<li>🔲 Manual cropping required (tedious)</li>
<li>🌓 Poor lighting = unusable scans</li>
<li>📐 Perspective distortion (holding phone at angle)</li>
<li>📄 One page at a time (inefficient for multi-page docs)</li>
<li>🎨 No enhancement (washed out, low contrast)</li>
</ul>
<p><strong>Result:</strong> Users waste time cropping, retaking photos, and dealing with poor quality scans.</p>
<hr>
<h2 id="what-ml-kit-document-scanner-provides">What ML Kit Document Scanner Provides</h2>
<h3 id="the-complete-package">The Complete Package:</h3>
<ol>
<li>
<p><strong>Real-time Edge Detection</strong></p>
<ul>
<li>AI instantly finds document corners</li>
<li>Visual overlay shows detected boundaries</li>
<li>Works even with complex backgrounds</li>
</ul>
</li>
<li>
<p><strong>Auto Perspective Correction</strong></p>
<ul>
<li>Straightens tilted/skewed documents</li>
<li>Removes keystoning (trapezoid effect)</li>
<li>Perfect rectangular output every time</li>
</ul>
</li>
<li>
<p><strong>Smart Enhancement</strong></p>
<ul>
<li>Removes shadows and glare</li>
<li>Adjusts contrast automatically</li>
<li>Optimizes for text readability</li>
<li>Handles various lighting conditions</li>
</ul>
</li>
<li>
<p><strong>Multi-page Scanning</strong></p>
<ul>
<li>Scan entire documents in one flow</li>
<li>Add/remove pages easily</li>
<li>Page reordering built-in</li>
</ul>
</li>
<li>
<p><strong>Multiple Export Formats</strong></p>
<ul>
<li>High-quality images (JPEG/PNG)</li>
<li>PDF generation built-in</li>
<li>Configurable resolution</li>
</ul>
</li>
</ol>
<hr>
<h2 id="implementation">Implementation</h2>
<h3 id="step-1-add-dependencies"><strong>Step 1: Add Dependencies</strong></h3>
<p>Add to your app&rsquo;s <code>build.gradle</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-gradle" data-lang="gradle"><span style="display:flex;"><span>dependencies <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ML Kit Document Scanner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    implementation <span style="color:#e6db74">&#39;com.google.android.gms:play-services-mlkit-document-scanner:16.0.0-beta1&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p><strong>Library size:</strong> ~3MB (tiny compared to the functionality you get!)</p>
<hr>
<h3 id="step-2-configure-scanner-options"><strong>Step 2: Configure Scanner Options</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScanner
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_JPEG
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.RESULT_FORMAT_PDF
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.mlkit.vision.documentscanner.GmsDocumentScannerOptions.SCANNER_MODE_FULL
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Create scanner with options
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> options = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setGalleryImportAllowed(<span style="color:#66d9ef">true</span>)              <span style="color:#75715e">// Allow importing from gallery
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setPageLimit(<span style="color:#ae81ff">10</span>)                            <span style="color:#75715e">// Max 10 pages per scan
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF)  <span style="color:#75715e">// Get both formats
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setScannerMode(SCANNER_MODE_FULL)           <span style="color:#75715e">// Full scanning experience
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .build()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">val</span> scanner = <span style="color:#a6e22e">GmsDocumentScanning</span>.getClient(options)
</span></span></code></pre></div><p><strong>Scanner Modes:</strong></p>
<ul>
<li><code>SCANNER_MODE_FULL</code> - Complete UI with all features (recommended)</li>
<li><code>SCANNER_MODE_BASE</code> - Minimal UI, faster scanning</li>
</ul>
<hr>
<h3 id="step-3-launch-scanner-and-handle-results"><strong>Step 3: Launch Scanner and Handle Results</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Activity Result Launcher
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">private</span> <span style="color:#66d9ef">val</span> scannerLauncher = registerForActivityResult(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ActivityResultContracts</span>.StartIntentSenderForResult()
</span></span><span style="display:flex;"><span>) { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (result.resultCode <span style="color:#f92672">==</span> RESULT_OK) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> scanningResult = <span style="color:#a6e22e">GmsDocumentScanningResult</span>.fromActivityResultIntent(result.<span style="color:#66d9ef">data</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        scanningResult<span style="color:#f92672">?.</span>let { scanResult <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Get scanned pages
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            scanResult.pages<span style="color:#f92672">?.</span>let { pages <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                pages.forEach { page <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Access page image URI
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    <span style="color:#66d9ef">val</span> imageUri = page.imageUri
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Use the scanned image
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    loadScannedImage(imageUri)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Get PDF if generated
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            scanResult.pdf<span style="color:#f92672">?.</span>let { pdf <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">val</span> pdfUri = pdf.uri
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">val</span> pageCount = pdf.pageCount
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Save or share PDF
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                savePdfDocument(pdfUri, pageCount)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Launch scanner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">startDocumentScan</span>() {
</span></span><span style="display:flex;"><span>    scanner.getStartScanIntent(<span style="color:#66d9ef">this</span>)
</span></span><span style="display:flex;"><span>        .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            scannerLauncher.launch(
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build()
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        .addOnFailureListener { exception <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Handle error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">Log</span>.e(<span style="color:#e6db74">&#34;Scanner&#34;</span>, <span style="color:#e6db74">&#34;Failed to start scanner&#34;</span>, exception)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h3 id="step-4-complete-jetpack-compose-integration"><strong>Step 4: Complete Jetpack Compose Integration</strong></h3>
<p>Here&rsquo;s a production-ready composable implementation:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">DocumentScannerButton</span>(
</span></span><span style="display:flex;"><span>    onDocumentsScanned: (List&lt;Uri&gt;) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    onPdfGenerated: (Uri, Int) <span style="color:#f92672">-&gt;</span> Unit,
</span></span><span style="display:flex;"><span>    modifier: Modifier = Modifier,
</span></span><span style="display:flex;"><span>    enabled: Boolean = <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> activity = context <span style="color:#66d9ef">as</span>? ComponentActivity
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Scanner options
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> scanner = remember {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> options = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>            .setGalleryImportAllowed(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>            .setPageLimit(<span style="color:#ae81ff">10</span>)
</span></span><span style="display:flex;"><span>            .setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF)
</span></span><span style="display:flex;"><span>            .setScannerMode(SCANNER_MODE_FULL)
</span></span><span style="display:flex;"><span>            .build()
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">GmsDocumentScanning</span>.getClient(options)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Result launcher
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> scannerLauncher = rememberLauncherForActivityResult(
</span></span><span style="display:flex;"><span>        contract = <span style="color:#a6e22e">ActivityResultContracts</span>.StartIntentSenderForResult()
</span></span><span style="display:flex;"><span>    ) { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (result.resultCode <span style="color:#f92672">==</span> <span style="color:#a6e22e">ComponentActivity</span>.RESULT_OK) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">val</span> scanResult = <span style="color:#a6e22e">GmsDocumentScanningResult</span>.fromActivityResultIntent(result.<span style="color:#66d9ef">data</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            scanResult<span style="color:#f92672">?.</span>let {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Handle scanned images
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">it</span>.pages<span style="color:#f92672">?.</span>let { pages <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">val</span> imageUris = pages.mapNotNull { page <span style="color:#f92672">-&gt;</span> page.imageUri }
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> (imageUris.isNotEmpty()) {
</span></span><span style="display:flex;"><span>                        onDocumentsScanned(imageUris)
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Handle PDF
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">it</span>.pdf<span style="color:#f92672">?.</span>let { pdf <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    onPdfGenerated(pdf.uri, pdf.pageCount)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Launch scanner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">launchScanner</span>() {
</span></span><span style="display:flex;"><span>        activity<span style="color:#f92672">?.</span>let { act <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            scanner.getStartScanIntent(act)
</span></span><span style="display:flex;"><span>                .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    scannerLauncher.launch(
</span></span><span style="display:flex;"><span>                        <span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build()
</span></span><span style="display:flex;"><span>                    )
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                .addOnFailureListener { exception <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Log</span>.e(<span style="color:#e6db74">&#34;DocumentScanner&#34;</span>, <span style="color:#e6db74">&#34;Failed to start&#34;</span>, exception)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Button(
</span></span><span style="display:flex;"><span>        onClick = { launchScanner() },
</span></span><span style="display:flex;"><span>        enabled = enabled,
</span></span><span style="display:flex;"><span>        modifier = modifier
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        Icon(
</span></span><span style="display:flex;"><span>            imageVector = <span style="color:#a6e22e">Icons</span>.<span style="color:#a6e22e">Default</span>.DocumentScanner,
</span></span><span style="display:flex;"><span>            contentDescription = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>            modifier = <span style="color:#a6e22e">Modifier</span>.size(<span style="color:#ae81ff">20.</span>dp)
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>        Spacer(modifier = <span style="color:#a6e22e">Modifier</span>.width(<span style="color:#ae81ff">8.</span>dp))
</span></span><span style="display:flex;"><span>        Text(<span style="color:#e6db74">&#34;Scan Document&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Usage:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">DocumentUploadScreen</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> scannedImages <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;List&lt;Uri&gt;&gt;(emptyList()) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> pdfUri <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;Uri?&gt;(<span style="color:#66d9ef">null</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column(
</span></span><span style="display:flex;"><span>        modifier = Modifier
</span></span><span style="display:flex;"><span>            .fillMaxSize()
</span></span><span style="display:flex;"><span>            .padding(<span style="color:#ae81ff">16.</span>dp)
</span></span><span style="display:flex;"><span>    ) {
</span></span><span style="display:flex;"><span>        DocumentScannerButton(
</span></span><span style="display:flex;"><span>            onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                scannedImages = images
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>                    context,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;Scanned </span><span style="color:#e6db74">${images.size}</span><span style="color:#e6db74"> pages&#34;</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT
</span></span><span style="display:flex;"><span>                ).show()
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            onPdfGenerated = { uri, pageCount <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                pdfUri = uri
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>                    context,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;PDF created with </span><span style="color:#e6db74">$pageCount</span><span style="color:#e6db74"> pages&#34;</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT
</span></span><span style="display:flex;"><span>                ).show()
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Display scanned images
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        LazyColumn {
</span></span><span style="display:flex;"><span>            items(scannedImages) { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                AsyncImage(
</span></span><span style="display:flex;"><span>                    model = imageUri,
</span></span><span style="display:flex;"><span>                    contentDescription = <span style="color:#e6db74">&#34;Scanned page&#34;</span>,
</span></span><span style="display:flex;"><span>                    modifier = Modifier
</span></span><span style="display:flex;"><span>                        .fillMaxWidth()
</span></span><span style="display:flex;"><span>                        .height(<span style="color:#ae81ff">200.</span>dp)
</span></span><span style="display:flex;"><span>                        .padding(vertical = <span style="color:#ae81ff">8.</span>dp)
</span></span><span style="display:flex;"><span>                )
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="real-world-use-cases">Real-World Use Cases</h2>
<h3 id="1-iddocument-verification"><strong>1. ID/Document Verification</strong></h3>
<p>Perfect for KYC (Know Your Customer) flows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">KYCDocumentUpload</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> idCardUri <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;Uri?&gt;(<span style="color:#66d9ef">null</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column {
</span></span><span style="display:flex;"><span>        Text(<span style="color:#e6db74">&#34;Upload ID Card&#34;</span>, style = <span style="color:#a6e22e">MaterialTheme</span>.typography.titleLarge)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        DocumentScannerButton(
</span></span><span style="display:flex;"><span>            onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                idCardUri = images.firstOrNull()
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Automatically extract text with ML Kit Text Recognition
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                verifyIDDocument(idCardUri)
</span></span><span style="display:flex;"><span>            },
</span></span><span style="display:flex;"><span>            onPdfGenerated = { _, _ <span style="color:#f92672">-&gt;</span> }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        idCardUri<span style="color:#f92672">?.</span>let { uri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            AsyncImage(
</span></span><span style="display:flex;"><span>                model = uri,
</span></span><span style="display:flex;"><span>                contentDescription = <span style="color:#e6db74">&#34;ID Card&#34;</span>,
</span></span><span style="display:flex;"><span>                modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth()
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="2-receiptinvoice-scanning"><strong>2. Receipt/Invoice Scanning</strong></h3>
<p>For expense tracking or accounting apps:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">ExpenseReceiptScanner</span>(
</span></span><span style="display:flex;"><span>    onReceiptScanned: (Uri, String) <span style="color:#f92672">-&gt;</span> Unit
</span></span><span style="display:flex;"><span>) {
</span></span><span style="display:flex;"><span>    DocumentScannerButton(
</span></span><span style="display:flex;"><span>        onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            images.firstOrNull()<span style="color:#f92672">?.</span>let { receiptUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Extract text from receipt
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                <span style="color:#66d9ef">val</span> amount = extractReceiptAmount(receiptUri)
</span></span><span style="display:flex;"><span>                onReceiptScanned(receiptUri, amount)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        onPdfGenerated = { _, _ <span style="color:#f92672">-&gt;</span> }
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="3-multi-page-document-archival"><strong>3. Multi-page Document Archival</strong></h3>
<p>For scanning contracts, forms, or books:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">DocumentArchiver</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> documentTitle <span style="color:#66d9ef">by</span> remember { mutableStateOf(<span style="color:#e6db74">&#34;&#34;</span>) }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> savedPdfUri <span style="color:#66d9ef">by</span> remember { mutableStateOf&lt;Uri?&gt;(<span style="color:#66d9ef">null</span>) }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Column {
</span></span><span style="display:flex;"><span>        OutlinedTextField(
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">value</span> = documentTitle,
</span></span><span style="display:flex;"><span>            onValueChange = { documentTitle = <span style="color:#66d9ef">it</span> },
</span></span><span style="display:flex;"><span>            label = { Text(<span style="color:#e6db74">&#34;Document Name&#34;</span>) }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        DocumentScannerButton(
</span></span><span style="display:flex;"><span>            onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Individual pages available if needed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            },
</span></span><span style="display:flex;"><span>            onPdfGenerated = { pdfUri, pageCount <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Save PDF with title
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                savedPdfUri = pdfUri
</span></span><span style="display:flex;"><span>                saveToDocuments(documentTitle, pdfUri)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="4-note-taking-apps"><strong>4. Note-Taking Apps</strong></h3>
<p>Scan handwritten notes or whiteboard content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">ScanAndConvertNotes</span>() {
</span></span><span style="display:flex;"><span>    DocumentScannerButton(
</span></span><span style="display:flex;"><span>        onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            images.forEach { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Use ML Kit Text Recognition
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                extractHandwrittenText(imageUri) { text <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Convert to editable text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                    saveAsNote(text)
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        },
</span></span><span style="display:flex;"><span>        onPdfGenerated = { _, _ <span style="color:#f92672">-&gt;</span> }
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="advanced-configuration-options">Advanced Configuration Options</h2>
<h3 id="custom-scanner-settings"><strong>Custom Scanner Settings</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Minimal scanner (faster, less features)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> minimalOptions = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setScannerMode(SCANNER_MODE_BASE)
</span></span><span style="display:flex;"><span>    .setPageLimit(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>    .setResultFormats(RESULT_FORMAT_JPEG)
</span></span><span style="display:flex;"><span>    .setGalleryImportAllowed(<span style="color:#66d9ef">false</span>)
</span></span><span style="display:flex;"><span>    .build()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Professional scanner (all features)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> professionalOptions = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setScannerMode(SCANNER_MODE_FULL)
</span></span><span style="display:flex;"><span>    .setPageLimit(<span style="color:#ae81ff">50</span>)                           <span style="color:#75715e">// Up to 50 pages
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    .setResultFormats(RESULT_FORMAT_JPEG, RESULT_FORMAT_PDF)
</span></span><span style="display:flex;"><span>    .setGalleryImportAllowed(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>    .build()
</span></span></code></pre></div><h3 id="handling-different-result-formats"><strong>Handling Different Result Formats</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>scanningResult<span style="color:#f92672">?.</span>let { result <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Option 1: Process individual images
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    result.pages<span style="color:#f92672">?.</span>forEach { page <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> imageUri = page.imageUri
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Each page as separate image
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        processImage(imageUri)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Option 2: Get consolidated PDF
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    result.pdf<span style="color:#f92672">?.</span>let { pdf <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> pdfUri = pdf.uri
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> pageCount = pdf.pageCount
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Single PDF with all pages
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        sharePDF(pdfUri)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="comparison-before-vs-after">Comparison: Before vs After</h2>
<h3 id="custom-camera-implementation-old-way"><strong>Custom Camera Implementation (Old Way)</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// 500+ lines of code for:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">CustomCameraActivity</span> : AppCompatActivity() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">lateinit</span> <span style="color:#66d9ef">var</span> cameraProvider: ProcessCameraProvider
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">var</span> imageCapture: ImageCapture? = <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Camera setup
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Permission handling
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Custom UI overlay
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Manual cropping UI
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Image enhancement logic
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Edge detection algorithm
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Perspective correction math
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Multi-page management
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// PDF generation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// Error handling
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// ... 450+ more lines
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p><strong>Problems:</strong></p>
<ul>
<li>500+ lines of complex code</li>
<li>Camera permission management</li>
<li>Device compatibility issues</li>
<li>Manual cropping UI needed</li>
<li>No auto enhancement</li>
<li>Mediocre results</li>
<li>Maintenance burden</li>
</ul>
<h3 id="ml-kit-document-scanner-new-way"><strong>ML Kit Document Scanner (New Way)</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// 10 lines of code:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> scanner = <span style="color:#a6e22e">GmsDocumentScanning</span>.getClient(options)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>    .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        launcher.launch(<span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build())
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Done! Professional scanning with AI.
</span></span></span></code></pre></div><p><strong>Benefits:</strong></p>
<ul>
<li>10 lines of code</li>
<li>No permissions needed</li>
<li>Works on all devices</li>
<li>Auto cropping included</li>
<li>AI enhancement</li>
<li>Professional results</li>
<li>Zero maintenance</li>
</ul>
<hr>
<h2 id="performance--best-practices">Performance &amp; Best Practices</h2>
<h3 id="library-size-impact"><strong>Library Size Impact</strong></h3>
<pre tabindex="0"><code>ML Kit Document Scanner:  ~3MB
Custom camera + CV libs:  ~15-25MB
</code></pre><p><strong>Worth it?</strong> Absolutely. You get professional features for 1/5th the size.</p>
<h3 id="memory-management"><strong>Memory Management</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Don&#39;t load all images at once
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>scannedImages.forEach { uri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Process one at a time
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    processImage(uri)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Or use paging for large batches
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Use Coil/Glide for efficient image loading
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>AsyncImage(
</span></span><span style="display:flex;"><span>    model = imageUri,
</span></span><span style="display:flex;"><span>    contentDescription = <span style="color:#66d9ef">null</span>,
</span></span><span style="display:flex;"><span>    contentScale = <span style="color:#a6e22e">ContentScale</span>.Fit
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h3 id="error-handling"><strong>Error Handling</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>    .addOnSuccessListener { intentSender <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        launcher.launch(<span style="color:#a6e22e">IntentSenderRequest</span>.Builder(intentSender).build())
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    .addOnFailureListener { exception <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">when</span> (exception) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">is</span> MlKitException <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// ML Kit specific error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                showError(<span style="color:#e6db74">&#34;Scanner unavailable: </span><span style="color:#e6db74">${exception.message}</span><span style="color:#e6db74">&#34;</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">else</span> <span style="color:#f92672">-&gt;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// Generic error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                showError(<span style="color:#e6db74">&#34;Failed to start scanner&#34;</span>)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div><h3 id="testing-on-different-devices"><strong>Testing on Different Devices</strong></h3>
<p>ML Kit Document Scanner works on:</p>
<ul>
<li>✅ Android 5.0+ (API 21+)</li>
<li>✅ Devices with Google Play Services</li>
<li>✅ All form factors (phones, tablets)</li>
<li>✅ Various camera qualities</li>
</ul>
<p><strong>Note:</strong> Requires Google Play Services. Check availability:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">isDocumentScannerAvailable</span>(context: Context): Boolean {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> status = <span style="color:#a6e22e">GoogleApiAvailability</span>.getInstance()
</span></span><span style="display:flex;"><span>            .isGooglePlayServicesAvailable(context)
</span></span><span style="display:flex;"><span>        status <span style="color:#f92672">==</span> <span style="color:#a6e22e">ConnectionResult</span>.SUCCESS
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="when-to-use-ml-kit-vs-custom-camera">When to Use ML Kit vs Custom Camera</h2>
<h3 id="use-ml-kit-document-scanner-when"><strong>Use ML Kit Document Scanner When:</strong></h3>
<p>✅ Scanning documents, receipts, IDs, contracts
✅ Need professional quality scans
✅ Want multi-page support
✅ Need PDF generation
✅ Auto enhancement required
✅ Limited development time/budget</p>
<h3 id="use-custom-camera-when"><strong>Use Custom Camera When:</strong></h3>
<p>⚠️ Capturing photos (not documents)
⚠️ Need real-time filters/effects
⚠️ Building a camera app
⚠️ Very specific custom workflow
⚠️ Can&rsquo;t use Google Play Services</p>
<p><strong>Bottom line:</strong> For 95% of document capture use cases, ML Kit is superior.</p>
<hr>
<h2 id="common-pitfalls-to-avoid">Common Pitfalls to Avoid</h2>
<h3 id="1-not-checking-for-play-services"><strong>1. Not Checking for Play Services</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ❌ BAD - Assumes availability
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ✅ GOOD - Check first
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">if</span> (isDocumentScannerAvailable(context)) {
</span></span><span style="display:flex;"><span>    scanner.getStartScanIntent(activity)
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>    showFallbackOption()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="2-ignoring-uri-permissions"><strong>2. Ignoring URI Permissions</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ✅ Grant persistent URI permissions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>contentResolver.takePersistableUriPermission(
</span></span><span style="display:flex;"><span>    uri,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Intent</span>.FLAG_GRANT_READ_URI_PERMISSION
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h3 id="3-not-handling-page-limits"><strong>3. Not Handling Page Limits</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// ✅ Set appropriate page limits
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> options = <span style="color:#a6e22e">GmsDocumentScannerOptions</span>.Builder()
</span></span><span style="display:flex;"><span>    .setPageLimit(
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (multiPage) <span style="color:#ae81ff">50</span> <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">1</span>  <span style="color:#75715e">// Adjust based on use case
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    )
</span></span><span style="display:flex;"><span>    .build()
</span></span></code></pre></div><hr>
<h2 id="integration-with-other-ml-kit-features">Integration with Other ML Kit Features</h2>
<h3 id="combine-with-text-recognition"><strong>Combine with Text Recognition</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Scan document, then extract text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    images.forEach { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        recognizeText(imageUri) { extractedText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Use extracted text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            saveDocumentWithText(imageUri, extractedText)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">recognizeText</span>(uri: Uri, onTextExtracted: (String) <span style="color:#f92672">-&gt;</span> Unit) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> image = <span style="color:#a6e22e">InputImage</span>.fromFilePath(context, uri)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> recognizer = <span style="color:#a6e22e">TextRecognition</span>.getClient(<span style="color:#a6e22e">TextRecognizerOptions</span>.DEFAULT_OPTIONS)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    recognizer.process(image)
</span></span><span style="display:flex;"><span>        .addOnSuccessListener { visionText <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            onTextExtracted(visionText.text)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="barcode-scanning-from-documents"><strong>Barcode Scanning from Documents</strong></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Scan document with barcode/QR code
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>onDocumentsScanned = { images <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>    images.forEach { imageUri <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>        scanBarcode(imageUri) { barcodeValue <span style="color:#f92672">-&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Handle barcode data
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="quick-implementation-checklist">Quick Implementation Checklist</h2>
<p>Before shipping document scanning to production:</p>
<ul>
<li><input disabled="" type="checkbox"> ✅ Added ML Kit dependency (~3MB)</li>
<li><input disabled="" type="checkbox"> 📱 Tested on devices with/without Play Services</li>
<li><input disabled="" type="checkbox"> 🔧 Configured appropriate scanner mode</li>
<li><input disabled="" type="checkbox"> 📄 Set reasonable page limits</li>
<li><input disabled="" type="checkbox"> 🎨 Handled both image and PDF results</li>
<li><input disabled="" type="checkbox"> ⚠️ Implemented error handling</li>
<li><input disabled="" type="checkbox"> 💾 Managed URI permissions properly</li>
<li><input disabled="" type="checkbox"> 🧪 Tested with various document types</li>
<li><input disabled="" type="checkbox"> 📏 Verified image quality/resolution</li>
<li><input disabled="" type="checkbox"> 🔄 Added loading states for scanning</li>
</ul>
<hr>
<h2 id="real-world-impact">Real-World Impact</h2>
<h3 id="before-ml-kit"><strong>Before ML Kit:</strong></h3>
<ul>
<li>⏱️ Users spent 2-3 minutes per document (capture, crop, adjust)</li>
<li>😤 30-40% required retakes due to poor quality</li>
<li>📉 High abandonment rates on document upload flows</li>
<li>🐛 Constant bug reports about scanning issues</li>
</ul>
<h3 id="after-ml-kit"><strong>After ML Kit:</strong></h3>
<ul>
<li>⚡ 20-30 seconds per document (all automatic)</li>
<li>✨ &lt;5% retake rate (AI handles most issues)</li>
<li>📈 50-70% improvement in completion rates</li>
<li>😊 Positive feedback about scanning experience</li>
</ul>
<hr>
<h2 id="-related-resources">🔗 Related Resources</h2>
<ul>
<li><a href="https://developers.google.com/ml-kit/vision/doc-scanner">ML Kit Document Scanner Documentation</a></li>
<li><a href="https://developers.google.com/ml-kit/vision/text-recognition">ML Kit Text Recognition</a></li>
<li><a href="https://developers.google.com/android/guides/setup">Google Play Services Setup</a></li>
</ul>
<hr>
<h2 id="-final-thoughts">💡 Final Thoughts</h2>
<p>Stop wasting time building custom document capture solutions. <strong>ML Kit Document Scanner gives you professional, AI-powered scanning with minimal code.</strong></p>
<p><strong>The math is simple:</strong></p>
<ul>
<li>🕐 Custom implementation: 2-3 weeks + ongoing maintenance</li>
<li>⚡ ML Kit integration: 2-3 hours + zero maintenance</li>
<li>🎯 Result quality: ML Kit wins every time</li>
</ul>
<p><strong>Key takeaways:</strong></p>
<ol>
<li><strong>Stop using basic camera capture</strong> for documents</li>
<li><strong>ML Kit is tiny</strong> (~3MB) for massive functionality</li>
<li><strong>10 lines of code</strong> beats 500+ lines</li>
<li><strong>Professional results</strong> without CV expertise</li>
<li><strong>Users notice the difference</strong> - completion rates improve significantly</li>
</ol>
<p>Your users deserve better than blurry, crooked photos. Give them professional document scanning with ML Kit.</p>
<hr>
<p><strong>That&rsquo;s it!</strong> You now have the knowledge to implement professional document scanning in your Android app. 🎉</p>
<p>Feel free to reach out via my social handles with questions or to share your implementation! 😊</p>
<p><strong>Happy scanning!</strong> 📄✨</p>
]]></content:encoded></item><item><title>Supercharging Claude Code with Serena - Save 70% on Tokens</title><link>https://md.eknath.dev/posts/ai-ml/serena-claude-code-setup/</link><pubDate>Wed, 04 Feb 2026 10:00:00 +0530</pubDate><guid>https://md.eknath.dev/posts/ai-ml/serena-claude-code-setup/</guid><description>&lt;h2 id="tldr---quick-setup">TL;DR - Quick Setup&lt;/h2>
&lt;p>Already know what Serena is? Here&amp;rsquo;s the fast track:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 1. Install uv and Serena&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>curl -LsSf https://astral.sh/uv/install.sh | sh
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>source ~/.zshrc
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>uv tool install git+https://github.com/oraios/serena
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 2. Restart terminal, then connect to your project&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd /path/to/your/project
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>claude mcp add serena -- serena-mcp-server --project &lt;span style="color:#66d9ef">$(&lt;/span>pwd&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;.serena/&amp;#34;&lt;/span> &amp;gt;&amp;gt; .gitignore
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># 3. Start Claude&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>claude --allowedTools &lt;span style="color:#e6db74">&amp;#34;mcp__serena*&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Then tell Claude: &lt;em>&amp;ldquo;Use Serena to onboard this project.&amp;rdquo;&lt;/em>&lt;/p>
&lt;p>Want to understand what this does and why? Read on.&lt;/p></description><content:encoded><![CDATA[<h2 id="tldr---quick-setup">TL;DR - Quick Setup</h2>
<p>Already know what Serena is? Here&rsquo;s the fast track:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># 1. Install uv and Serena</span>
</span></span><span style="display:flex;"><span>curl -LsSf https://astral.sh/uv/install.sh | sh
</span></span><span style="display:flex;"><span>source ~/.zshrc
</span></span><span style="display:flex;"><span>uv tool install git+https://github.com/oraios/serena
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 2. Restart terminal, then connect to your project</span>
</span></span><span style="display:flex;"><span>cd /path/to/your/project
</span></span><span style="display:flex;"><span>claude mcp add serena -- serena-mcp-server --project <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.serena/&#34;</span> &gt;&gt; .gitignore
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># 3. Start Claude</span>
</span></span><span style="display:flex;"><span>claude --allowedTools <span style="color:#e6db74">&#34;mcp__serena*&#34;</span>
</span></span></code></pre></div><p>Then tell Claude: <em>&ldquo;Use Serena to onboard this project.&rdquo;</em></p>
<p>Want to understand what this does and why? Read on.</p>
<hr>
<h2 id="a-quick-note">A Quick Note</h2>
<p>This is a continuation of my <a href="https://md.eknath.dev/posts/ai-ml/claude-code-notes/">Claude Code notes</a>. If you haven&rsquo;t read that yet, I recommend starting there for the fundamentals. This article focuses on a specific optimization that has dramatically improved my Claude Code experience.</p>
<p><strong>What you&rsquo;ll learn:</strong></p>
<ul>
<li>Why Claude Code can get expensive on large codebases</li>
<li>How Serena uses LSP to provide semantic code navigation</li>
<li>Step-by-step setup (takes ~5 minutes)</li>
<li>Practical usage patterns and prompts</li>
<li>When to use Serena vs. vanilla Claude Code</li>
</ul>
<hr>
<h2 id="the-problem-token-costs-add-up-fast">The Problem: Token Costs Add Up Fast</h2>
<p>If you&rsquo;ve been using Claude Code for a while, you&rsquo;ve probably noticed that <strong>token costs can spiral quickly</strong> - especially on large codebases. Here&rsquo;s why:</p>
<p>When you ask Claude something like <em>&ldquo;Where is the authentication logic?&rdquo;</em>, it often:</p>
<ol>
<li>Reads entire files to understand context</li>
<li>Scans through multiple modules looking for patterns</li>
<li>Sometimes re-reads files it already looked at</li>
</ol>
<p>For a medium-sized project (50k+ lines of code), a single exploration session can consume <strong>thousands of tokens</strong> just reading files. Multiply that across a day&rsquo;s work, and you&rsquo;re looking at serious costs.</p>
<p>Worse, when the context window fills up, Claude can start <strong>hallucinating</strong> - referencing functions that don&rsquo;t exist or suggesting patterns that don&rsquo;t match your codebase.</p>
<hr>
<h2 id="enter-serena-semantic-code-navigation">Enter Serena: Semantic Code Navigation</h2>
<p><strong>Serena</strong> is an open-source MCP (Model Context Protocol) server that gives Claude Code <strong>semantic understanding</strong> of your codebase. Instead of reading entire files, Claude can now:</p>
<ul>
<li><strong>Jump directly to function definitions</strong></li>
<li><strong>Find all usages of a class or method</strong></li>
<li><strong>Navigate imports and dependencies</strong></li>
<li><strong>Understand type hierarchies</strong></li>
</ul>
<p>It does this by leveraging <strong>LSP (Language Server Protocol)</strong> - the same technology that powers your IDE&rsquo;s &ldquo;Go to Definition&rdquo; and &ldquo;Find All References&rdquo; features.</p>
<h3 id="the-results">The Results?</h3>
<p>In my testing on a ~100k line Android/KMP project:</p>
<table>
  <thead>
      <tr>
          <th>Metric</th>
          <th>Without Serena</th>
          <th>With Serena</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tokens for &ldquo;Find auth logic&rdquo;</td>
          <td>~15,000</td>
          <td>~4,500</td>
      </tr>
      <tr>
          <td>Context preservation</td>
          <td>Poor</td>
          <td>Excellent</td>
      </tr>
      <tr>
          <td>Navigation accuracy</td>
          <td>File-based guessing</td>
          <td>Semantic precision</td>
      </tr>
      <tr>
          <td>Estimated cost savings</td>
          <td>Baseline</td>
          <td><strong>~70%</strong></td>
      </tr>
  </tbody>
</table>
<p>That 70% isn&rsquo;t marketing fluff - it&rsquo;s real savings from not reading entire files when you only need specific symbols.</p>
<hr>
<h2 id="how-serena-works-under-the-hood">How Serena Works Under the Hood</h2>
<p>MCP servers extend Claude&rsquo;s capabilities through a standardized protocol (see <a href="#mcp-model-context-protocol">Glossary</a> if this is new to you). Serena specifically provides:</p>
<ol>
<li>
<p><strong>Code Indexing</strong>: When you connect Serena to Claude, it automatically builds an index of your codebase&rsquo;s symbols, types, and relationships.</p>
</li>
<li>
<p><strong>LSP Integration</strong>: Serena wraps language servers (for Kotlin, TypeScript, Python, etc.) to provide semantic navigation.</p>
</li>
<li>
<p><strong>Smart Querying</strong>: When Claude asks &ldquo;Where is <code>UserRepository</code>?&rdquo;, Serena returns the exact file and line number - not a file dump.</p>
</li>
<li>
<p><strong>Live Updates</strong>: The index updates as you modify code, staying in sync with your project.</p>
</li>
</ol>
<pre tabindex="0"><code>┌─────────────────┐      MCP Protocol      ┌─────────────────┐
│   Claude Code   │ ◄──────────────────────► │     Serena      │
│    (Client)     │                          │   (MCP Server)  │
└─────────────────┘                          └────────┬────────┘
                                                      │
                                                      │ LSP
                                                      ▼
                                             ┌─────────────────┐
                                             │  Language Server │
                                             │  (kotlin-ls,    │
                                             │   tsserver, etc) │
                                             └─────────────────┘
</code></pre><hr>
<h2 id="one-time-setup">One-Time Setup</h2>
<h3 id="prerequisites">Prerequisites</h3>
<ul>
<li>Claude Code CLI installed (<a href="https://md.eknath.dev/posts/ai-ml/claude-code-notes/">see my setup guide</a>)</li>
<li>A supported project (Kotlin, TypeScript, Python, Go, Rust, and more)</li>
</ul>
<h3 id="step-1-install-uv-package-manager">Step 1: Install <code>uv</code> Package Manager</h3>
<p>Serena uses <code>uv</code> for installation. If you don&rsquo;t have it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># macOS/Linux</span>
</span></span><span style="display:flex;"><span>curl -LsSf https://astral.sh/uv/install.sh | sh
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Reload your shell</span>
</span></span><span style="display:flex;"><span>source ~/.zshrc  <span style="color:#75715e"># or ~/.bashrc</span>
</span></span></code></pre></div><p>Why <code>uv</code>? It&rsquo;s a fast Python package manager that handles Serena&rsquo;s dependencies cleanly.</p>
<h3 id="step-2-install-serena-tools">Step 2: Install Serena Tools</h3>
<p>Install the CLI tools globally:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>uv tool install git+https://github.com/oraios/serena
</span></span></code></pre></div><p>This gives you two commands:</p>
<ul>
<li><code>serena</code> - Project management CLI</li>
<li><code>serena-mcp-server</code> - The MCP server that Claude connects to</li>
</ul>
<h3 id="step-3-restart-your-terminal">Step 3: Restart Your Terminal</h3>
<blockquote>
<p><strong>Important</strong>: After installing <code>uv</code> and Serena, <strong>close all terminal windows and open a fresh one</strong>. This ensures your shell recognizes the new commands.</p>
</blockquote>
<p>I spent time debugging &ldquo;command not found&rdquo; errors only to realize a terminal restart fixed everything. Save yourself the frustration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Close all terminals, then open a new one and verify</span>
</span></span><span style="display:flex;"><span>which serena
</span></span><span style="display:flex;"><span>which serena-mcp-server
</span></span></code></pre></div><p>Both should return valid paths. If not, try running <code>source ~/.zshrc</code> (or <code>~/.bashrc</code>).</p>
<h3 id="step-4-connect-serena-to-claude-code">Step 4: Connect Serena to Claude Code</h3>
<p>Navigate to your project root and register Serena as an MCP server:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd /path/to/your/project
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Add the server (this also initializes the project index automatically)</span>
</span></span><span style="display:flex;"><span>claude mcp add serena -- serena-mcp-server --project <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>
</span></span></code></pre></div><p><strong>What this does:</strong></p>
<ul>
<li>Registers Serena as an MCP server for Claude</li>
<li>Auto-initializes the project index (creates <code>.serena/</code> folder)</li>
<li>Scans your codebase for symbols, types, and relationships</li>
</ul>
<p><strong>Initial indexing time</strong> depends on project size:</p>
<table>
  <thead>
      <tr>
          <th>Project Size</th>
          <th>Approximate Time</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Small (&lt; 10k lines)</td>
          <td>10-30 seconds</td>
      </tr>
      <tr>
          <td>Medium (10k-50k lines)</td>
          <td>1-3 minutes</td>
      </tr>
      <tr>
          <td>Large (50k-200k lines)</td>
          <td>3-10 minutes</td>
      </tr>
      <tr>
          <td>Very Large (200k+ lines)</td>
          <td>10-20 minutes</td>
      </tr>
  </tbody>
</table>
<p>You can monitor progress at <code>http://localhost:24282</code> during indexing.</p>
<blockquote>
<p><strong>Important</strong>: Add <code>.serena/</code> to your <code>.gitignore</code>. This folder is local cache and shouldn&rsquo;t be committed.</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;.serena/&#34;</span> &gt;&gt; .gitignore
</span></span></code></pre></div><p>Verify it&rsquo;s connected:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Check MCP server status</span>
</span></span><span style="display:flex;"><span>claude mcp list
</span></span></code></pre></div><p>You should see <code>serena</code> in the list with a green status.</p>
<hr>
<h2 id="handling-permission-prompts">Handling Permission Prompts</h2>
<p>When you start using Serena with Claude Code, you&rsquo;ll encounter <strong>multiple permission prompts</strong>. Claude asks for approval each time Serena wants to:</p>
<ul>
<li>Read files</li>
<li>Navigate to definitions</li>
<li>Search for symbols</li>
<li>Access the index</li>
</ul>
<p>This is good for security, but can get tedious during intensive coding sessions.</p>
<h3 id="option-1-approve-permissions-individually-recommended-for-learning">Option 1: Approve Permissions Individually (Recommended for Learning)</h3>
<p>When starting out, approve each permission manually. This helps you understand what Serena is doing and builds trust in the tool.</p>
<h3 id="option-2-auto-accept-permissions-for-serena">Option 2: Auto-Accept Permissions for Serena</h3>
<p>If you trust Serena and want a smoother experience, you can configure Claude to auto-accept its tool calls:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start Claude with auto-accept for the current session</span>
</span></span><span style="display:flex;"><span>claude --allowedTools <span style="color:#e6db74">&#34;mcp__serena*&#34;</span>
</span></span></code></pre></div><p>This allows all Serena MCP tools without prompting, while still prompting for other potentially dangerous operations.</p>
<h3 id="option-3-dangerously-skip-all-permissions-use-with-caution">Option 3: Dangerously Skip All Permissions (Use with Caution)</h3>
<p>For experienced users who understand the risks:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Skip ALL permission prompts (not just Serena)</span>
</span></span><span style="display:flex;"><span>claude --dangerously-skip-permissions
</span></span></code></pre></div><blockquote>
<p><strong>Warning</strong>: This flag bypasses ALL safety prompts - file writes, shell commands, everything. Only use this if:</p>
<ul>
<li>You&rsquo;re on a development machine (not production)</li>
<li>You understand Claude can modify/delete files without asking</li>
<li>You&rsquo;re working in a git-tracked project (easy to revert mistakes)</li>
<li>You trust your judgment to review changes before committing</li>
</ul>
</blockquote>
<p>For most users, Option 2 (<code>--allowedTools &quot;mcp__serena*&quot;</code>) is the sweet spot - smooth Serena experience while keeping other safeguards in place.</p>
<hr>
<h2 id="using-serena-with-claude-code">Using Serena with Claude Code</h2>
<h3 id="starting-a-session">Starting a Session</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Navigate to your project</span>
</span></span><span style="display:flex;"><span>cd /path/to/your/project
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Start Claude with Serena connected</span>
</span></span><span style="display:flex;"><span>claude
</span></span></code></pre></div><h3 id="onboarding-claude-to-your-project">Onboarding Claude to Your Project</h3>
<p>The first time you use Serena on a project, run this prompt:</p>
<pre tabindex="0"><code>Use Serena to onboard this project. Understand the architecture,
main modules, and key entry points.
</code></pre><p>Claude will use Serena&rsquo;s semantic capabilities to build a mental model of your codebase - without reading every file.</p>
<h3 id="practical-examples">Practical Examples</h3>
<p><strong>Finding specific implementations:</strong></p>
<pre tabindex="0"><code>Where is the UserRepository interface implemented?
</code></pre><p>Without Serena: Claude reads multiple files guessing where implementations might be.
With Serena: Claude jumps directly to the concrete class.</p>
<p><strong>Understanding call hierarchies:</strong></p>
<pre tabindex="0"><code>What functions call the `syncUserData()` method?
</code></pre><p>Serena traces all callers semantically, giving Claude precise context.</p>
<p><strong>Navigating multi-module projects:</strong></p>
<pre tabindex="0"><code>How does the :feature:auth module communicate with :core:network?
</code></pre><p>Serena understands module boundaries and can trace cross-module dependencies.</p>
<hr>
<h2 id="monitoring-and-debugging">Monitoring and Debugging</h2>
<h3 id="live-dashboard">Live Dashboard</h3>
<p>Serena provides a local web dashboard for monitoring and debugging:</p>
<pre tabindex="0"><code>http://localhost:24282
</code></pre><p>Open this URL in your browser while Serena is running. You&rsquo;ll see:</p>
<p><strong>Index Status Panel</strong></p>
<ul>
<li>Total symbols indexed (classes, functions, variables)</li>
<li>Indexing progress percentage</li>
<li>Last index update timestamp</li>
<li>Any indexing errors or warnings</li>
</ul>
<p><strong>Query Logs</strong></p>
<ul>
<li>Real-time log of Claude&rsquo;s queries to Serena</li>
<li>Which symbols were requested</li>
<li>Response times for each query</li>
<li>Helps you understand what Claude is &ldquo;thinking&rdquo;</li>
</ul>
<p><strong>Language Server Status</strong></p>
<ul>
<li>Connected language servers (kotlin-ls, tsserver, etc.)</li>
<li>Server health and memory usage</li>
<li>Restart buttons if a server becomes unresponsive</li>
</ul>
<p><strong>Cache Statistics</strong></p>
<ul>
<li>Hit/miss ratios</li>
<li>Memory usage</li>
<li>Option to clear cache if things get stale</li>
</ul>
<blockquote>
<p><strong>Tip</strong>: Keep the dashboard open in a browser tab during intensive coding sessions. If Claude seems confused or slow, check the dashboard - you might spot a language server that crashed or an indexing error.</p>
</blockquote>
<h3 id="serena-tools-available-to-claude">Serena Tools Available to Claude</h3>
<p>When connected, Claude gains access to these MCP tools (you can see these with <code>claude mcp list</code>):</p>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>serena_get_definition</code></td>
          <td>Jump to where a symbol is defined</td>
      </tr>
      <tr>
          <td><code>serena_get_references</code></td>
          <td>Find all usages of a symbol</td>
      </tr>
      <tr>
          <td><code>serena_get_symbols</code></td>
          <td>List all symbols in a file</td>
      </tr>
      <tr>
          <td><code>serena_search_symbols</code></td>
          <td>Search for symbols by name pattern</td>
      </tr>
      <tr>
          <td><code>serena_get_hover</code></td>
          <td>Get type info and documentation</td>
      </tr>
      <tr>
          <td><code>serena_get_diagnostics</code></td>
          <td>Get compiler errors/warnings</td>
      </tr>
  </tbody>
</table>
<p>Claude automatically chooses the right tool based on your question.</p>
<h3 id="troubleshooting-common-issues">Troubleshooting Common Issues</h3>
<p><strong>&ldquo;command not found: serena&rdquo; after installation:</strong></p>
<p>This is the most common issue. Your terminal doesn&rsquo;t know about the new commands yet.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Option 1: Reload shell config</span>
</span></span><span style="display:flex;"><span>source ~/.zshrc  <span style="color:#75715e"># or ~/.bashrc</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Option 2 (recommended): Close ALL terminal windows and open a fresh one</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This ensures a clean shell environment</span>
</span></span></code></pre></div><p><strong>&ldquo;1 MCP server failed&rdquo; error:</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Remove and re-add the server</span>
</span></span><span style="display:flex;"><span>claude mcp remove serena
</span></span><span style="display:flex;"><span>claude mcp add serena -- serena-mcp-server --project <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>
</span></span></code></pre></div><p><strong>Index out of sync:</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Rebuild the index</span>
</span></span><span style="display:flex;"><span>serena project update --name &lt;your-project-name&gt;
</span></span></code></pre></div><p><strong>Server not starting:</strong></p>
<p>Make sure you&rsquo;re in the correct project root where you ran the <code>claude mcp add</code> command.</p>
<p><strong>Language not supported:</strong></p>
<p>Check <a href="https://github.com/oraios/serena#supported-languages">Serena&rsquo;s supported languages</a>. For Android/KMP projects, Kotlin support works out of the box.</p>
<hr>
<h2 id="tip-simplify-with-aliases">Tip: Simplify with Aliases</h2>
<p>These Serena commands are verbose. If you find yourself typing them often, consider setting up shell aliases or using a tool like <a href="https://github.com/Eganathan/aliasly">Aliasly</a> to manage shortcuts across projects.</p>
<hr>
<h2 id="best-practices">Best Practices</h2>
<h3 id="1-keep-your-index-updated">1. Keep Your Index Updated</h3>
<p>After major refactors or pulling new changes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>serena project update --name &lt;your-project-name&gt;
</span></span></code></pre></div><h3 id="2-use-specific-queries">2. Use Specific Queries</h3>
<p>Instead of:</p>
<pre tabindex="0"><code>How does authentication work?
</code></pre><p>Try:</p>
<pre tabindex="0"><code>Show me the AuthViewModel and its dependencies using Serena.
</code></pre><p>The more specific your query, the better Serena can target the exact symbols.</p>
<h3 id="3-combine-with-claudemd">3. Combine with CLAUDE.md</h3>
<p>Your <code>CLAUDE.md</code> file complements Serena perfectly. Use CLAUDE.md for:</p>
<ul>
<li>Project conventions and coding standards</li>
<li>Build commands and configuration</li>
<li>Architecture overview</li>
</ul>
<p>Use Serena for:</p>
<ul>
<li>Navigating actual code</li>
<li>Finding implementations</li>
<li>Tracing dependencies</li>
</ul>
<h3 id="4-monitor-your-savings">4. Monitor Your Savings</h3>
<p>Use <code>/cost</code> in Claude Code to track your token usage. Compare sessions before and after Serena to see the actual savings.</p>
<hr>
<h2 id="comparison-claude-code-vs-claude-code--serena">Comparison: Claude Code vs. Claude Code + Serena</h2>
<table>
  <thead>
      <tr>
          <th>Feature</th>
          <th>Without Serena</th>
          <th>With Serena</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Search Method</strong></td>
          <td>Text-based / Full file reads</td>
          <td>Symbolic / LSP-powered</td>
      </tr>
      <tr>
          <td><strong>Code Retrieval</strong></td>
          <td>Reads entire files</td>
          <td>Extracts specific symbols/blocks</td>
      </tr>
      <tr>
          <td><strong>Token Usage</strong></td>
          <td>High (linear to file size)</td>
          <td>Low (targeted retrieval)</td>
      </tr>
      <tr>
          <td><strong>Memory</strong></td>
          <td>Session-based only</td>
          <td>Persistent project indexing</td>
      </tr>
      <tr>
          <td><strong>Navigation</strong></td>
          <td>File path guessing</td>
          <td>Precise &ldquo;Go to Definition&rdquo;</td>
      </tr>
      <tr>
          <td><strong>Cross-references</strong></td>
          <td>Manual grep patterns</td>
          <td>Semantic &ldquo;Find All References&rdquo;</td>
      </tr>
      <tr>
          <td><strong>Type Understanding</strong></td>
          <td>Inferred from context</td>
          <td>Actual type hierarchy from LSP</td>
      </tr>
      <tr>
          <td><strong>Multi-module Support</strong></td>
          <td>Reads each module separately</td>
          <td>Understands module relationships</td>
      </tr>
      <tr>
          <td><strong>Context Preservation</strong></td>
          <td>Fills up quickly</td>
          <td>Stays efficient longer</td>
      </tr>
      <tr>
          <td><strong>Setup Required</strong></td>
          <td>None</td>
          <td>One-time (~5 min)</td>
      </tr>
      <tr>
          <td><strong>Token Efficiency</strong></td>
          <td>Baseline</td>
          <td><strong>~70% reduction</strong></td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="supported-languages">Supported Languages</h2>
<p>Serena works with any language that has LSP support. Here&rsquo;s the current status:</p>
<table>
  <thead>
      <tr>
          <th>Language</th>
          <th>Support Level</th>
          <th>Language Server</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Kotlin</strong></td>
          <td>Excellent</td>
          <td>kotlin-language-server</td>
      </tr>
      <tr>
          <td><strong>TypeScript/JavaScript</strong></td>
          <td>Excellent</td>
          <td>tsserver</td>
      </tr>
      <tr>
          <td><strong>Python</strong></td>
          <td>Excellent</td>
          <td>pylsp / pyright</td>
      </tr>
      <tr>
          <td><strong>Go</strong></td>
          <td>Excellent</td>
          <td>gopls</td>
      </tr>
      <tr>
          <td><strong>Rust</strong></td>
          <td>Excellent</td>
          <td>rust-analyzer</td>
      </tr>
      <tr>
          <td><strong>Java</strong></td>
          <td>Good</td>
          <td>eclipse.jdt.ls</td>
      </tr>
      <tr>
          <td><strong>C/C++</strong></td>
          <td>Good</td>
          <td>clangd</td>
      </tr>
      <tr>
          <td><strong>Swift</strong></td>
          <td>Experimental</td>
          <td>sourcekit-lsp</td>
      </tr>
  </tbody>
</table>
<p>For <strong>Android/KMP projects</strong>, Kotlin support is what matters most - and it works great.</p>
<blockquote>
<p><strong>Note</strong>: Serena auto-detects your project&rsquo;s languages and starts the appropriate language servers. You don&rsquo;t need to configure this manually.</p>
</blockquote>
<hr>
<h2 id="updating-and-managing-serena">Updating and Managing Serena</h2>
<h3 id="update-serena-to-latest-version">Update Serena to Latest Version</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>uv tool upgrade serena
</span></span></code></pre></div><h3 id="rebuild-project-index">Rebuild Project Index</h3>
<p>After major refactors, dependency updates, or pulling large changes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>serena project update --name &lt;your-project-name&gt;
</span></span></code></pre></div><h3 id="remove-serena-from-a-project">Remove Serena from a Project</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Remove MCP server from Claude</span>
</span></span><span style="display:flex;"><span>claude mcp remove serena
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Delete the local index (optional)</span>
</span></span><span style="display:flex;"><span>rm -rf .serena/
</span></span></code></pre></div><h3 id="working-with-multiple-projects">Working with Multiple Projects</h3>
<p>Serena indexes are project-specific. To switch between projects:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Project A</span>
</span></span><span style="display:flex;"><span>cd /path/to/project-a
</span></span><span style="display:flex;"><span>claude mcp add serena -- serena-mcp-server --project <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Project B (in a different terminal/session)</span>
</span></span><span style="display:flex;"><span>cd /path/to/project-b
</span></span><span style="display:flex;"><span>claude mcp add serena -- serena-mcp-server --project <span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span>
</span></span></code></pre></div><p>Each project maintains its own <code>.serena/</code> index.</p>
<hr>
<h2 id="when-not-to-use-serena">When NOT to Use Serena</h2>
<p>Serena isn&rsquo;t always the best choice:</p>
<ul>
<li><strong>Small scripts or single-file projects</strong>: The overhead of indexing doesn&rsquo;t pay off</li>
<li><strong>Heavily dynamic languages</strong>: LSP works best with typed languages</li>
<li><strong>Quick one-off questions</strong>: Sometimes just asking Claude directly is faster</li>
<li><strong>Non-code tasks</strong>: Documentation, git operations, etc. don&rsquo;t benefit from Serena</li>
</ul>
<p>Use judgment - Serena is a tool for <strong>navigating complex codebases</strong>, not a universal solution.</p>
<hr>
<h2 id="integration-with-my-workflow">Integration with My Workflow</h2>
<p>Here&rsquo;s how Serena fits into my daily Claude Code usage:</p>
<ol>
<li>
<p><strong>Morning context building</strong>: &ldquo;Use Serena to show me what I worked on yesterday in the :feature:dashboard module&rdquo;</p>
</li>
<li>
<p><strong>Feature development</strong>: &ldquo;Using Serena, find all places where we handle network errors and show me the patterns&rdquo;</p>
</li>
<li>
<p><strong>Code review</strong>: &ldquo;Navigate to the UserService implementation and review it for potential issues&rdquo;</p>
</li>
<li>
<p><strong>Debugging</strong>: &ldquo;Trace all callers of <code>processPayment()</code> and identify where the null check might be failing&rdquo;</p>
</li>
<li>
<p><strong>Onboarding teammates</strong>: &ldquo;Use Serena to explain the data flow from API response to UI state&rdquo;</p>
</li>
</ol>
<hr>
<h2 id="cost-analysis">Cost Analysis</h2>
<p>Let&rsquo;s break down the real savings. With Claude Sonnet at ~$3/1M input tokens:</p>
<table>
  <thead>
      <tr>
          <th>Session Type</th>
          <th>Without Serena</th>
          <th>With Serena</th>
          <th>Savings</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quick exploration</td>
          <td>10k tokens</td>
          <td>3k tokens</td>
          <td>$0.02</td>
      </tr>
      <tr>
          <td>Feature implementation</td>
          <td>50k tokens</td>
          <td>15k tokens</td>
          <td>$0.10</td>
      </tr>
      <tr>
          <td>Full-day coding</td>
          <td>200k tokens</td>
          <td>60k tokens</td>
          <td>$0.42</td>
      </tr>
      <tr>
          <td>Monthly usage (20 days)</td>
          <td>4M tokens</td>
          <td>1.2M tokens</td>
          <td><strong>$8.40</strong></td>
      </tr>
  </tbody>
</table>
<p>These are conservative estimates. For larger codebases or Opus usage, savings multiply significantly.</p>
<hr>
<h2 id="setup-checklist">Setup Checklist</h2>
<p>Quick reference for new projects:</p>
<ul>
<li><input disabled="" type="checkbox"> Install <code>uv</code>: <code>curl -LsSf https://astral.sh/uv/install.sh | sh</code></li>
<li><input disabled="" type="checkbox"> Install Serena: <code>uv tool install git+https://github.com/oraios/serena</code></li>
<li><input disabled="" type="checkbox"> <strong>Restart terminal</strong> (close all windows, open fresh)</li>
<li><input disabled="" type="checkbox"> Verify install: <code>which serena &amp;&amp; which serena-mcp-server</code></li>
<li><input disabled="" type="checkbox"> Connect to Claude (auto-creates index): <code>claude mcp add serena -- serena-mcp-server --project $(pwd)</code></li>
<li><input disabled="" type="checkbox"> Add to .gitignore: <code>echo &quot;.serena/&quot; &gt;&gt; .gitignore</code></li>
<li><input disabled="" type="checkbox"> Test connection: <code>claude mcp list</code></li>
<li><input disabled="" type="checkbox"> Start Claude: <code>claude</code> (or <code>claude --allowedTools &quot;mcp__serena*&quot;</code> to auto-accept Serena permissions)</li>
<li><input disabled="" type="checkbox"> Onboard Claude: &ldquo;Use Serena to onboard this project&rdquo;</li>
</ul>
<hr>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/oraios/serena">Serena GitHub Repository</a></li>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol Documentation</a></li>
<li><a href="https://md.eknath.dev/posts/ai-ml/claude-code-notes/">My Claude Code Notes</a></li>
<li><a href="https://microsoft.github.io/language-server-protocol/">LSP Specification</a></li>
</ul>
<hr>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Serena has become an essential part of my Claude Code setup. The token savings are nice, but the real value is <strong>better context preservation</strong>. Claude makes fewer mistakes when it has precise semantic information instead of guessing from partial file reads.</p>
<p>If you&rsquo;re working on any non-trivial codebase - especially multi-module Android/KMP projects - give Serena a try. The 10-minute setup pays for itself within the first session.</p>
<p>As always, remember: <strong>AI is a copilot, not a pilot</strong>. Serena makes the copilot more efficient, but you&rsquo;re still in control.</p>
<hr>
<p><em>This article is a living document. Last updated: February 2026</em></p>
<hr>
<h2 id="glossary">Glossary</h2>
<p>New to some of these terms? Here&rsquo;s a quick reference:</p>
<h3 id="mcp-model-context-protocol">MCP (Model Context Protocol)</h3>
<p><strong>Model Context Protocol</strong> is an open standard created by Anthropic that allows AI assistants (like Claude) to connect to external tools and data sources. Think of it as a &ldquo;USB port&rdquo; for AI - any tool that implements MCP can plug into Claude and extend its capabilities.</p>
<p><strong>Example</strong>: Serena is an MCP server. When you run <code>claude mcp add serena</code>, you&rsquo;re telling Claude &ldquo;hey, there&rsquo;s a new tool you can use.&rdquo;</p>
<p><a href="https://modelcontextprotocol.io/">Learn more</a></p>
<hr>
<h3 id="lsp-language-server-protocol">LSP (Language Server Protocol)</h3>
<p><strong>Language Server Protocol</strong> is a standard created by Microsoft that powers IDE features like:</p>
<ul>
<li>&ldquo;Go to Definition&rdquo; (Ctrl/Cmd + Click)</li>
<li>&ldquo;Find All References&rdquo;</li>
<li>Auto-completion</li>
<li>Syntax errors and warnings</li>
</ul>
<p>Instead of each IDE implementing these features separately for every language, LSP provides a common interface. Your IDE talks to a &ldquo;language server&rdquo; that understands the specific language.</p>
<p><strong>Example</strong>: When you Ctrl+Click a function in VS Code and it jumps to the definition - that&rsquo;s LSP in action. Serena uses this same technology to give Claude semantic code navigation.</p>
<p><a href="https://microsoft.github.io/language-server-protocol/">Learn more</a></p>
<hr>
<h3 id="uv">uv</h3>
<p><strong>uv</strong> is a fast Python package and project manager created by <a href="https://astral.sh/">Astral</a>. It&rsquo;s like npm for Python, but significantly faster (written in Rust).</p>
<p><strong>Why Serena uses it</strong>: Serena is a Python project. <code>uv tool install</code> installs CLI tools globally, similar to <code>npm install -g</code>.</p>
<p><strong>Key commands</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install a tool globally</span>
</span></span><span style="display:flex;"><span>uv tool install package-name
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Install from git repository</span>
</span></span><span style="display:flex;"><span>uv tool install git+https://github.com/user/repo
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Update a tool</span>
</span></span><span style="display:flex;"><span>uv tool upgrade package-name
</span></span></code></pre></div><p><a href="https://docs.astral.sh/uv/">Learn more</a></p>
<hr>
<h3 id="tokens">Tokens</h3>
<p><strong>Tokens</strong> are the fundamental units that AI models process. Roughly:</p>
<ul>
<li>1 token ≈ 4 characters in English</li>
<li>1 token ≈ 0.75 words</li>
<li>100 tokens ≈ 75 words</li>
</ul>
<p>When you send a prompt to Claude, it counts tokens. When Claude responds, it generates tokens. Both cost money with the API.</p>
<p><strong>Why this matters</strong>: If Claude reads a 1000-line file to answer a simple question, that&rsquo;s a lot of tokens wasted. Serena helps Claude read only what it needs.</p>
<hr>
<h3 id="context-window">Context Window</h3>
<p>The <strong>context window</strong> is the maximum amount of text (in tokens) that an AI model can &ldquo;remember&rdquo; during a conversation. Think of it as the AI&rsquo;s working memory.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Context Window</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Sonnet</td>
          <td>200k tokens</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.5</td>
          <td>500k tokens</td>
      </tr>
      <tr>
          <td>GPT-4</td>
          <td>128k tokens</td>
      </tr>
  </tbody>
</table>
<p><strong>Why this matters</strong>: When the context window fills up, older information gets &ldquo;forgotten&rdquo; or summarized. With large codebases, this can cause Claude to lose track of important details. Serena&rsquo;s efficient queries help preserve context.</p>
<hr>
<h3 id="semantic-vs-syntactic">Semantic vs. Syntactic</h3>
<ul>
<li><strong>Syntactic</strong>: Understanding code as text/patterns (like grep searching for &ldquo;function&rdquo;)</li>
<li><strong>Semantic</strong>: Understanding code&rsquo;s meaning and relationships (knowing that <code>UserRepository implements Repository&lt;User&gt;</code>)</li>
</ul>
<p>Serena provides <strong>semantic</strong> navigation - it understands your code&rsquo;s structure, not just the text.</p>
<hr>
<h3 id="index--indexing">Index / Indexing</h3>
<p>When Serena &ldquo;indexes&rdquo; your project, it&rsquo;s building a searchable database of your code&rsquo;s structure:</p>
<ul>
<li>All classes, functions, and variables</li>
<li>Their locations (file + line number)</li>
<li>Their relationships (what calls what, what implements what)</li>
</ul>
<p>This is similar to how search engines index websites - they pre-process content so searches are fast.</p>
<hr>
<h3 id="cli-command-line-interface">CLI (Command Line Interface)</h3>
<p>A <strong>CLI</strong> is a text-based interface for interacting with software. Instead of clicking buttons in a GUI, you type commands.</p>
<p><strong>Examples</strong>:</p>
<ul>
<li><code>git</code> - Version control CLI</li>
<li><code>npm</code> - Node.js package manager CLI</li>
<li><code>claude</code> - Claude Code&rsquo;s CLI</li>
</ul>
<hr>
<h3 id="mcp-server-vs-client">MCP Server vs. Client</h3>
<p>In the MCP architecture:</p>
<ul>
<li><strong>Client</strong>: The AI assistant (Claude Code) that uses tools</li>
<li><strong>Server</strong>: The tool that provides capabilities (Serena, file system access, etc.)</li>
</ul>
<p>When you run <code>claude mcp add serena</code>, you&rsquo;re registering Serena as a server that Claude (the client) can connect to.</p>
<hr>
<p>Questions or feedback? Reach out:</p>
<ul>
<li><a href="mailto:mail@eknath.dev">Email</a></li>
<li><a href="https://eknath.dev">Website</a></li>
</ul>
]]></content:encoded></item><item><title>Staying Relevant with Claude Code - A Self-Note for Android &amp; KMP Developers</title><link>https://md.eknath.dev/posts/ai-ml/claude-code-notes/</link><pubDate>Fri, 09 Jan 2026 21:47:13 +0530</pubDate><guid>https://md.eknath.dev/posts/ai-ml/claude-code-notes/</guid><description>&lt;h2 id="a-quick-disclaimer">A Quick Disclaimer&lt;/h2>
&lt;p>This article is primarily a &lt;strong>self-note&lt;/strong> that I keep updating as I learn more about Claude Code. The AI tooling landscape evolves rapidly, so some information might be outdated by the time you read this. If you find something that needs updating, feel free to reach out!&lt;/p>
&lt;p>Whether you&amp;rsquo;re a &lt;strong>junior developer&lt;/strong> just getting started or a &lt;strong>senior developer&lt;/strong> looking to boost your workflow, Claude Code has something for everyone.&lt;/p></description><content:encoded><![CDATA[<h2 id="a-quick-disclaimer">A Quick Disclaimer</h2>
<p>This article is primarily a <strong>self-note</strong> that I keep updating as I learn more about Claude Code. The AI tooling landscape evolves rapidly, so some information might be outdated by the time you read this. If you find something that needs updating, feel free to reach out!</p>
<p>Whether you&rsquo;re a <strong>junior developer</strong> just getting started or a <strong>senior developer</strong> looking to boost your workflow, Claude Code has something for everyone.</p>
<hr>
<h2 id="why-should-you-care">Why Should You Care?</h2>
<p>If you&rsquo;ve read my <a href="https://md.eknath.dev/posts/software-development/devfest2025-solutionist-mindset-talk/">Solutionist Mindset talk</a>, you know I believe in <strong>using AI as a copilot, not a pilot</strong>. Claude Code embodies this philosophy perfectly—it&rsquo;s a CLI tool that sits alongside your existing workflow, helping you move faster while keeping you in control.</p>
<p>For Android and <strong>Compose Multiplatform (KMP)</strong> developers like us, having a tool that understands our codebase context is game-changing. Gradle configurations, multi-module architectures, platform-specific implementations—Claude Code can navigate all of this.</p>
<hr>
<h2 id="what-is-claude-code">What is Claude Code?</h2>
<p>Claude Code is Anthropic&rsquo;s <strong>official CLI tool</strong> that brings Claude directly into your terminal. Unlike the web interface, it:</p>
<ul>
<li><strong>Has full access to your codebase</strong> (with your permission)</li>
<li><strong>Can read, write, and edit files</strong> directly</li>
<li><strong>Runs shell commands</strong> for you</li>
<li><strong>Understands project context</strong> across multiple files</li>
<li><strong>Integrates with Git</strong> for version control operations</li>
</ul>
<p>Think of it as having a senior developer sitting next to you who can:</p>
<ul>
<li>Explore your codebase instantly</li>
<li>Write and refactor code</li>
<li>Debug issues by reading logs and stack traces</li>
<li>Create commits and PRs</li>
<li>Explain complex code sections</li>
</ul>
<hr>
<h2 id="latest-updates-january-2026">Latest Updates (January 2026)</h2>
<p><strong>Update available for Claude Code users (v: 2.1.2)!</strong></p>
<h3 id="model-selection">Model Selection</h3>
<p>Opus 4.5 is now available in model selection. While <strong>Sonnet is superior for general coding tasks</strong>, <strong>Opus is amazing for complex features and crazy bugs</strong>. You can use <code>-m</code> flag for selecting a specific model for particular task sessions.</p>
<table>
  <thead>
      <tr>
          <th>Model ID</th>
          <th>Description</th>
          <th>Context Window</th>
          <th>Relative Cost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>claude-sonnet-4-20250514</code></td>
          <td>Sonnet 4 (default, most balanced)</td>
          <td>200K</td>
          <td>$$ (Moderate)</td>
      </tr>
      <tr>
          <td><code>claude-opus-4-20250514</code></td>
          <td>Opus 4 (most capable, slower)</td>
          <td>200K</td>
          <td>$$$$ (Very High)</td>
      </tr>
      <tr>
          <td><code>claude-sonnet-4-5-20250929</code></td>
          <td>Sonnet 4.5 (smartest, efficient)</td>
          <td>500K</td>
          <td>$$$ (High)</td>
      </tr>
      <tr>
          <td><code>claude-haiku-4-5-20251001</code></td>
          <td>Haiku 4.5 (fastest, most economical)</td>
          <td>200K</td>
          <td>$ (Low)</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p>[!WARNING]
<strong>Token Usage Warning</strong>: Continuous usage of <strong>Opus</strong> models will consume your rate limits and quota significantly faster (approx. 5-10x) than Sonnet. Use Opus only for complex debugging or architectural tasks.</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Run with a specific model</span>
</span></span><span style="display:flex;"><span>claude -m claude-opus-4-20250514
</span></span></code></pre></div><h3 id="official-plugins">Official Plugins</h3>
<p>Check out the official plugins:</p>
<ul>
<li><a href="https://github.com/anthropics/claude-code/tree/main/plugins">All Plugins</a></li>
<li><a href="https://github.com/anthropics/claude-code/tree/main/plugins/code-review">Code Review Plugin</a></li>
</ul>
<hr>
<h2 id="getting-started">Getting Started</h2>
<h3 id="installation">Installation</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Using npm</span>
</span></span><span style="display:flex;"><span>npm install -g @anthropic-ai/claude-code
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Using Homebrew (macOS)</span>
</span></span><span style="display:flex;"><span>brew install claude-code
</span></span></code></pre></div><p>After installation, run <code>claude</code> in your terminal to start an interactive session. You&rsquo;ll need to authenticate with your Anthropic API key or use the <code>claude --login</code> command to login via the browser.</p>
<h3 id="basic-usage">Basic Usage</h3>
<p>Navigate to your project directory and simply run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>claude
</span></span></code></pre></div><p>This starts an interactive session where you can ask questions, request code changes, or explore your codebase.</p>
<hr>
<h2 id="essential-commands-every-developer-should-know">Essential Commands Every Developer Should Know</h2>
<h3 id="1-slash-commands">1️⃣ Slash Commands</h3>
<p>Claude Code has built-in slash commands that trigger specific workflows:</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>What It Does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/help</code></td>
          <td>Shows available commands and usage tips</td>
      </tr>
      <tr>
          <td><code>/clear</code></td>
          <td>Clears conversation history (starts fresh)</td>
      </tr>
      <tr>
          <td><code>/compact</code></td>
          <td>Compresses the conversation to save context</td>
      </tr>
      <tr>
          <td><code>/cost</code></td>
          <td>Shows token usage and estimated costs</td>
      </tr>
      <tr>
          <td><code>/doctor</code></td>
          <td>Diagnoses installation and configuration issues</td>
      </tr>
      <tr>
          <td><code>/init</code></td>
          <td>Creates a CLAUDE.md file with project context</td>
      </tr>
      <tr>
          <td><code>/review</code></td>
          <td>Triggers code review for recent changes</td>
      </tr>
      <tr>
          <td><code>/commit</code></td>
          <td>Creates a git commit with meaningful message</td>
      </tr>
  </tbody>
</table>
<h3 id="2-the-claudemd-file">2️⃣ The CLAUDE.md File</h3>
<p>One of the most powerful features for <strong>multi-module Android/KMP projects</strong> is the <code>CLAUDE.md</code> file. Create this at your project root:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Project: MyKMPApp
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Architecture
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Multi-module KMP project
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Shared module: commonMain, androidMain, iosMain
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Android app module with Jetpack Compose UI
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> iOS app using SwiftUI
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Key Conventions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> Use Koin for dependency injection
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Room for local database (Android), SQLDelight for shared
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Ktor for networking
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> Kotlin Coroutines + Flow for async operations
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Build Commands
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`./gradlew assembleDebug`</span> - Build Android debug
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`./gradlew :shared:build`</span> - Build shared module only
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`./gradlew connectedAndroidTest`</span> - Run instrumented tests
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Module Structure
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> :app - Android application entry point
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :shared - KMP shared code
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :feature:home - Home feature module
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :feature:settings - Settings feature module
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :core:network - Networking utilities
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> :core:database - Database layer
</span></span></code></pre></div><p>Claude reads this file and uses it as <strong>persistent context</strong> for every conversation. This is incredibly useful for:</p>
<ul>
<li><strong>Multi-module navigation</strong> - Claude knows your module structure</li>
<li><strong>Consistent coding patterns</strong> - Follows your conventions</li>
<li><strong>Faster builds</strong> - Knows the right Gradle commands</li>
</ul>
<h3 id="3-vim-style-keybindings">3️⃣ Vim-Style Keybindings</h3>
<p>For terminal enthusiasts, Claude Code supports vim keybindings:</p>
<table>
  <thead>
      <tr>
          <th>Key</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Escape</code></td>
          <td>Enter command mode</td>
      </tr>
      <tr>
          <td><code>i</code></td>
          <td>Return to insert mode</td>
      </tr>
      <tr>
          <td><code>Ctrl+C</code></td>
          <td>Cancel current operation</td>
      </tr>
      <tr>
          <td><code>Ctrl+D</code></td>
          <td>Exit Claude Code</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="practical-use-cases-for-androidkmp-developers">Practical Use Cases for Android/KMP Developers</h2>
<h3 id="use-case-1-exploring-unfamiliar-codebases">Use Case 1: Exploring Unfamiliar Codebases</h3>
<p>When you join a new project or inherit legacy code:</p>
<pre tabindex="0"><code>You: How is the authentication flow implemented in this app?
     Show me the key files involved.
</code></pre><p>Claude will search through your codebase, identify relevant files (ViewModels, Repositories, API services), and explain the flow.</p>
<h3 id="use-case-2-writing-compose-ui-components">Use Case 2: Writing Compose UI Components</h3>
<pre tabindex="0"><code>You: Create a bottom sheet component for filtering products.
     It should have checkboxes for categories and a price range slider.
     Follow our existing design system in :core:designsystem module.
</code></pre><p>Claude will:</p>
<ol>
<li>Look at your existing design system</li>
<li>Match the patterns and naming conventions</li>
<li>Create the component following your architecture</li>
</ol>
<h3 id="use-case-3-debugging-build-issues">Use Case 3: Debugging Build Issues</h3>
<pre tabindex="0"><code>You: I&#39;m getting this Gradle error when building the shared module:
     [paste error here]

     Help me understand and fix it.
</code></pre><p>Claude can read your <code>build.gradle.kts</code> files, understand the dependency graph, and suggest fixes.</p>
<h3 id="use-case-4-writing-platform-specific-implementations">Use Case 4: Writing Platform-Specific Implementations</h3>
<pre tabindex="0"><code>You: I need to implement biometric authentication.
     Create the expect/actual declarations for Android and iOS
     in the :core:auth module.
</code></pre><p>Claude understands KMP&rsquo;s expect/actual mechanism and generates appropriate platform-specific code.</p>
<h3 id="use-case-5-creating-git-commits">Use Case 5: Creating Git Commits</h3>
<pre tabindex="0"><code>You: /commit
</code></pre><p>Claude will:</p>
<ol>
<li>Analyze your staged changes</li>
<li>Understand the context of modifications</li>
<li>Generate a meaningful commit message</li>
<li>Create the commit</li>
</ol>
<h3 id="use-case-6-room-database-migrations">Use Case 6: Room Database Migrations</h3>
<pre tabindex="0"><code>You: I need to add a &#39;lastSyncedAt&#39; column to the UserEntity.
     Create the migration and update the entity.
</code></pre><p>Claude handles the boilerplate of Room migrations, which can be error-prone manually.</p>
<hr>
<h2 id="best-practices-for-effective-usage">Best Practices for Effective Usage</h2>
<h3 id="-do">✅ DO</h3>
<ol>
<li>
<p><strong>Be specific with context</strong> - Instead of &ldquo;fix this bug&rdquo;, say &ldquo;fix the crash in <code>UserRepository.kt</code> when the token expires&rdquo; make sure you add the file and line-number of the function or scope.</p>
</li>
<li>
<p><strong>Review generated code</strong> - Always understand what Claude writes. Don&rsquo;t blindly accept suggestions.</p>
</li>
<li>
<p><strong>Use it for exploration</strong> - Ask Claude to explain complex parts of your codebase or third-party libraries</p>
</li>
<li>
<p><strong>Leverage for boilerplate</strong> - Let Claude handle repetitive tasks like:</p>
<ul>
<li>Creating data classes from API responses</li>
<li>Writing Room entities and DAOs</li>
<li>Setting up Hilt/Koin modules</li>
<li>Creating navigation graphs</li>
<li>Writing unit test boilerplate</li>
</ul>
</li>
<li>
<p><strong>Maintain your CLAUDE.md</strong> - Keep it updated as your project evolves</p>
</li>
<li>
<p><strong>Use the right model for the task</strong> - Haiku for quick questions, Sonnet for general coding, Opus for complex debugging</p>
</li>
</ol>
<h3 id="-dont">❌ DON&rsquo;T</h3>
<ol>
<li>
<p><strong>Don&rsquo;t share sensitive data</strong> - Avoid passing API keys, secrets, or user data through Claude</p>
</li>
<li>
<p><strong>Don&rsquo;t skip the review</strong> - Especially for security-critical code (authentication, payment processing, encryption etc)</p>
</li>
<li>
<p><strong>Don&rsquo;t use it as a crutch</strong> - You should still understand the fundamentals. AI is a multiplier, not a replacement.</p>
</li>
<li>
<p><strong>Don&rsquo;t expect perfection</strong> - Claude can make mistakes. Treat its output as a starting point.</p>
</li>
<li>
<p><strong>Don&rsquo;t ignore Gradle sync</strong> - After Claude modifies <code>build.gradle.kts</code>, sync manually in Android Studio</p>
</li>
</ol>
<hr>
<h2 id="cost-management-tips">Cost Management Tips</h2>
<p>Claude Code uses API tokens, which cost money. Here&rsquo;s how to optimize:</p>
<h3 id="1-choose-the-right-model">1️⃣ Choose the Right Model</h3>
<table>
  <thead>
      <tr>
          <th>Task Type</th>
          <th>Recommended Model</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quick questions, simple edits</td>
          <td>Haiku 4.5</td>
      </tr>
      <tr>
          <td>General development</td>
          <td>Sonnet 4/4.5</td>
      </tr>
      <tr>
          <td>Complex debugging, architecture</td>
          <td>Opus 4/4.5</td>
      </tr>
  </tbody>
</table>
<h3 id="2-use-compact-regularly">2️⃣ Use <code>/compact</code> Regularly</h3>
<p>Long conversations consume more tokens. Use <code>/compact</code> to summarize and reduce context.</p>
<h3 id="3-start-fresh-for-new-tasks">3️⃣ Start Fresh for New Tasks</h3>
<p>Use <code>/clear</code> when switching to unrelated tasks. No need to carry previous context.</p>
<h3 id="4-be-concise">4️⃣ Be Concise</h3>
<p>Instead of:</p>
<pre tabindex="0"><code>&#34;Hey Claude, I was wondering if you could maybe help me
understand how the user authentication works in this app,
like when someone logs in, what happens step by step?&#34;
</code></pre><p>Try:</p>
<pre tabindex="0"><code>&#34;Explain the login flow. Start from LoginViewModel.&#34;
</code></pre><h3 id="6-offload-tasks-to-other-modelstools-save-those-tokens">6️⃣ Offload Tasks to Other Models/Tools (Save those Tokens!)</h3>
<p>Not everything requires Claude Code&rsquo;s deep context awareness. Save your tokens by routing tasks to the right tool:</p>
<ul>
<li><strong>Use Gemini for Quick Concepts</strong>: Need to understand &ldquo;How <code>LruCache</code> works internally&rdquo; or &ldquo;Explain the Builder pattern&rdquo;? Use <strong>Gemini</strong>. It&rsquo;s fast, free/cheap, and great for general knowledge that doesn&rsquo;t need your private codebase context.</li>
<li><strong>Use ChatGPT for High-Level Project Questions</strong>: If you need advice on &ldquo;Best practices for modularizing a KMP project&rdquo; or architecture discussions where providing full code access isn&rsquo;t necessary, <strong>ChatGPT</strong> is a great option.</li>
<li><strong>Use CLI Tools for Quick Answers</strong>: If you&rsquo;re a terminal power user (using <code>tmux</code>, <code>dia</code>, etc.), tools like <strong>ddgr</strong> (DuckDuckGo from terminal) or <strong>Ollama</strong> (local models) are fantastic for quick lookups without leaving your flow.</li>
</ul>
<blockquote>
<p>[!TIP]
<strong>Pro Tip</strong>: Reserve Claude Code for tasks that <em>specifically</em> need to read your files, understand your project structure, or perform edits. For everything else, cheaper (or free) alternatives often work just as well!</p>
</blockquote>
<h3 id="7-check-costs-with-cost">7️⃣ Check Costs with <code>/cost</code></h3>
<p>Regularly run <code>/cost</code> to monitor your usage.</p>
<hr>
<h2 id="integration-with-development-workflow">Integration with Development Workflow</h2>
<h3 id="ide-integration">IDE Integration</h3>
<p>Claude Code works alongside your IDE, not inside it. A typical workflow:</p>
<ol>
<li><strong>Have your IDE open</strong> (Android Studio / Fleet / IntelliJ)</li>
<li><strong>Run Claude in a terminal</strong> (split screen works great)</li>
<li><strong>Ask Claude to make changes</strong></li>
<li><strong>Review changes in IDE</strong> (they appear instantly)</li>
<li><strong>Test and iterate</strong></li>
</ol>
<h3 id="git-workflow-integration">Git Workflow Integration</h3>
<p>Claude Code integrates nicely with Git:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start a session</span>
</span></span><span style="display:flex;"><span>claude
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create meaningful commits</span>
</span></span><span style="display:flex;"><span>You: /commit
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create a pull request</span>
</span></span><span style="display:flex;"><span>You: Create a PR <span style="color:#66d9ef">for</span> these changes. The target branch is develop.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Review code before pushing</span>
</span></span><span style="display:flex;"><span>You: Review the changes in the last commit <span style="color:#66d9ef">for</span> any issues.
</span></span></code></pre></div><hr>
<h2 id="common-gotchas-for-androidkmp-projects">Common Gotchas for Android/KMP Projects</h2>
<h3 id="1-gradle-sync-after-changes">1️⃣ Gradle Sync After Changes</h3>
<p>When Claude modifies <code>build.gradle.kts</code> files, you&rsquo;ll need to sync in Android Studio manually. Claude can&rsquo;t trigger this for you.</p>
<h3 id="2-resource-files">2️⃣ Resource Files</h3>
<p>Claude can create/modify XML resources (layouts, strings, drawables), but be careful with:</p>
<ul>
<li><strong>Generated resources</strong> (R class) - These regenerate on build</li>
<li><strong>Vector drawables</strong> - Complex paths might need manual tweaking</li>
</ul>
<h3 id="3-compose-preview">3️⃣ Compose Preview</h3>
<p>Claude-generated Compose components might need <code>@Preview</code> annotations added for visibility in Android Studio&rsquo;s preview pane.</p>
<h3 id="4-ios-specific-code">4️⃣ iOS-Specific Code</h3>
<p>For KMP projects, Claude can write Swift/Objective-C code for iOS implementations, but:</p>
<ul>
<li>You&rsquo;ll need Xcode to verify it compiles</li>
<li>Swift interop with Kotlin can be tricky</li>
</ul>
<h3 id="5-version-catalog">5️⃣ Version Catalog</h3>
<p>If you use <code>libs.versions.toml</code>, make sure Claude knows about it in your <code>CLAUDE.md</code>. Otherwise, it might use hardcoded versions.</p>
<hr>
<h2 id="my-personal-workflow">My Personal Workflow</h2>
<p>Here&rsquo;s how I typically use Claude Code for Android development:</p>
<ol>
<li>
<p><strong>Morning exploration</strong> - &ldquo;What did I work on yesterday? Show me recent changes.&rdquo;</p>
</li>
<li>
<p><strong>Feature development</strong> - Start with asking Claude to explore existing patterns, then implement following those patterns</p>
</li>
<li>
<p><strong>Code review helper</strong> - &ldquo;Review this ViewModel for potential memory leaks or coroutine issues&rdquo;</p>
</li>
<li>
<p><strong>Documentation</strong> - &ldquo;Generate KDoc comments for the public methods in NetworkClient.kt&rdquo;</p>
</li>
<li>
<p><strong>Refactoring</strong> - &ldquo;Migrate this callback-based API to use Kotlin Coroutines with Flow&rdquo;</p>
</li>
<li>
<p><strong>Test writing</strong> - &ldquo;Write unit tests for UserRepository using MockK&rdquo;</p>
</li>
</ol>
<hr>
<h2 id="advanced-mcp-servers">Advanced: MCP Servers</h2>
<p>Claude Code supports <strong>Model Context Protocol (MCP)</strong> servers, which extend its capabilities. For Android developers, interesting MCPs include:</p>
<ul>
<li><strong>File system access</strong> - Already built-in</li>
<li><strong>Git operations</strong> - Built-in</li>
<li><strong>Web search</strong> - For documentation lookups</li>
<li><strong>Custom tools</strong> - You can create your own MCP servers</li>
</ul>
<p>Check out the <a href="https://modelcontextprotocol.io/">MCP Documentation</a> for more details.</p>
<hr>
<h2 id="useful-one-liners">Useful One-Liners</h2>
<p>Quick commands I use frequently:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start with a specific model for complex tasks</span>
</span></span><span style="display:flex;"><span>claude -m claude-opus-4-20250514
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Resume last conversation</span>
</span></span><span style="display:flex;"><span>claude --resume
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check installation health</span>
</span></span><span style="display:flex;"><span>claude /doctor
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Quick code review</span>
</span></span><span style="display:flex;"><span>claude <span style="color:#e6db74">&#34;Review my staged changes for issues&#34;</span>
</span></span></code></pre></div><hr>
<h2 id="whats-next">What&rsquo;s Next?</h2>
<p>This article covers the fundamentals, but Claude Code is constantly evolving. I plan to update this note as I discover:</p>
<ul>
<li>New features and capabilities</li>
<li>Better workflows for KMP development</li>
<li>Integration patterns with CI/CD</li>
<li>Team collaboration strategies</li>
</ul>
<hr>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Claude Code is a <strong>powerful addition to the Android/KMP developer toolkit</strong>. It&rsquo;s not about replacing your skills—it&rsquo;s about <strong>amplifying</strong> them. Use it to handle boilerplate, explore unfamiliar code, and move faster on repetitive tasks.</p>
<p>But remember: <strong>You are still the pilot</strong>. Claude is your copilot. Understand what it generates, review its suggestions, and keep learning the fundamentals.</p>
<p>The developers who thrive in the AI age won&rsquo;t be those who code the fastest—they&rsquo;ll be the ones who <strong>solve problems thoughtfully</strong> while leveraging every tool at their disposal.</p>
<hr>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://docs.anthropic.com/claude-code">Official Claude Code Documentation</a></li>
<li><a href="https://github.com/anthropics/claude-code">Claude Code GitHub</a></li>
<li><a href="https://github.com/anthropics/claude-code/tree/main/plugins">Official Plugins</a></li>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a></li>
<li><a href="https://md.eknath.dev/posts/software-development/devfest2025-solutionist-mindset-talk/">My Solutionist Mindset Talk</a></li>
</ul>
<hr>
<p>Feel free to connect with me on:
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong>
🌍 <strong><a href="https://eknath.dev">Website</a></strong></p>
<p><em>I wish to keep this article as a living document. Last updated: January 2026</em></p>
]]></content:encoded></item><item><title>Clickable Links in Jetpack Compose: AnnotatedString with Custom URL Handler</title><link>https://md.eknath.dev/posts/android/clickable-links/</link><pubDate>Tue, 11 Nov 2025 22:30:28 +0530</pubDate><guid>https://md.eknath.dev/posts/android/clickable-links/</guid><description>&lt;p>While building the onboarding screen for my app, I needed a simple text that read something like this:&lt;/p>
&lt;p>&lt;img alt="Example of the Implementation" loading="lazy" src="https://md.eknath.dev/img/android/clickable-text-android-example.jpeg">
&lt;em>The green hyperlinks are clickable&lt;/em>&lt;/p>
&lt;p>Pretty straightforward, right? Just a bit of text with two clickable links — &lt;code>Privacy Policy&lt;/code> and &lt;code>Terms of Service&lt;/code>.&lt;/p>
&lt;p>But as I started implementing it, I realized: &lt;strong>there isn&amp;rsquo;t a single complete guide&lt;/strong> explaining how to make part of a text clickable using &lt;code>AnnotatedString&lt;/code> in Jetpack Compose — especially with the new &lt;strong>LinkAnnotation API&lt;/strong> and a &lt;strong>custom URL handler&lt;/strong> for better control.&lt;/p></description><content:encoded><![CDATA[<p>While building the onboarding screen for my app, I needed a simple text that read something like this:</p>
<p><img alt="Example of the Implementation" loading="lazy" src="/img/android/clickable-text-android-example.jpeg">
<em>The green hyperlinks are clickable</em></p>
<p>Pretty straightforward, right? Just a bit of text with two clickable links — <code>Privacy Policy</code> and <code>Terms of Service</code>.</p>
<p>But as I started implementing it, I realized: <strong>there isn&rsquo;t a single complete guide</strong> explaining how to make part of a text clickable using <code>AnnotatedString</code> in Jetpack Compose — especially with the new <strong>LinkAnnotation API</strong> and a <strong>custom URL handler</strong> for better control.</p>
<p>So I&rsquo;m writing this as both a reference for myself and for anyone else struggling with this tiny, under-documented detail.</p>
<hr>
<h2 id="why-custom-url-handling">Why Custom URL Handling?</h2>
<p>By default, Compose can open links using the system browser. But what if you want:</p>
<ul>
<li><strong>Better UX with Chrome Custom Tabs</strong> (opens links in-app with smooth transitions)</li>
<li><strong>Graceful fallbacks</strong> (handle cases where browsers aren&rsquo;t installed)</li>
<li><strong>Full control</strong> over how URLs are opened (analytics, deep links, etc.)</li>
</ul>
<p>That&rsquo;s exactly what we&rsquo;ll implement.</p>
<hr>
<h2 id="step-by-step-implementation">Step-by-Step Implementation</h2>
<h3 id="1-add-the-browser-library">1. Add the Browser Library</h3>
<p>We&rsquo;ll use <strong>Android Browser Library</strong> to implement Chrome Custom Tabs for a better link-opening experience.</p>
<p>Add this to your <code>libs.versions.toml</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">versions</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">browser</span> = <span style="color:#e6db74">&#34;1.8.0&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>[<span style="color:#a6e22e">libraries</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">androidx-browser</span> = { <span style="color:#a6e22e">group</span> = <span style="color:#e6db74">&#34;androidx.browser&#34;</span>, <span style="color:#a6e22e">name</span> = <span style="color:#e6db74">&#34;browser&#34;</span>, <span style="color:#a6e22e">version</span>.<span style="color:#a6e22e">ref</span> = <span style="color:#e6db74">&#34;browser&#34;</span> }
</span></span></code></pre></div><p>Then add the dependency to your app&rsquo;s <code>build.gradle.kts</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>dependencies {
</span></span><span style="display:flex;"><span>    implementation(libs.androidx.browser)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Why Chrome Custom Tabs?</strong>
Custom Tabs open web content <strong>inside your app</strong> with a smooth transition, pre-loaded browser engine, and shared cookies/sessions — all without leaving your app&rsquo;s context.</p>
<hr>
<h3 id="2-create-a-custom-url-handler">2. Create a Custom URL Handler</h3>
<p>This function handles opening URLs with <strong>three layers of fallback</strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">urlHandler</span>(url: String, context: Context) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// First: Try Chrome Custom Tabs (best UX)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> customTabsIntent = <span style="color:#a6e22e">CustomTabsIntent</span>.Builder().build()
</span></span><span style="display:flex;"><span>        customTabsIntent.launchUrl(context, <span style="color:#a6e22e">Uri</span>.parse(url))
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: ActivityNotFoundException) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Second: Fallback to system browser
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#66d9ef">val</span> intent = Intent(<span style="color:#a6e22e">Intent</span>.ACTION_VIEW, <span style="color:#a6e22e">Uri</span>.parse(url))
</span></span><span style="display:flex;"><span>            context.startActivity(intent)
</span></span><span style="display:flex;"><span>        } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Third: Show toast if no browser exists
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>                context,
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;Browser not installed! visit: </span><span style="color:#e6db74">$url</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Toast</span>.LENGTH_LONG
</span></span><span style="display:flex;"><span>            ).show()
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">catch</span> (e: Exception) {
</span></span><span style="display:flex;"><span>        e.printStackTrace()
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Toast</span>.makeText(
</span></span><span style="display:flex;"><span>            context,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#34;Unable to open link! visit: </span><span style="color:#e6db74">$url</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Toast</span>.LENGTH_SHORT
</span></span><span style="display:flex;"><span>        ).show()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>How it works:</strong></p>
<ol>
<li><strong>Chrome Custom Tabs</strong> (preferred): Opens in-app with native feel</li>
<li><strong>System Browser</strong> (fallback): Opens in the default browser if Custom Tabs fail</li>
<li><strong>Toast Message</strong> (last resort): Shows URL if no browser is available</li>
</ol>
<p>This ensures <strong>your app never crashes</strong> when opening links, even on devices without browsers.</p>
<hr>
<h3 id="3-building-the-clickable-text-component">3. Building the Clickable Text Component</h3>
<p>Now let&rsquo;s create the actual composable with clickable links.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@OptIn</span>(ExperimentalTextApi<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">TermsAndCondition</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> context = <span style="color:#a6e22e">LocalContext</span>.current
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Define how links should behave when clicked
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> linkInteractionListener = LinkInteractionListener {
</span></span><span style="display:flex;"><span>        urlHandler((<span style="color:#66d9ef">it</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">LinkAnnotation</span>.Url).url, context)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Define link styling (color + underline)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> linkStyle = TextLinkStyles(
</span></span><span style="display:flex;"><span>        SpanStyle(
</span></span><span style="display:flex;"><span>            color = DGreen,
</span></span><span style="display:flex;"><span>            textDecoration = <span style="color:#a6e22e">TextDecoration</span>.Underline
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Create Privacy Policy link annotation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> privacyPolicy = <span style="color:#a6e22e">LinkAnnotation</span>.Url(
</span></span><span style="display:flex;"><span>        url = <span style="color:#a6e22e">URLS</span>.PRIVACY,
</span></span><span style="display:flex;"><span>        styles = linkStyle,
</span></span><span style="display:flex;"><span>        linkInteractionListener = linkInteractionListener
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Create Terms of Service link annotation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> toc = <span style="color:#a6e22e">LinkAnnotation</span>.Url(
</span></span><span style="display:flex;"><span>        url = <span style="color:#a6e22e">URLS</span>.TOC,
</span></span><span style="display:flex;"><span>        styles = linkStyle,
</span></span><span style="display:flex;"><span>        linkInteractionListener = linkInteractionListener
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Helper functions for reusable link text (supports localization!)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">AnnotatedString</span>.<span style="color:#a6e22e">Builder</span>.privacyLink() {
</span></span><span style="display:flex;"><span>        withLink(privacyPolicy) {
</span></span><span style="display:flex;"><span>            append(stringResource(id = <span style="color:#a6e22e">R</span>.string.privacy_policy))
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">AnnotatedString</span>.<span style="color:#a6e22e">Builder</span>.tocLink() {
</span></span><span style="display:flex;"><span>        withLink(toc) {
</span></span><span style="display:flex;"><span>            append(stringResource(id = <span style="color:#a6e22e">R</span>.string.terms_of_service))
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Build the complete annotated string with mixed text + links
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> annotatedString = buildAnnotatedString {
</span></span><span style="display:flex;"><span>        append(<span style="color:#e6db74">&#34;Read &#34;</span>)
</span></span><span style="display:flex;"><span>        privacyLink()  <span style="color:#75715e">// Clickable Privacy Policy
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        append(<span style="color:#e6db74">&#34; and tap on `&#34;</span>)
</span></span><span style="display:flex;"><span>        withStyle(style = SpanStyle(fontWeight = <span style="color:#a6e22e">FontWeight</span>.Bold)) {
</span></span><span style="display:flex;"><span>            append(<span style="color:#e6db74">&#34;Agree and Continue&#34;</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        append(<span style="color:#e6db74">&#34;` to accept the &#34;</span>)
</span></span><span style="display:flex;"><span>        tocLink()  <span style="color:#75715e">// Clickable Terms of Service
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    Text(
</span></span><span style="display:flex;"><span>        text = annotatedString,
</span></span><span style="display:flex;"><span>        style = <span style="color:#a6e22e">PannaiTheme</span>.typography.semiBold12.copy(
</span></span><span style="display:flex;"><span>            color = LGray,
</span></span><span style="display:flex;"><span>            textAlign = <span style="color:#a6e22e">TextAlign</span>.Center
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        modifier = <span style="color:#a6e22e">Modifier</span>.fillMaxWidth(<span style="color:#ae81ff">0.8f</span>),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><hr>
<h2 id="breaking-down-the-code">Breaking Down the Code</h2>
<h3 id="linkinteractionlistener">LinkInteractionListener</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> linkInteractionListener = LinkInteractionListener {
</span></span><span style="display:flex;"><span>    urlHandler((<span style="color:#66d9ef">it</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">LinkAnnotation</span>.Url).url, context)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li>This listener gets triggered when <strong>any link is clicked</strong></li>
<li>It extracts the URL from the <code>LinkAnnotation.Url</code> object</li>
<li>Passes it to our custom <code>urlHandler()</code> for controlled opening</li>
</ul>
<hr>
<h3 id="linkannotationurl">LinkAnnotation.Url</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> privacyPolicy = <span style="color:#a6e22e">LinkAnnotation</span>.Url(
</span></span><span style="display:flex;"><span>    url = <span style="color:#a6e22e">URLS</span>.PRIVACY,
</span></span><span style="display:flex;"><span>    styles = linkStyle,
</span></span><span style="display:flex;"><span>    linkInteractionListener = linkInteractionListener
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li><strong>url</strong>: The actual link destination</li>
<li><strong>styles</strong>: How the link looks (color, underline, etc.)</li>
<li><strong>linkInteractionListener</strong>: What happens when clicked</li>
</ul>
<p>This is the <strong>new Compose way</strong> of handling links (replaces older <code>pushStringAnnotation</code> approach).</p>
<hr>
<h3 id="helper-functions-for-localization">Helper Functions for Localization</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Composable</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">AnnotatedString</span>.<span style="color:#a6e22e">Builder</span>.privacyLink() {
</span></span><span style="display:flex;"><span>    withLink(privacyPolicy) {
</span></span><span style="display:flex;"><span>        append(stringResource(id = <span style="color:#a6e22e">R</span>.string.privacy_policy))
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Why helper functions?</strong>
By using <code>stringResource()</code>, the link text <strong>automatically updates</strong> based on user&rsquo;s language. This makes your code cleaner, reusable, and supports multi-language apps without duplicating logic.</p>
<hr>
<h3 id="building-the-final-text">Building the Final Text</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">val</span> annotatedString = buildAnnotatedString {
</span></span><span style="display:flex;"><span>    append(<span style="color:#e6db74">&#34;Read &#34;</span>)
</span></span><span style="display:flex;"><span>    privacyLink()  <span style="color:#75715e">// This part is clickable!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    append(<span style="color:#e6db74">&#34; and tap on `&#34;</span>)
</span></span><span style="display:flex;"><span>    withStyle(style = SpanStyle(fontWeight = <span style="color:#a6e22e">FontWeight</span>.Bold)) {
</span></span><span style="display:flex;"><span>        append(<span style="color:#e6db74">&#34;Agree and Continue&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    append(<span style="color:#e6db74">&#34;` to accept the &#34;</span>)
</span></span><span style="display:flex;"><span>    tocLink()  <span style="color:#75715e">// This part is also clickable!
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p>This combines <strong>regular text</strong> + <strong>clickable links</strong> + <strong>styled text</strong> in one component. <code>withLink()</code> wraps text and makes it interactive, while <code>withStyle()</code> applies visual formatting without making it clickable.</p>
<hr>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><strong>LinkAnnotation API</strong> is the modern way to create clickable links in Compose</li>
<li><strong>Custom URL handler</strong> gives you full control over how links open</li>
<li><strong>Chrome Custom Tabs</strong> provides better UX than default browser</li>
<li><strong>Graceful fallbacks</strong> prevent app crashes on edge cases</li>
<li><strong>Helper functions + stringResource</strong> make your code localization-ready</li>
</ul>
<hr>
<h2 id="why-this-approach-is-better">Why This Approach is Better</h2>
<p>Compared to older methods using <code>pushStringAnnotation</code>, this approach:</p>
<ul>
<li><strong>Type-safe</strong>: LinkAnnotation is strongly typed</li>
<li><strong>Easier styling</strong>: Direct style application per link</li>
<li><strong>Better separation</strong>: Link logic separate from text building</li>
<li><strong>Localization-friendly</strong>: Works seamlessly with multi-language apps</li>
</ul>
<hr>
<h2 id="related-resources">Related Resources</h2>
<ul>
<li><a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/text/LinkAnnotation">LinkAnitation Documentation</a></li>
<li><a href="https://developer.chrome.com/docs/android/custom-tabs/guide-get-started">Chrome Custom Tabs Guide</a></li>
</ul>
<hr>
<p><strong>Thanks for reading!</strong> If you found this helpful, feel free to reach out via my social handles.</p>
]]></content:encoded></item><item><title>Solutionist Mindset: Reclaiming Purpose in the Age of AI - My DevFest2025 Talk</title><link>https://md.eknath.dev/posts/software-development/devfest2025-solutionist-mindset-talk/</link><pubDate>Sun, 09 Nov 2025 00:00:00 +0000</pubDate><guid>https://md.eknath.dev/posts/software-development/devfest2025-solutionist-mindset-talk/</guid><description>&lt;p>&lt;em>This is an elaborated version of my talk at Google Developer Group Chennai&amp;rsquo;s DevFest2025, where I shared my journey from rock bottom to becoming an Android Developer at Zoho and how the &amp;ldquo;Solutionist Mindset&amp;rdquo; can help us thrive in the age of AI.&lt;/em>&lt;/p>
&lt;p>&lt;img alt="Speaking at the Platform" loading="lazy" src="https://md.eknath.dev/img/devfest_2025/speaking-at-the-platform-01.JPG">
&lt;em>Presenting the Solutionist Mindset on stage&lt;/em>&lt;/p>
&lt;script defer class="speakerdeck-embed" data-id="ae22be07dca842408af4c8ec3fd4b234" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js">&lt;/script>
&lt;p>Standing on that stage at DevFest2025 in Chennai, looking out to you all, I asked a simple question: &lt;strong>&amp;ldquo;How many of you are worried about AI taking over your job?&amp;rdquo;&lt;/strong>&lt;/p></description><content:encoded><![CDATA[<p><em>This is an elaborated version of my talk at Google Developer Group Chennai&rsquo;s DevFest2025, where I shared my journey from rock bottom to becoming an Android Developer at Zoho and how the &ldquo;Solutionist Mindset&rdquo; can help us thrive in the age of AI.</em></p>
<p><img alt="Speaking at the Platform" loading="lazy" src="/img/devfest_2025/speaking-at-the-platform-01.JPG">
<em>Presenting the Solutionist Mindset on stage</em></p>
<script defer class="speakerdeck-embed" data-id="ae22be07dca842408af4c8ec3fd4b234" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script>
<p>Standing on that stage at DevFest2025 in Chennai, looking out to you all, I asked a simple question: <strong>&ldquo;How many of you are worried about AI taking over your job?&rdquo;</strong></p>
<p>The show of hands was overwhelming (some shared why they are confident too, I wish I was them). The anxiety in the room was palpable.</p>
<p>I get it. I&rsquo;ve been there. In fact, I&rsquo;m still navigating these waters myself. As an Android Developer at Zoho, I&rsquo;ve watched AI tools evolve from curiosities to formidable coding partners. They&rsquo;re impressive—spitting out code 10 to 100 times faster than we can. It&rsquo;s almost unsettling, watching something produce in minutes what would take us hours or days.</p>
<p>The predictions are scary. The layoff numbers are even scarier. Tech Twitter is full of doom-scrolling material about how AI will replace developers, how junior positions are vanishing, how the barrier to entry is impossibly high now.</p>
<p>So I did what any rational person would do—I took a step back and contemplated this deeper.</p>
<p><strong>What&rsquo;s the worst thing that can happen to me?</strong></p>
<p>And then I realized something profound: I&rsquo;ve been through worse.</p>
<hr>
<h2 id="rock-bottom-varanasi-december-2017">Rock Bottom: Varanasi, December 2017</h2>
<p>Let me share something deeply personal with you. Something I don&rsquo;t talk about often, but something that fundamentally changed how I approach adversity.</p>
<p>In December 2017, my life fell apart.</p>
<p>Not in the dramatic, movie-like way—but in the quiet, suffocating way that creeps up on you. Emotionally stuck, I watched as friends I believed would be with me forever just&hellip; disappeared. The community I had built my identity around crumbled. My entire belief system, everything I thought I knew about life and relationships, shattered.</p>
<p>Losing all hope, I set out on what I genuinely thought would be my final journey: to Varanasi.</p>
<h3 id="the-ghats-of-varanasi">The Ghats of Varanasi</h3>
<p>I spent weeks alone at the ghats with almost nothing. It was the coldest winter I&rsquo;ve ever experienced, and I wasn&rsquo;t prepared—not physically, not mentally, not emotionally.</p>
<p>Surviving on just one meal a day at a nearby temple, I would spend my days sitting by the river, gazing into the Ganges in silence. The helplessness, the solitude—it was unlike anything I had ever known before. Even now, years later, I struggle to put that experience into words.</p>
<p>Imagine this: You&rsquo;re standing at the crossroads of chaos. Motion swirls all around you—boats on the river, pilgrims performing rituals, the constant murmur of life and death existing side by side. Yet despite all this movement, you&rsquo;re paralyzed. You can&rsquo;t decide where to step next. Your mind feels numb. The whole experience is so overwhelming that language itself becomes inadequate.</p>
<p>But here&rsquo;s what that experience taught me: <strong>Clarity doesn&rsquo;t come from escaping pain. It comes from sitting with it, accepting it, and taking it in as a lesson from the universe.</strong></p>
<p>When you&rsquo;ve sat at the ghats of Varanasi, contemplating the impermanence of everything while watching funeral pyres burn and pilgrims bathe, the fear of losing a job to AI becomes&hellip; manageable. Not trivial—but definitely manageable.</p>
<hr>
<h2 id="fast-forward-finding-auroville">Fast Forward: Finding Auroville</h2>
<p>After a few more reality checks and life lessons (because apparently, Varanasi wasn&rsquo;t enough), I came across Auroville and its unique philosophy around money and community living.</p>
<p>For those unfamiliar, Auroville is an experimental township in Tamil Nadu, founded on the principles of human unity, sustainable living, and spiritual evolution. Their approach to money and work fascinated me—the idea that work should be about contribution, not just compensation.</p>
<p>I felt drawn to it. It seemed like a place that resonated with what I was searching for—a new way of living, a new framework for understanding my place in the world.</p>
<h3 id="reality-meets-philosophy">Reality Meets Philosophy</h3>
<p>When I arrived, I realized that while the philosophy was inspiring, it was still very much a work in progress—an idea in the process of becoming real. The gap between the ideal and the reality was significant. But you know what? That&rsquo;s okay. That&rsquo;s how all great experiments work.</p>
<p>Yet, amidst that gap, I found something invaluable: a warm, accepting community and my very first job at a busy guest house.</p>
<p>I started as a <strong>waiter</strong>. Then I worked my way up—biller, receptionist, whatever was needed. Each role taught me something new about human interaction, problem-solving, and the simple dignity of work.</p>
<p>It was genuinely educative in ways a classroom never could be. I learned about hospitality, about managing stress during rush hours, about the intricate dance of keeping customers happy while maintaining operational efficiency.</p>
<p>But there was one problem that bugged me daily, especially at the billing counter.</p>
<hr>
<h2 id="my-first-solution-the-python-calculator">My First Solution: The Python Calculator</h2>
<p>Restaurants in Auroville had unique discount structures that reflected their community-oriented values:</p>
<ul>
<li>10% off for volunteers</li>
<li>20% off for Auroville residents</li>
<li>Plus 5% GST for regular guests</li>
</ul>
<p>Simple enough, right? Except during rush hours, when we had to calculate all this <strong>manually using a calculator</strong>.</p>
<p>Picture this: It&rsquo;s lunch rush. The kitchen is yelling that orders are backing up. Customers are waiting, getting increasingly impatient. You&rsquo;re trying to calculate percentages on a basic calculator while keeping track of multiple bills, and someone changes their mind about their order halfway through.</p>
<p>It was absolute chaos.</p>
<p>So, I did something I had never done before—I wrote a <strong>simple Python Tkinter app</strong> to simplify this process.</p>
<h3 id="the-joy-of-building-your-first-solution">The Joy of Building Your First Solution</h3>
<p>Now, to any experienced developer reading this, a Python Tkinter calculator probably sounds laughably simple. And it is. But back then, for someone with minimal programming experience, it was <strong>transformative</strong>.</p>
<p>It took me a couple of days and several iterations. The first version was buggy. The UI was ugly. But it worked. And during the next busy lunch rush, it saved my sanity.</p>
<p>This was my first real solution. Not a tutorial project. Not a homework assignment. A real problem, solved with real code, that helped real people.</p>
<p>Looking back now, it&rsquo;s not sparkly or impressive. I wouldn&rsquo;t put it in my portfolio. But it was a <strong>huge milestone</strong> in my journey. It empowered me to believe that I could build tools to solve problems.</p>
<h3 id="the-ai-comparison">The AI Comparison</h3>
<p>Here&rsquo;s the kicker: If I faced the same problem today, with access to Claude, GPT-4, or Copilot, it would take me <strong>5 minutes or less</strong> to build the same thing. Probably better, with proper error handling and a cleaner UI.</p>
<p>That&rsquo;s both amazing and terrifying. Amazing because of the productivity boost. Terrifying because if I were starting my journey today, would I have learned the same lessons? Would I have struggled enough to truly understand the fundamentals?</p>
<p>This is the paradox we&rsquo;re living in.</p>
<hr>
<h2 id="the-bootcamp-zoho-schools-of-graduate-studies">The Bootcamp: Zoho Schools of Graduate Studies</h2>
<p>After a while working at the restaurant, things were getting repetitive. I could do the job in my sleep. The initial learning curve had flattened out completely.</p>
<p>Then, I was given an opportunity to participate in the <strong>Zoho Schools of Graduate Studies (ZSGS)</strong>—a pilot 3-month fast-track program into software development.</p>
<p>There were interviews. There were coding tests. There were problem-solving assessments.</p>
<p>But you know what stood out during my interview? <strong>That custom calculator project I built to solve the billing problem.</strong></p>
<p>It wasn&rsquo;t the fanciest project. It wasn&rsquo;t built with the latest framework or deploying cutting-edge architecture. But it demonstrated something crucial: <strong>I could identify a problem and build a solution.</strong></p>
<h3 id="the-underdog-journey">The Underdog Journey</h3>
<p>With a few more rounds of interviews and small projects, I was accepted into the program. But here&rsquo;s the thing—acceptance didn&rsquo;t guarantee employment at Zoho. It just meant you&rsquo;d get direct interviews if there were openings afterward.</p>
<p>And I wasn&rsquo;t under any illusion about my chances.</p>
<p>I was the <strong>only individual with just an SSLC (10th grade) certificate</strong> among some incredibly bright minds with degrees in Computer Science, Electronics, Mathematics, and various other technical streams. On paper, I was the least qualified person in that room.</p>
<p>But I knew one thing: I could give my absolute best and absorb everything like a sponge.</p>
<p>I was like a thirsty man stumbling upon an oasis—desperate to quench my thirst by learning everything I possibly could.</p>
<h3 id="the-learning-environment">The Learning Environment</h3>
<p>The program was intense. We learned data structures, algorithms, software design principles, databases, and practical software development. But more than the technical content, it was the <strong>mindset shift</strong> that mattered.</p>
<p>My mentors didn&rsquo;t just teach me how to code—they taught me how to <strong>think</strong>. How to break down complex problems. How to ask the right questions. How to learn independently.</p>
<p>The other students, despite their impressive credentials, were incredibly supportive. We learned from each other, challenged each other, and grew together.</p>
<p>Thanks to my mentors and the collaborative environment, I managed to not just survive, but thrive.</p>
<h3 id="the-interview">The Interview</h3>
<p>And the first team that interviewed me at the end of the program? <strong>They took me in.</strong></p>
<p>Not because of my credentials (I had none). Not because of my degree (I had none). But because I demonstrated the ability to learn, adapt, and build solutions.</p>
<p>That&rsquo;s how I became part of this wonderful tech world. Not through traditional credentials, but through small projects, relentless learning, and incredible mentors who saw potential where others might have seen only limitations.</p>
<hr>
<h2 id="a-glimpse-of-tech-life">A Glimpse of Tech Life</h2>
<p>After joining a team at Zoho, I slowly started getting accustomed to this new life—realizing how incredibly fortunate I was to be on an <strong>autodidactic journey</strong> that felt like a dream.</p>
<p>Learning, growing, and getting paid for it? It seemed almost too good to be true.</p>
<h3 id="the-difference-from-previous-jobs">The Difference from Previous Jobs</h3>
<p>Unlike any job I&rsquo;d had before—waiter, receptionist, biller—this one was fundamentally different. Everything I learned for work actually <strong>empowered me personally</strong>. It felt like one of those rare systems where the more you give, the more you grow.</p>
<p>It was like tending a bonsai tree—carefully shaped by external constraints and guidance, yet every bit of effort you put in feeds your own roots, strengthens your own foundation.</p>
<p>I was getting paid to:</p>
<ul>
<li>Learn new technologies</li>
<li>Experiment with different approaches</li>
<li>Build features that many would use</li>
<li>Solve genuinely interesting problems</li>
</ul>
<p>It felt unreal. It felt like the job I had dreamed about during those cold nights in Varanasi.</p>
<h3 id="the-honeymoon-phase">The Honeymoon Phase</h3>
<p>For a couple of years, it was blissful. The learning curve was steep but manageable. Android development was evolving rapidly—Kotlin was gaining traction, Jetpack Compose was being introduced, and the ecosystem was thriving.</p>
<p>I threw myself into it. I learned Kotlin inside and out. I became proficient in Android architecture patterns. I contributed to significant features in our products.</p>
<p>For a while, I felt secure. I had carved out expertise. I had value.</p>
<p>And then… <strong>the AI wave hit.</strong></p>
<hr>
<h2 id="then-ai-happened">Then AI Happened</h2>
<p>ChatGPT launched in November 2022. At first, it was a curiosity. We played with it, asked it silly questions, marveled at its responses.</p>
<p>Then GitHub Copilot became mainstream. Then GPT-4 arrived. Then Claude. Then specialized coding models like Codex and Replit Ghostwriter.</p>
<p>Watching AI get better at coding, debugging, and doing things that used to be uniquely ours—suddenly, that old fear came back.</p>
<p>Not the fear of losing everything (I&rsquo;d survived that in Varanasi). But the <strong>fear of becoming irrelevant</strong> was just as scary in a different way.</p>
<h3 id="the-existential-questions">The Existential Questions</h3>
<p>What happens when the skills we&rsquo;ve painstakingly built over years suddenly don&rsquo;t matter anymore?</p>
<p>What happens when a fresh graduate with AI assistance can be as productive as a senior developer?</p>
<p>What happens when companies realize they can maintain codebases with fewer people because AI handles the routine work?</p>
<p>These weren&rsquo;t hypothetical questions anymore. They were becoming reality. The layoff announcements from major tech companies included explicit mentions of &ldquo;productivity improvements through AI&rdquo; as justification.</p>
<h3 id="the-essay-that-changed-my-perspective">The Essay That Changed My Perspective</h3>
<p>Around that time, in one of our team discussions about the future of software development, my manager shared an essay from <strong>1932</strong> by Bertrand Russell called <strong>&ldquo;In Praise of Idleness.&rdquo;</strong></p>
<p>Has anyone here read it? If not, I highly recommend it.</p>
<p>Russell wrote this during the <strong>Great Depression</strong>, when millions were unemployed and the world was in economic turmoil. Yet his argument wasn&rsquo;t about working harder to save the economy—it was about working <strong>smarter</strong> and questioning the entire premise of constant labor.</p>
<h3 id="russells-radical-idea">Russell&rsquo;s Radical Idea</h3>
<p>Russell argued that modern civilization had created a <strong>moral trap</strong> where we measure human worth by hours worked, not by problems solved or lives improved.</p>
<p>He believed that if machines could handle labor, we shouldn&rsquo;t artificially create more meaningless work just to keep people busy. Instead, we should use that freedom for:</p>
<ul>
<li>Creativity</li>
<li>Learning</li>
<li>Art</li>
<li>Philosophy</li>
<li>Solving problems that actually matter</li>
</ul>
<p>Think about that. Written in <strong>1932</strong>, before:</p>
<ul>
<li>Computers</li>
<li>The Internet</li>
<li>Mobile phones</li>
<li>Artificial Intelligence</li>
</ul>
<p>Yet it feels like it was written specifically for us, right now, in 2025.</p>
<h3 id="the-question-reframed">The Question Reframed</h3>
<p>Russell&rsquo;s essay helped me reframe the question entirely.</p>
<p>It&rsquo;s not &ldquo;Will AI replace us?&rdquo;</p>
<p>It&rsquo;s <strong>&ldquo;What will we do with the freedom AI creates?&rdquo;</strong></p>
<p>Tasks that used to take weeks or months of research, development, and iteration now take hours or days thanks to AI assistance.</p>
<p>Yes, there will be side effects:</p>
<ul>
<li>Layoffs</li>
<li>Reduced entry-level positions</li>
<li>Increased pressure on individual productivity</li>
<li>Market corrections</li>
</ul>
<p>But these aren&rsquo;t permanent states—they&rsquo;re transition periods. And the question for each of us is: <strong>How do we position ourselves to thrive in this transition?</strong></p>
<hr>
<h2 id="breaking-out-of-the-box">Breaking Out of the Box</h2>
<p>For years, I had confined myself to what I call a <strong>&ldquo;boxed developer&rdquo; mindset</strong>.</p>
<p>Everything revolved around:</p>
<ul>
<li>Kotlin</li>
<li>Android</li>
<li>Jetpack Compose</li>
</ul>
<p>I was good at it. I was comfortable. I could solve most Android problems thrown my way.</p>
<p>But I realized that comfort was becoming a <strong>cage</strong>.</p>
<h3 id="the-title-trap">The Title Trap</h3>
<p>We do this to ourselves, don&rsquo;t we? We accept titles and roles that become our entire identity:</p>
<ul>
<li>&ldquo;I&rsquo;m an Android Developer&rdquo;</li>
<li>&ldquo;I&rsquo;m a Frontend Engineer&rdquo;</li>
<li>&ldquo;I&rsquo;m a Backend Specialist&rdquo;</li>
</ul>
<p>These titles are useful for resumes and LinkedIn profiles. They help us find jobs. They give us a sense of expertise.</p>
<p>But they also <strong>limit our view</strong>. They keep us from looking beyond what our immediate work needs us to see.</p>
<h3 id="developer-vs-solutionist">Developer vs. Solutionist</h3>
<p>Here&rsquo;s the crucial distinction I&rsquo;ve discovered:</p>
<p>A <strong>developer</strong> writes code to win bread. They learn what they need to stay employed. They specialize to remain valuable. It&rsquo;s transactional.</p>
<p>A <strong>solutionist</strong> goes much deeper. They enjoy and learn every bit they can, like a magnet drawn to new knowledge. They&rsquo;re driven by curiosity and the joy of solving problems, not just by paychecks.</p>
<p>The developer asks: &ldquo;What do I need to know for my job?&rdquo;</p>
<p>The solutionist asks: &ldquo;What do I need to know to solve this problem effectively?&rdquo;</p>
<p>See the difference?</p>
<hr>
<h2 id="staying-relevant-build-continuously">Staying Relevant: Build Continuously</h2>
<p>This distinction matters immensely because once the AI tsunami completes its first wave, <strong>the next one is already forming</strong>.</p>
<p>Quantum computing advancements. More sophisticated AI models. New paradigms we haven&rsquo;t even conceived of yet.</p>
<p>Staying relevant through all of these requires constant adaptation and learning.</p>
<p>My proposition for navigating this? <strong>BUILD CONTINUOUSLY.</strong></p>
<h3 id="why-build-more">Why Build More?</h3>
<p>Because what better way to learn than building projects?</p>
<p>Not theoretical knowledge. Not tutorial hell. Not certifications that gather dust.</p>
<p><strong>Actual projects</strong> that solve real problems faced by real humans (even if that human is just you).</p>
<h3 id="the-compounding-effect-of-small-projects">The Compounding Effect of Small Projects</h3>
<p>Just like that simple billing calculator I mentioned earlier—you never know where a small project might lead.</p>
<p>After experiencing the joy of building that first solution, I built many more:</p>
<ul>
<li>A personal expense tracker (because existing ones didn&rsquo;t fit my mental model)</li>
<li>A community link-sharing platform (to solve coordination problems in my friend group)</li>
<li>A personal task management system (because I needed something simpler than Notion but more structured than notes)</li>
<li>Various automation scripts for repetitive tasks</li>
</ul>
<p>All of these are available on my website. Are they perfect? Absolutely not. Do they have bugs? Definitely. Are they going to change the world? Probably not.</p>
<p>But they&rsquo;ve changed <strong>my world</strong>.</p>
<h3 id="what-building-teaches-you">What Building Teaches You</h3>
<p>Building small things consistently <strong>transforms you</strong>:</p>
<ol>
<li>
<p><strong>You see problems differently</strong> - Instead of complaining about inefficiencies, you see opportunities to build solutions.</p>
</li>
<li>
<p><strong>You stop feeling stuck</strong> - There&rsquo;s always something you can build, always some small improvement you can make.</p>
</li>
<li>
<p><strong>You develop taste</strong> - You understand what makes software feel good to use because you&rsquo;re both the builder and the user.</p>
</li>
<li>
<p><strong>You become resilient</strong> - When things break (and they will), you learn to fix them. When approaches don&rsquo;t work, you pivot.</p>
</li>
<li>
<p><strong>You evolve unexpectedly</strong> - You slowly become someone you didn&rsquo;t know you could become. Skills compound. Confidence grows.</p>
</li>
</ol>
<hr>
<h2 id="so-what-is-a-solutionist">So, What Is a Solutionist?</h2>
<p>After all these stories and reflections, let me give you a concrete definition:</p>
<p><strong>A Solutionist isn&rsquo;t defined by tools, but by problems solved.</strong></p>
<p>They are:</p>
<ul>
<li>Language-agnostic</li>
<li>Framework-flexible</li>
<li>System-aware</li>
<li>Problem-focused</li>
</ul>
<h3 id="the-four-traits-of-a-solutionist">The Four Traits of a Solutionist</h3>
<p>Through my journey and observation of people I admire, I&rsquo;ve identified four core traits:</p>
<h4 id="1-empathy--understanding-people-not-just-specs">1. Empathy — Understanding People, Not Just Specs</h4>
<p>The best solutions come from understanding the <strong>human problem</strong>, not just the technical specification.</p>
<p>This means:</p>
<ul>
<li>Putting yourself in the user&rsquo;s shoes</li>
<li>Understanding their frustrations, not just their feature requests</li>
<li>Recognizing that &ldquo;make it faster&rdquo; often means &ldquo;I feel frustrated waiting&rdquo;</li>
<li>Seeing the emotional journey, not just the user journey</li>
</ul>
<p>When I built that billing calculator, I wasn&rsquo;t solving a math problem. I was solving the <strong>stress and anxiety</strong> of my colleagues during rush hours.</p>
<h4 id="2-systems-thinking--seeing-ripple-effects">2. Systems Thinking — Seeing Ripple Effects</h4>
<p>Nothing exists in isolation. One change ripples through everything.</p>
<p>Systems thinking means:</p>
<ul>
<li>Understanding dependencies and consequences</li>
<li>Recognizing that &ldquo;fixing&rdquo; one thing might break another</li>
<li>Seeing the whole forest, not just the tree you&rsquo;re debugging</li>
<li>Appreciating long-term implications of short-term decisions</li>
</ul>
<p>In Android development, this might mean understanding how a seemingly innocent change in a shared ViewModel might affect multiple screens, or how a database migration could impact app startup time.</p>
<h4 id="3-lifelong-learning--venturing-into-the-unknown-without-fear">3. Lifelong Learning — Venturing Into the Unknown Without Fear</h4>
<p>The moment you stop learning, you start becoming obsolete.</p>
<p>Lifelong learning means:</p>
<ul>
<li>Being comfortable with being a beginner again and again</li>
<li>Exploring technologies outside your comfort zone</li>
<li>Reading papers, essays, and documentation for things you don&rsquo;t &ldquo;need&rdquo; yet</li>
<li>Understanding that every new skill multiplies with every previous skill you&rsquo;ve learned</li>
</ul>
<p>From SSLC certificate to Android Developer to exploring AI/ML, backend systems, DevOps—each layer adds new dimensions to how you solve problems.</p>
<h4 id="4-communication--bridging-technical-and-non-technical-minds">4. Communication — Bridging Technical and Non-Technical Minds</h4>
<p>The best solution is useless if no one understands it.</p>
<p>Communication means:</p>
<ul>
<li>Explaining complex technical concepts to non-technical stakeholders</li>
<li>Writing documentation that actually helps</li>
<li>Listening actively to understand requirements beneath the surface</li>
<li>Collaborating effectively across disciplines—design, product, business</li>
</ul>
<p>I&rsquo;ve seen brilliant solutions fail because they were explained poorly. And I&rsquo;ve seen simple solutions succeed because they were communicated effectively.</p>
<hr>
<h2 id="ai-as-freedom-russells-prophecy-coming-true">AI as Freedom: Russell&rsquo;s Prophecy Coming True</h2>
<p>When AI arrived, many feared job loss. And yes, that&rsquo;s happening in some sectors.</p>
<p>But what if Russell was right all along?</p>
<h3 id="what-ai-actually-removes">What AI Actually Removes</h3>
<p>AI removes:</p>
<ul>
<li>Repetitive, mechanical work</li>
<li>Boilerplate that makes our days feel routine</li>
<li>The cognitive load of remembering syntax</li>
<li>The tedium of debugging simple logic errors</li>
</ul>
<p>It gives time back for:</p>
<ul>
<li>Creativity</li>
<li>Empathy</li>
<li>Real problem-solving</li>
<li>Strategic thinking</li>
<li>Learning deeply instead of superficially</li>
</ul>
<h3 id="vibe-coding-thanks-to-andrej-karpathy">Vibe Coding: Thanks to Andrej Karpathy</h3>
<p>This is where <strong>&lsquo;vibe coding&rsquo;</strong> comes in—a term popularized by Andrej Karpathy.</p>
<p>Let AI help you move faster. Let it handle the repetition, the boilerplate, the syntax you can never quite remember.</p>
<p>But—and this is crucial—<strong>understand what it&rsquo;s changing</strong>.</p>
<p>Know the patterns. Understand the tweaks. Grasp the reasoning behind every line it suggests.</p>
<h3 id="the-danger-of-blind-coding">The Danger of Blind Coding</h3>
<p>If you code blindly, just accepting whatever AI suggests without understanding:</p>
<ul>
<li>You lose the joy of building</li>
<li>You become oblivious to your own vision</li>
<li>You can&rsquo;t debug effectively when things inevitably break</li>
<li>You stop learning and start copy-pasting</li>
</ul>
<p>AI should be your <strong>copilot</strong>, not your pilot. You&rsquo;re still flying the plane.</p>
<hr>
<h2 id="how-to-actually-start-practical-steps">How to Actually Start: Practical Steps</h2>
<p>Enough philosophy. Let&rsquo;s talk practically.</p>
<p>Want to build something? Here&rsquo;s what I&rsquo;ve learned through building multiple projects with AI assistance:</p>
<h3 id="step-1-start-with-the-problem">Step 1: Start with the Problem</h3>
<p>Write it down. Not the solution—<strong>the problem</strong>.</p>
<p>Ask yourself:</p>
<ul>
<li>What are you trying to solve?</li>
<li>Who are you solving it for?</li>
<li>Why does this problem matter?</li>
<li>What does success look like?</li>
</ul>
<p>Be specific. &ldquo;I want to build a todo app&rdquo; is not a problem. &ldquo;I keep forgetting to follow up on important emails, and existing todo apps don&rsquo;t integrate well with my email workflow&rdquo; is a problem.</p>
<h3 id="step-2-document-your-vision-first">Step 2: Document Your Vision First</h3>
<p>Before you write a single line of code, create a <strong>clear vision</strong> of what you&rsquo;re building and why.</p>
<p>Your first commit should be your vision, not your code.</p>
<p>This document becomes your North Star. When AI suggests clever solutions that drift from your vision, you can catch it. When scope creep tempts you, you can resist it.</p>
<h3 id="step-3-use-ai-to-move-fast-but-understand-everything">Step 3: Use AI to Move Fast, But Understand Everything</h3>
<p>This is the balance:</p>
<ul>
<li>Let AI generate boilerplate → But understand the structure it creates</li>
<li>Let AI suggest solutions → But understand why those solutions work</li>
<li>Let AI write tests → But understand what they&rsquo;re testing and why</li>
</ul>
<p>Know the patterns. Understand the tweaks. Grasp the reasoning behind every line.</p>
<p>If you find yourself accepting suggestions without understanding them, <strong>stop</strong>. Research. Learn. Only then proceed.</p>
<h3 id="step-4-iterate-deliberately">Step 4: Iterate Deliberately</h3>
<p>Add features <strong>one at a time</strong>.</p>
<p>Track changes with Git. See what&rsquo;s happening at each step.</p>
<p>This isn&rsquo;t just about version control—it&rsquo;s about <strong>understanding the evolution</strong> of your project. Each commit is a story. Each feature is a chapter.</p>
<p>Deliberate iteration means:</p>
<ul>
<li>Build one thing</li>
<li>Test it thoroughly</li>
<li>Document what you learned</li>
<li>Commit it</li>
<li>Move to the next thing</li>
</ul>
<h3 id="step-5-the-tools-dont-matter-the-mindset-does">Step 5: The Tools Don&rsquo;t Matter. The Mindset Does.</h3>
<p>I cannot stress this enough.</p>
<p>The language doesn&rsquo;t matter. The framework doesn&rsquo;t matter. The cloud provider doesn&rsquo;t matter.</p>
<p>What matters is:</p>
<ul>
<li>Do you understand the problem?</li>
<li>Does your solution solve it effectively?</li>
<li>Can you explain why you made the choices you made?</li>
<li>Did you learn something building it?</li>
</ul>
<p>That&rsquo;s all that matters.</p>
<hr>
<h2 id="the-real-power-wearing-all-the-hats">The Real Power: Wearing All the Hats</h2>
<p>When you build your own projects, you&rsquo;re not just a developer.</p>
<p>You&rsquo;re everything:</p>
<ul>
<li><strong>Visionary</strong> — Defining what should exist</li>
<li><strong>Product Manager</strong> — Prioritizing features</li>
<li><strong>Designer</strong> — Crafting the experience</li>
<li><strong>Developer</strong> — Building the solution</li>
<li><strong>QA</strong> — Testing and breaking things</li>
<li><strong>DevOps</strong> — Deploying and maintaining</li>
<li><strong>Support</strong> — Handling bugs and feedback</li>
</ul>
<h3 id="the-freedom-this-gives-you">The Freedom This Gives You</h3>
<p>Maybe you&rsquo;ll build something the world uses. Maybe not.</p>
<p>Either way, <strong>it&rsquo;s yours</strong>. It&rsquo;s your journey.</p>
<p>In a typical software job, you might not get to wear all these hats due to company policies and defined roles. You&rsquo;re the &ldquo;backend developer&rdquo; or the &ldquo;mobile engineer&rdquo; or the &ldquo;frontend specialist.&rdquo;</p>
<p>But here, on your own project, you are the <strong>boss</strong>. The founder. The everything.</p>
<p>This level of ownership is both daunting and incredibly empowering.</p>
<h3 id="the-career-impact">The Career Impact</h3>
<p>I&rsquo;m certain that this experience—building complete projects from vision to deployment—will help you solve day-to-day professional problems much faster.</p>
<p>Because you&rsquo;ve seen the whole picture. You understand how the pieces fit together. You&rsquo;ve debugged across the stack. You&rsquo;ve made tradeoffs and lived with the consequences.</p>
<p>That holistic understanding is <strong>invaluable</strong> and increasingly rare in our specialized world.</p>
<hr>
<h2 id="start-today-your-action-items">Start Today: Your Action Items</h2>
<p>Don&rsquo;t wait. Don&rsquo;t prepare. Don&rsquo;t plan endlessly.</p>
<p><strong>Start small. Start now.</strong></p>
<h3 id="pick-one-frustrating-problem">Pick One Frustrating Problem</h3>
<p>Look at your daily life. What frustrates you repeatedly?</p>
<ul>
<li>Tracking expenses?</li>
<li>Managing reading lists?</li>
<li>Coordinating with friends?</li>
<li>Remembering to water plants?</li>
<li>Finding recipes based on ingredients you have?</li>
</ul>
<p>Pick <strong>one</strong>. Just one.</p>
<h3 id="dont-chase-perfection-chase-progress">Don&rsquo;t Chase Perfection. Chase Progress.</h3>
<p>Your first version will be ugly. It will have bugs. It will be embarrassing to show others.</p>
<p><strong>Build it anyway.</strong></p>
<p>Because version 0.1 is infinitely better than the perfect app that exists only in your imagination.</p>
<h3 id="use-every-project-as-a-playground">Use Every Project as a Playground</h3>
<p>Want to learn a new framework? Use it for your next project.</p>
<p>Curious about a new database? Try it.</p>
<p>Interested in a new deployment strategy? Experiment.</p>
<p>Every project is an opportunity to <strong>play</strong>, to <strong>experiment</strong>, to <strong>learn</strong>.</p>
<h3 id="every-problem-solved-is-progress">Every Problem Solved Is Progress</h3>
<p>Each problem solved is one step closer to being a Solutionist.</p>
<p>Not because you&rsquo;re accumulating credentials or certificates, but because you&rsquo;re <strong>training yourself to see opportunities where others see obstacles</strong>.</p>
<hr>
<h2 id="closing-reclaiming-purpose">Closing: Reclaiming Purpose</h2>
<p>When my manager first sent me &ldquo;In Praise of Idleness,&rdquo; I thought it was ironic. Here we are, working in tech, and he&rsquo;s sending me an essay about working less?</p>
<p>Now I see the wisdom.</p>
<p>It&rsquo;s not about doing less. It&rsquo;s about <strong>doing better</strong>.</p>
<p>It&rsquo;s about focusing on work that matters, that challenges us, that makes us grow.</p>
<h3 id="what-ai-cant-replace">What AI Can&rsquo;t Replace</h3>
<p>We&rsquo;ve built technology powerful enough to automate tasks, but not powerful enough to replace people who bring <strong>meaning</strong> to them.</p>
<p>AI can:</p>
<ul>
<li>Write code</li>
<li>Debug programs</li>
<li>Architect systems</li>
<li>Generate tests</li>
<li>Refactor codebases</li>
</ul>
<p>But it can&rsquo;t:</p>
<ul>
<li>Understand <strong>why</strong> we build</li>
<li>Feel <strong>empathy</strong> for users</li>
<li>Ask deeper questions about <strong>impact</strong></li>
<li>Navigate the <strong>human</strong> complexities of real projects</li>
<li>Find <strong>purpose</strong> in the work</li>
</ul>
<p>That&rsquo;s still our job. <strong>Always will be.</strong></p>
<h3 id="the-world-needs-solutionists">The World Needs Solutionists</h3>
<p>The world doesn&rsquo;t just need coders. We have enough people who can write syntactically correct code.</p>
<p>The world needs people who:</p>
<ul>
<li>See problems and craft thoughtful solutions</li>
<li>Combine empathy with engineering</li>
<li>Explore fearlessly and learn relentlessly</li>
<li>Ask &ldquo;why&rdquo; as often as they ask &ldquo;how&rdquo;</li>
<li>Reclaim purpose in a world obsessed with productivity</li>
</ul>
<h3 id="dont-confine-yourself-to-a-box">Don&rsquo;t Confine Yourself to a Box</h3>
<p>So don&rsquo;t confine yourself to a box labeled &ldquo;Android Developer&rdquo; or &ldquo;Frontend Engineer&rdquo; or &ldquo;Data Scientist.&rdquo;</p>
<p><strong>Explore.</strong> Venture into the unknown.</p>
<p><strong>Build.</strong> Create things that matter to you.</p>
<p><strong>Reflect.</strong> Learn from what works and what doesn&rsquo;t.</p>
<p><strong>Be a Solutionist.</strong></p>
<p>Because the world doesn&rsquo;t just need more developers.</p>
<p><strong>It needs you.</strong></p>
<hr>
<h2 id="thank-you">Thank You</h2>
<p>First and foremost, a huge thank you to <strong>Google Developer Group Chennai</strong> for organizing DevFest2025 and giving me this incredible platform to share my journey and thoughts with the community.</p>
<p>To all the <strong>volunteers</strong> who worked tirelessly behind the scenes—setting up the venue, managing logistics, helping attendees, and ensuring everything ran smoothly—you are the unsung heroes who make events like this possible. Your dedication and energy created the welcoming atmosphere that made DevFest2025 special.</p>
<p>A special thanks to all the <strong>sponsors</strong> who believed in this event and invested in our developer community. Your support makes it possible for us to gather, learn, share, and grow together.</p>
<p>To the <strong>GDG Chennai organizers and core team members</strong>—thank you for building and nurturing this vibrant tech community in Chennai. Your consistent efforts to bring developers together, facilitate learning, and create opportunities for knowledge sharing are truly invaluable.</p>
<p>To everyone who <strong>attended the talk</strong> and engaged with these ideas—your questions, your reflections, your nodding heads, and even your skeptical looks made this talk so much more meaningful. The conversations we had afterward were some of the most enriching parts of the entire experience.</p>
<p>And to you, reading this now—whether you attended DevFest2025 or are discovering this for the first time—I hope something here resonates. I hope it sparks something in you.</p>
<h2 id="now-lets-build-something"><strong>Now let&rsquo;s build something.</strong></h2>
<h2 id="sone-of-the-feedbacks-from-participants">Sone of the feedbacks from Participants</h2>
<p><img alt="Feedbacks from participants" loading="lazy" src="/img/devfest_2025/updated-feedbacks-10-11-2025.png">
<em>Heartwarming feedback from the DevFest2025 participants</em></p>
<hr>
<h2 id="event-highlights">Event Highlights</h2>
<p>Here are some moments from DevFest2025:</p>
<p><img alt="Speaker Announcement" loading="lazy" src="/img/devfest_2025/speeker-annoncement-screenshot.png">
<em>The announcement that started it all</em></p>
<p><img alt="Speaking at DevFest2025" loading="lazy" src="/img/devfest_2025/talking-at-the-event.jpeg">
<em>Sharing the Solutionist Mindset with the DevFest2025 community</em></p>
<p><img alt="Speaking at the Platform" loading="lazy" src="/img/devfest_2025/speaking-at-the-platform-01.JPG">
<em>Presenting the Solutionist Mindset on stage</em></p>
<p><img alt="Speaking at the Stage" loading="lazy" src="/img/devfest_2025/speaking-at-the-stage.JPG">
<em>Engaging with the audience during the talk</em></p>
<p><img alt="DevFest 2025 Moment" loading="lazy" src="/img/devfest_2025/DevFest%2025%20(1).JPG">
<em>A memorable moment from DevFest2025</em></p>
<p><img alt="All the Volunteers" loading="lazy" src="/img/devfest_2025/DevFest%2025%20SEP%200461.-all-the%20voluntters-who-made-it-possible.JPG">
<em>All the amazing volunteers who made DevFest2025 possible - couldn&rsquo;t have done it without you!</em></p>
<p><img alt="Speaker Goodies" loading="lazy" src="/img/devfest_2025/speaker-goddies.jpeg">
<em>The speaker goodies bag - thanks GDG Chennai! For everyone who asked what&rsquo;s inside: GDG swag, stickers, and some awesome tech goodies!</em></p>
<hr>
<p><em>If you want to discuss any of these ideas further, or if you&rsquo;re building something and want to share your journey, feel free to reach out. I&rsquo;m always excited to connect with fellow solutionists.</em></p>
<p><em>All my projects are available on my website. They&rsquo;re not perfect, but they&rsquo;re mine, and they&rsquo;re continuously evolving. Just like me. Just like you.</em></p>
]]></content:encoded></item><item><title>Introduction to Nginx: Hosting Your First Website</title><link>https://md.eknath.dev/posts/software-development/introduction-to-nginx/</link><pubDate>Sat, 13 Sep 2025 12:00:00 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/introduction-to-nginx/</guid><description>&lt;p>So you have a server, and now you want to share your project with the world. To do that, you need a web server. Nginx (pronounced &amp;ldquo;Engine-X&amp;rdquo;) is one of the most popular, powerful, and efficient web servers available. It&amp;rsquo;s famous for its high performance and its ability to also act as a reverse proxy, but at its core, it&amp;rsquo;s brilliant at serving web pages.&lt;/p>
&lt;p>This guide will walk you through the basics of Nginx, explaining its structure and showing you how to set up your very first website.&lt;/p></description><content:encoded><![CDATA[<p>So you have a server, and now you want to share your project with the world. To do that, you need a web server. Nginx (pronounced &ldquo;Engine-X&rdquo;) is one of the most popular, powerful, and efficient web servers available. It&rsquo;s famous for its high performance and its ability to also act as a reverse proxy, but at its core, it&rsquo;s brilliant at serving web pages.</p>
<p>This guide will walk you through the basics of Nginx, explaining its structure and showing you how to set up your very first website.</p>
<h2 id="1-installation">1. Installation</h2>
<p>If you haven&rsquo;t already, you can install Nginx on any Debian-based system (like Ubuntu) with a single command. It&rsquo;s also good practice to ensure your firewall allows web traffic.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install Nginx</span>
</span></span><span style="display:flex;"><span>sudo apt update
</span></span><span style="display:flex;"><span>sudo apt install nginx -y
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Allow Nginx through the firewall</span>
</span></span><span style="display:flex;"><span>sudo ufw allow <span style="color:#e6db74">&#39;Nginx Full&#39;</span>
</span></span></code></pre></div><p>To check that it&rsquo;s running, you can use <code>systemctl</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-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl status nginx
</span></span></code></pre></div><p>If it&rsquo;s active, you can visit your server&rsquo;s IP address in a web browser, and you should see the default &ldquo;Welcome to Nginx!&rdquo; page.</p>
<h2 id="2-understanding-the-nginx-directory-structure">2. Understanding the Nginx Directory Structure</h2>
<p>Nginx&rsquo;s configuration can seem intimidating, but it&rsquo;s very logical. All the important files live in <code>/etc/nginx/</code>.</p>
<ul>
<li><code>/etc/nginx/nginx.conf</code>: The main configuration file. You will rarely need to edit this file directly.</li>
<li><code>/etc/nginx/sites-available/</code>: This is where you will store the configuration files for each of your websites. Think of it as a library of all possible sites you <em>could</em> host.</li>
<li><code>/etc/nginx/sites-enabled/</code>: This directory contains symbolic links (shortcuts) to the files in <code>sites-available</code>. Nginx only reads the configurations in this <code>sites-enabled</code> directory. This setup allows you to easily turn websites on and off without deleting their configuration.</li>
</ul>
<h2 id="3-setting-up-your-first-site-a-server-block">3. Setting Up Your First Site (A Server Block)</h2>
<p>Let&rsquo;s host a website for a domain you own, <code>your-domain.com</code>. First, ensure you have pointed your domain to your server&rsquo;s IP address using an <code>A</code> record at your domain registrar.</p>
<h3 id="step-1-create-a-home-for-your-website">Step 1: Create a Home for Your Website</h3>
<p>It&rsquo;s standard practice to store website files in the <code>/var/www/</code> directory.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Create a directory for your domain</span>
</span></span><span style="display:flex;"><span>sudo mkdir -p /var/www/your-domain.com/html
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Assign ownership to your current user so you can edit files easily</span>
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /var/www/your-domain.com/html
</span></span></code></pre></div><h3 id="step-2-create-a-sample-page">Step 2: Create a Sample Page</h3>
<p>Let&rsquo;s create a simple <code>index.html</code> file for Nginx to serve.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&lt;h1&gt;Success! The your-domain.com server block is working!&lt;/h1&gt;&#34;</span> &gt; /var/www/your-domain.com/html/index.html
</span></span></code></pre></div><h3 id="step-3-create-the-nginx-configuration-file">Step 3: Create the Nginx Configuration File</h3>
<p>Nginx calls the configuration for a single site a &ldquo;server block&rdquo;. We will create a new file in <code>sites-available</code> for our domain.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nano /etc/nginx/sites-available/your-domain.com
</span></span></code></pre></div><p>Paste in the following configuration. Each line is explained by the comments.</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#75715e"># This defines a server
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Listen on port 80 (standard HTTP) for both IPv4 and IPv6
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># The root directory where the website files are stored
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/your-domain.com/html</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># The order of files to look for when a request comes in
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span> <span style="color:#e6db74">index.htm</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># The domain names this server block should respond to
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">your-domain.com</span> <span style="color:#e6db74">www.your-domain.com</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># This block handles how to find files
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e"># Try to serve the requested file, then a directory, or else show a 404 error
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#f92672">try_files</span> $uri $uri/ =<span style="color:#ae81ff">404</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="step-4-enable-your-site">Step 4: Enable Your Site</h3>
<p>Now, we need to tell Nginx to actually use this configuration. We do this by creating a symbolic link to the file in the <code>sites-enabled</code> directory.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/
</span></span></code></pre></div><h3 id="step-5-test-and-restart-nginx">Step 5: Test and Restart Nginx</h3>
<p>It&rsquo;s very important to test your configuration file for syntax errors before restarting Nginx.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nginx -t
</span></span></code></pre></div><p>If you see <code>syntax is ok</code> and <code>test is successful</code>, you are good to go. Restart Nginx to apply the changes.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl restart nginx
</span></span></code></pre></div><p>That&rsquo;s it! If you now visit <code>http://your-domain.com</code> in your browser, you will see your &ldquo;Success!&rdquo; message instead of the default Nginx welcome page.</p>
<h2 id="4-whats-next">4. What&rsquo;s Next?</h2>
<p>You have successfully configured Nginx to host a basic website. This is the foundation for hosting any web project. From here, you can:</p>
<ul>
<li><strong>Host multiple websites</strong> on the same server by creating a new directory and a new server block file for each domain.</li>
<li><strong>Set up a reverse proxy</strong> to pass traffic to applications running on different ports (like Node.js or Python apps).</li>
<li><strong>Secure your sites with SSL/TLS</strong> using a free tool like Certbot to enable HTTPS.</li>
</ul>
<p>These more advanced topics are covered in our &ldquo;Advanced VPS Guide: Mastering Nginx, Subdomains, and Reverse Proxies&rdquo;.</p>
]]></content:encoded></item><item><title>A Beginner's Guide to Your First Hour on a Linux VPS</title><link>https://md.eknath.dev/posts/software-development/beginners-guide-to-vps-setup/</link><pubDate>Sat, 13 Sep 2025 11:00:00 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/beginners-guide-to-vps-setup/</guid><description>&lt;p>Congratulations on your new Virtual Private Server (VPS)! This is your own private space on the internet, a blank canvas ready for your projects. That freedom can also be a bit intimidating. What should you do first?&lt;/p>
&lt;p>This guide is designed for the absolute beginner. We will walk through the first essential steps to secure your server, get it ready for projects, and host your first website securely with Nginx and a free SSL certificate.&lt;/p></description><content:encoded><![CDATA[<p>Congratulations on your new Virtual Private Server (VPS)! This is your own private space on the internet, a blank canvas ready for your projects. That freedom can also be a bit intimidating. What should you do first?</p>
<p>This guide is designed for the absolute beginner. We will walk through the first essential steps to secure your server, get it ready for projects, and host your first website securely with Nginx and a free SSL certificate.</p>
<h2 id="1-your-first-login-the-root-user">1. Your First Login: The Root User</h2>
<p>Your hosting provider will give you an IP address (e.g., <code>123.45.67.89</code>) and a password for the <code>root</code> user. The <code>root</code> user is the super-administrator with unlimited power. It&rsquo;s powerful, but also risky to use for everyday tasks.</p>
<h3 id="step-1-connect-to-the-server">Step 1: Connect to the Server</h3>
<p>Open a terminal on your computer and connect to your server using the <code>ssh</code> command.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh root@YOUR_SERVER_IP
</span></span></code></pre></div><h3 id="step-2-change-the-root-password">Step 2: Change the Root Password</h3>
<p>If your provider gave you a temporary password, your first action should be to change it to something strong and unique.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>passwd
</span></span></code></pre></div><h2 id="2-create-your-own-user-account">2. Create Your Own User Account</h2>
<p>This is the single most important security step. We will create a standard user account and give it administrative privileges using the <code>sudo</code> command.</p>
<h3 id="step-1-create-the-new-user">Step 1: Create the New User</h3>
<p>Replace <code>devuser</code> with a username you like.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>adduser devuser
</span></span></code></pre></div><h3 id="step-2-grant-administrative-privileges">Step 2: Grant Administrative Privileges</h3>
<p>Add your new user to the <code>sudo</code> group, which allows it to run commands as the administrator.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>usermod -aG sudo devuser
</span></span></code></pre></div><h3 id="step-3-log-in-as-your-new-user">Step 3: Log in as Your New User</h3>
<p>Log out of the root account (<code>exit</code>) and log back in with your new credentials.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh devuser@YOUR_SERVER_IP
</span></span></code></pre></div><p>From now on, you should <strong>always</strong> log in with this user.</p>
<h2 id="3-update-system-and-install-essential-tools">3. Update System and Install Essential Tools</h2>
<p>Now that you are logged in as your new <code>sudo</code> user, the first actions are to update the server and install some essential tools.</p>
<h3 id="step-1-update-the-system">Step 1: Update the System</h3>
<p>This command downloads the latest list of available software (<code>update</code>) and then installs those updates (<code>upgrade</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-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt update <span style="color:#f92672">&amp;&amp;</span> sudo apt upgrade -y
</span></span></code></pre></div><h3 id="step-2-install-basic-tools">Step 2: Install Basic Tools</h3>
<p>Next, install a few common and useful tools.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt install curl git vim htop unzip nginx -y
</span></span></code></pre></div><p>We include <strong>Nginx</strong> here as it is a lightweight, high-performance web server we will configure later.</p>
<h2 id="4-set-your-timezone">4. Set Your Timezone</h2>
<p>Correct server time is important for logs and many applications. Find your timezone from the list and set it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Find your timezone (press &#39;q&#39; to quit the list)</span>
</span></span><span style="display:flex;"><span>timedatectl list-timezones
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Set your timezone (replace with your own)</span>
</span></span><span style="display:flex;"><span>sudo timedatectl set-timezone Asia/Kolkata
</span></span></code></pre></div><h2 id="5-basic-firewall-setup">5. Basic Firewall Setup</h2>
<p>A firewall is a digital security guard. We will use <code>ufw</code> (Uncomplicated Firewall) to block unwanted traffic.</p>
<h3 id="step-1-allow-essential-traffic">Step 1: Allow Essential Traffic</h3>
<p><strong>This is critical:</strong> You must allow SSH traffic, otherwise the firewall will lock you out. We will also allow Nginx traffic.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ufw allow OpenSSH
</span></span><span style="display:flex;"><span>sudo ufw allow <span style="color:#e6db74">&#39;Nginx Full&#39;</span>
</span></span></code></pre></div><p>&lsquo;Nginx Full&rsquo; allows traffic on both HTTP (port 80) and HTTPS (port 443).</p>
<h3 id="step-2-enable-the-firewall">Step 2: Enable the Firewall</h3>
<p>Now, turn the firewall on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ufw enable
</span></span></code></pre></div><h2 id="6-host-a-website-with-nginx">6. Host a Website with Nginx</h2>
<p>Now we will configure Nginx to serve a website for a domain you own.</p>
<h3 id="step-0-point-your-domain-to-the-server-dns">Step 0: Point Your Domain to the Server (DNS)</h3>
<p>Before Nginx can work, you have to tell the internet that your domain should point to your server&rsquo;s IP address. You do this at your <strong>domain registrar</strong> (the company where you bought your domain, like GoDaddy, Namecheap, Google Domains, etc.).</p>
<ol>
<li>Log in to your domain registrar&rsquo;s website.</li>
<li>Find the DNS management page for your domain.</li>
<li>You need to create two <strong>&lsquo;A&rsquo; records</strong>:
<ul>
<li><strong>Record 1 (Root Domain):</strong>
<ul>
<li><strong>Type:</strong> <code>A</code></li>
<li><strong>Host/Name:</strong> <code>@</code> (this symbol represents the root domain itself)</li>
<li><strong>Value/Points to:</strong> Your server&rsquo;s IP address (e.g., <code>123.45.67.89</code>)</li>
<li><strong>TTL (Time to Live):</strong> Leave as default (often 1 hour).</li>
</ul>
</li>
<li><strong>Record 2 (www subdomain):</strong>
<ul>
<li><strong>Type:</strong> <code>A</code></li>
<li><strong>Host/Name:</strong> <code>www</code></li>
<li><strong>Value/Points to:</strong> Your server&rsquo;s IP address (the same one)</li>
<li><strong>TTL:</strong> Leave as default.</li>
</ul>
</li>
</ul>
</li>
</ol>
<p>DNS changes can take anywhere from a few minutes to a few hours to update across the internet.</p>
<h3 id="step-1-create-a-directory-for-your-site">Step 1: Create a Directory for Your Site</h3>
<p>We&rsquo;ll store our website&rsquo;s files in the <code>/var/www</code> directory.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Create a directory for your domain</span>
</span></span><span style="display:flex;"><span>sudo mkdir -p /var/www/your-domain.com/html
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Assign ownership to your user</span>
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /var/www/your-domain.com/html
</span></span></code></pre></div><h3 id="step-2-create-a-sample-page">Step 2: Create a Sample Page</h3>
<p>Let&rsquo;s create a simple <code>index.html</code> file for testing.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&lt;h1&gt;Hello from my Nginx Site!&lt;/h1&gt;&#34;</span> | sudo tee /var/www/your-domain.com/html/index.html
</span></span></code></pre></div><h3 id="step-3-create-an-nginx-server-block">Step 3: Create an Nginx Server Block</h3>
<p>Nginx uses &ldquo;server block&rdquo; files to know how to handle incoming domains. We&rsquo;ll create one for our domain.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nano /etc/nginx/sites-available/your-domain.com
</span></span></code></pre></div><p>Paste the following configuration into the file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/your-domain.com/html</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">your-domain.com</span> <span style="color:#e6db74">www.your-domain.com</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">try_files</span> $uri $uri/ =<span style="color:#ae81ff">404</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="step-4-enable-the-server-block">Step 4: Enable the Server Block</h3>
<p>We enable the site by creating a symbolic link from this file into the <code>sites-enabled</code> directory.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/
</span></span></code></pre></div><p>Now, test your Nginx configuration for errors and restart it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nginx -t
</span></span><span style="display:flex;"><span>sudo systemctl restart nginx
</span></span></code></pre></div><p>Once your DNS has updated, if you visit <code>http://your-domain.com</code> in a browser, you should see your &ldquo;Hello World&rdquo; message.</p>
<h2 id="7-secure-your-site-with-ssl-https">7. Secure Your Site with SSL (HTTPS)</h2>
<p>To secure your site, we&rsquo;ll get a free SSL certificate from Let&rsquo;s Encrypt using a tool called Certbot.</p>
<h3 id="step-1-install-certbot">Step 1: Install Certbot</h3>
<p>Certbot has a dedicated Nginx plugin that makes the process simple.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt install certbot python3-certbot-nginx -y
</span></span></code></pre></div><h3 id="step-2-obtain-the-ssl-certificate">Step 2: Obtain the SSL Certificate</h3>
<p>Run Certbot. It will read your Nginx configuration, see the domain you just set up, and guide you through obtaining the certificate.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo certbot --nginx
</span></span></code></pre></div><p>Certbot will ask for your email, for you to agree to the terms of service, and if you want to redirect all HTTP traffic to HTTPS. It is highly recommended to choose the redirect option.</p>
<h3 id="step-3-verify-auto-renewal">Step 3: Verify Auto-Renewal</h3>
<p>Let&rsquo;s Encrypt certificates are valid for 90 days. Certbot automatically sets up a task to renew them for you. You can test the renewal process with a dry run.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo certbot renew --dry-run
</span></span></code></pre></div><p>If this runs without errors, you are all set. Your site is now secure and accessible via <code>https://your-domain.com</code>.</p>
<p><strong>To add a subdomain</strong> (e.g., <code>blog.your-domain.com</code>), you simply repeat the process: create a new directory in <code>/var/www</code>, create a new server block file in <code>sites-available</code>, enable it, and then run <code>sudo certbot --nginx</code> again.</p>
<h2 id="8-quick-tips-and-best-practices">8. Quick Tips and Best Practices</h2>
<ul>
<li>
<p><strong>Use SSH Keys:</strong> Passwords can be cracked. SSH keys are a much more secure way to log in. This is a highly recommended next security step.</p>
</li>
<li>
<p><strong>Keep Your System Updated:</strong> Get in the habit of running <code>sudo apt update &amp;&amp; sudo apt upgrade -y</code> at least once a week.</p>
</li>
<li>
<p><strong>Be Careful with Commands:</strong> Don&rsquo;t run scripts or commands from the internet without understanding what they do.</p>
</li>
<li>
<p><strong>Use Strong Passwords:</strong> The password for your <code>sudo</code> user should be strong and unique.</p>
</li>
</ul>
]]></content:encoded></item><item><title>How to Set Up Telegram Alerts for SSH Logins on Debian</title><link>https://md.eknath.dev/posts/software-development/telegram-ssh-login-alerts/</link><pubDate>Sat, 13 Sep 2025 10:00:00 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/telegram-ssh-login-alerts/</guid><description>&lt;p>Securing your Virtual Private Server (VPS) is critical. A simple and effective way to monitor access is to receive real-time notifications for every successful SSH login. This guide provides a step-by-step walkthrough for setting up instant Telegram alerts on a Debian-based server, giving you immediate awareness of all shell access.&lt;/p>
&lt;h2 id="1-prerequisites">1. Prerequisites&lt;/h2>
&lt;p>Before you begin, ensure you have the following:&lt;/p>
&lt;ul>
&lt;li>A server running a Debian-based Linux distribution.&lt;/li>
&lt;li>&lt;code>sudo&lt;/code> or &lt;code>root&lt;/code> access to the server.&lt;/li>
&lt;li>A Telegram account.&lt;/li>
&lt;li>&lt;code>curl&lt;/code> and &lt;code>jq&lt;/code> installed on your server. If you don&amp;rsquo;t have them, install them now:
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>sudo apt-get update &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> sudo apt-get install -y curl jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h2 id="2-create-a-telegram-bot">2. Create a Telegram Bot&lt;/h2>
&lt;p>Your alerts will be sent by a Telegram Bot.&lt;/p></description><content:encoded><![CDATA[<p>Securing your Virtual Private Server (VPS) is critical. A simple and effective way to monitor access is to receive real-time notifications for every successful SSH login. This guide provides a step-by-step walkthrough for setting up instant Telegram alerts on a Debian-based server, giving you immediate awareness of all shell access.</p>
<h2 id="1-prerequisites">1. Prerequisites</h2>
<p>Before you begin, ensure you have the following:</p>
<ul>
<li>A server running a Debian-based Linux distribution.</li>
<li><code>sudo</code> or <code>root</code> access to the server.</li>
<li>A Telegram account.</li>
<li><code>curl</code> and <code>jq</code> installed on your server. If you don&rsquo;t have them, install them now:
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt-get update <span style="color:#f92672">&amp;&amp;</span> sudo apt-get install -y curl jq
</span></span></code></pre></div></li>
</ul>
<h2 id="2-create-a-telegram-bot">2. Create a Telegram Bot</h2>
<p>Your alerts will be sent by a Telegram Bot.</p>
<h3 id="step-1-talk-to-botfather">Step 1: Talk to BotFather</h3>
<p>In your Telegram app, search for the verified <strong>BotFather</strong> account and start a chat.</p>
<h3 id="step-2-create-the-bot">Step 2: Create the Bot</h3>
<p>Send the <code>/newbot</code> command. Follow the prompts to give your bot a display name and a unique username, which must end in <code>bot</code>.</p>
<h3 id="step-3-save-the-api-token">Step 3: Save the API Token</h3>
<p>Once created, BotFather will provide a secret <strong>API token</strong>. Copy this token and keep it secure; you will need it in the next steps.</p>
<h2 id="3-get-your-personal-chat-id">3. Get Your Personal Chat ID</h2>
<p>The bot needs to know where to send the alerts. This will be your personal Telegram chat.</p>
<h3 id="step-1-start-a-chat-with-your-bot">Step 1: Start a Chat with Your Bot</h3>
<p>Search for your new bot in Telegram and send it any message (e.g., <code>/start</code>). This initializes the chat.</p>
<h3 id="step-2-retrieve-the-chat-id">Step 2: Retrieve the Chat ID</h3>
<p>In your server&rsquo;s terminal, run the following command. Replace <code>&lt;YOUR_BOT_TOKEN&gt;</code> with the token you saved from BotFather.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s <span style="color:#e6db74">&#34;https://api.telegram.org/bot&lt;YOUR_BOT_TOKEN&gt;/getUpdates&#34;</span> | jq <span style="color:#e6db74">&#39;.result[0].message.chat.id&#39;</span>
</span></span></code></pre></div><p>This command will output a long number, which is your <strong>Chat ID</strong>. Copy it.</p>
<h2 id="4-create-the-notification-script">4. Create the Notification Script</h2>
<p>This shell script will be triggered on login, gather information, and send the alert.</p>
<h3 id="step-1-create-the-script-file">Step 1: Create the Script File</h3>
<p>Using a text editor, create a new file for the script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nano /usr/local/bin/ssh-login-alert.sh
</span></span></code></pre></div><h3 id="step-2-add-the-script-content">Step 2: Add the Script Content</h3>
<p>Paste the following code into the file. <strong>Remember to replace the placeholder values for <code>BOT_TOKEN</code> and <code>CHAT_ID</code> with your actual credentials.</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># --- Replace with your details ---</span>
</span></span><span style="display:flex;"><span>BOT_TOKEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;YOUR_BOT_TOKEN&gt;&#34;</span>
</span></span><span style="display:flex;"><span>CHAT_ID<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&lt;YOUR_CHAT_ID&gt;&#34;</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"># Do nothing if not an interactive session</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -z <span style="color:#e6db74">&#34;</span>$PAM_TYPE<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">||</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$PAM_TYPE<span style="color:#e6db74">&#34;</span> !<span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;open_session&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Gather login information</span>
</span></span><span style="display:flex;"><span>HOSTNAME<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>hostname<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>USER<span style="color:#f92672">=</span>$PAM_USER
</span></span><span style="display:flex;"><span>IP_ADDRESS<span style="color:#f92672">=</span>$PAM_RHOST
</span></span><span style="display:flex;"><span>TIMESTAMP<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>date +<span style="color:#e6db74">&#34;%Y-%m-%d %H:%M:%S&#34;</span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Format the message using Markdown</span>
</span></span><span style="display:flex;"><span>MESSAGE<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>cat <span style="color:#e6db74">&lt;&lt;EOF
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">🔔 *New SSH Login Detected* 🔔
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">*Server:* \`$HOSTNAME\`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">*User:* \`$USER\`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">*From IP:* \`$IP_ADDRESS\`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">*Time:* \`$TIMESTAMP\`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Send the message via the Telegram API</span>
</span></span><span style="display:flex;"><span>curl -s --max-time <span style="color:#ae81ff">10</span> -X POST <span style="color:#e6db74">&#34;https://api.telegram.org/bot</span>$BOT_TOKEN<span style="color:#e6db74">/sendMessage&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>     -d chat_id<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$CHAT_ID<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>     -d text<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;</span>$MESSAGE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>     -d parse_mode<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Markdown&#34;</span> &gt; /dev/null
</span></span></code></pre></div><h3 id="step-3-make-the-script-executable">Step 3: Make the Script Executable</h3>
<p>Save the file and make it executable so the system can run it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo chmod +x /usr/local/bin/ssh-login-alert.sh
</span></span></code></pre></div><h2 id="5-configure-pam-to-trigger-the-script">5. Configure PAM to Trigger the Script</h2>
<p>We&rsquo;ll use the Pluggable Authentication Module (PAM) framework to execute our script whenever a user opens a new SSH session.</p>
<h3 id="step-1-edit-the-sshd-pam-configuration">Step 1: Edit the SSHD PAM Configuration</h3>
<p>Open the PAM configuration file for the SSH daemon:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nano /etc/pam.d/sshd
</span></span></code></pre></div><h3 id="step-2-add-the-execution-rule">Step 2: Add the Execution Rule</h3>
<p>Add the following line at the very <strong>end</strong> of the file. This tells PAM to run our script for every session, but our script is smart enough to only act on SSH logins.</p>
<pre tabindex="0"><code># Run script for SSH login notification
session optional pam_exec.so /usr/local/bin/ssh-login-alert.sh
</code></pre><p>Save and close the file.</p>
<h2 id="6-test-the-setup">6. Test the Setup</h2>
<p>You&rsquo;re all set! To test the notification, log out of your server and log back in via SSH.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>exit
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh your_user@your_server_ip
</span></span></code></pre></div><p>Within seconds, you should receive a neatly formatted notification on Telegram from your bot. If you don&rsquo;t, double-check that the <code>BOT_TOKEN</code> and <code>CHAT_ID</code> in your script are correct and that the script is executable.</p>
<pre tabindex="0"><code></code></pre>]]></content:encoded></item><item><title>Advanced VPS Guide: Mastering Nginx, Subdomains, and Reverse Proxies</title><link>https://md.eknath.dev/posts/software-development/vps-setup-guide/</link><pubDate>Sun, 07 Sep 2025 12:00:00 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/vps-setup-guide/</guid><description>&lt;p>A Virtual Private Server (VPS) is your personal canvas on the internet. While basic setup is straightforward, unlocking its true potential requires mastering the web server. This guide dives deep into using Nginx to host multiple projects, manage subdomains, and route traffic to different services, transforming your single server into a multi-functional powerhouse.&lt;/p>
&lt;h2 id="1-initial-server-setup">1. Initial Server Setup&lt;/h2>
&lt;p>After acquiring a VPS, you&amp;rsquo;ll get an IP address and root access. Your first steps are to secure the server and create a non-root user for daily operations.&lt;/p></description><content:encoded><![CDATA[<p>A Virtual Private Server (VPS) is your personal canvas on the internet. While basic setup is straightforward, unlocking its true potential requires mastering the web server. This guide dives deep into using Nginx to host multiple projects, manage subdomains, and route traffic to different services, transforming your single server into a multi-functional powerhouse.</p>
<h2 id="1-initial-server-setup">1. Initial Server Setup</h2>
<p>After acquiring a VPS, you&rsquo;ll get an IP address and root access. Your first steps are to secure the server and create a non-root user for daily operations.</p>
<p>Connect to your server via SSH:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ssh root@YOUR_SERVER_IP
</span></span></code></pre></div><h3 id="create-a-sudo-user">Create a Sudo User</h3>
<p>Operating as <code>root</code> is risky. Create a new user and grant administrative privileges.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Create the new user (replace &#39;devuser&#39; with your username)</span>
</span></span><span style="display:flex;"><span>adduser devuser
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Add the user to the &#39;sudo&#39; group</span>
</span></span><span style="display:flex;"><span>usermod -aG sudo devuser
</span></span></code></pre></div><p>Now, set up SSH key authentication for your new user for enhanced security and convenience, then log in as that user.</p>
<h2 id="2-essential-security-and-updates">2. Essential Security and Updates</h2>
<p>A public server is a constant target. Secure it immediately.</p>
<h3 id="configure-the-firewall">Configure the Firewall</h3>
<p><code>ufw</code> (Uncomplicated Firewall) makes this easy. We&rsquo;ll allow SSH, HTTP, and HTTPS traffic.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Allow OpenSSH (so you don&#39;t lock yourself out)</span>
</span></span><span style="display:flex;"><span>sudo ufw allow OpenSSH
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Allow Nginx to handle web traffic on ports 80 and 443</span>
</span></span><span style="display:flex;"><span>sudo ufw allow <span style="color:#e6db74">&#39;Nginx Full&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Enable the firewall</span>
</span></span><span style="display:flex;"><span>sudo ufw enable
</span></span></code></pre></div><p>After enabling, check its status to ensure it&rsquo;s active and your rules are loaded.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ufw status
</span></span></code></pre></div><h3 id="update-your-server">Update Your Server</h3>
<p>Keep your system&rsquo;s packages current to patch security vulnerabilities.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo apt update <span style="color:#f92672">&amp;&amp;</span> sudo apt upgrade -y
</span></span></code></pre></div><h2 id="3-installing-and-understanding-nginx">3. Installing and Understanding Nginx</h2>
<p>Nginx is the heart of our setup. It&rsquo;s a high-performance web server that can also act as a reverse proxy, load balancer, and more.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install Nginx</span>
</span></span><span style="display:flex;"><span>sudo apt install nginx -y
</span></span></code></pre></div><p>While the package should start and enable Nginx automatically, it&rsquo;s good practice to run the commands explicitly to be sure.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Start the Nginx service</span>
</span></span><span style="display:flex;"><span>sudo systemctl start nginx
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Enable Nginx to start automatically on boot</span>
</span></span><span style="display:flex;"><span>sudo systemctl enable nginx
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Now, check that it&#39;s running and enabled</span>
</span></span><span style="display:flex;"><span>sudo systemctl status nginx
</span></span></code></pre></div><p>You should see <code>active (running)</code> in the output. Visiting your server&rsquo;s IP in a browser should also show the Nginx welcome page.</p>
<h3 id="nginx-configuration-structure">Nginx Configuration Structure</h3>
<p>Nginx&rsquo;s configuration lives in <code>/etc/nginx</code>. The <code>-p</code> flag in the commands below ensures that the command does nothing if the directories already exist.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo mkdir -p /etc/nginx/sites-available
</span></span><span style="display:flex;"><span>sudo mkdir -p /etc/nginx/sites-enabled
</span></span></code></pre></div><p>The key directories are:</p>
<ul>
<li><code>/etc/nginx/nginx.conf</code>: The main configuration file. You rarely edit this.</li>
<li><code>/etc/nginx/sites-available/</code>: Where you store the configuration files for each of your sites (called &ldquo;server blocks&rdquo;).</li>
<li><code>/etc/nginx/sites-enabled/</code>: Where you create symbolic links to the configurations in <code>sites-available</code> that you want to be active.</li>
</ul>
<p>This structure lets you easily enable or disable sites without deleting their configuration files.</p>
<h2 id="4-advanced-hosting-subdomains-and-reverse-proxies">4. Advanced Hosting: Subdomains and Reverse Proxies</h2>
<p>This is where the magic happens. A single server can host a blog, a portfolio website, a web app, and several APIs, all neatly organized using subdomains.</p>
<p>The core concept is the <strong>Reverse Proxy</strong>. Your Nginx server listens on the standard web ports (80 for HTTP, 443 for HTTPS) and intelligently forwards incoming requests to the correct internal service based on the requested domain or subdomain.</p>
<h3 id="scenario">Scenario:</h3>
<p>Let&rsquo;s say we want to set up the following on our server:</p>
<ol>
<li><code>eknath.dev</code>: A static HTML/CSS website.</li>
<li><code>blog.eknath.dev</code>: A separate project, maybe a Hugo or Jekyll site.</li>
<li><code>api.eknath.dev</code>: A Node.js application running on port <code>3000</code>.</li>
</ol>
<h3 id="step-1-create-project-directories">Step 1: Create Project Directories</h3>
<p>Organize your projects in the <code>/var/www</code> directory.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Create directories for the main site and blog</span>
</span></span><span style="display:flex;"><span>sudo mkdir -p /var/www/eknath.dev/html
</span></span><span style="display:flex;"><span>sudo mkdir -p /var/www/blog.eknath.dev/html
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Set correct permissions</span>
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /var/www/eknath.dev
</span></span><span style="display:flex;"><span>sudo chown -R $USER:$USER /var/www/blog.eknath.dev
</span></span></code></pre></div><p>Verify that the directories were created with the correct ownership.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ls -ld /var/www/eknath.dev/
</span></span><span style="display:flex;"><span>ls -ld /var/www/blog.eknath.dev/
</span></span></code></pre></div><p>Now, place some placeholder files.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&lt;h1&gt;Welcome to Eknath&#39;s Site&lt;/h1&gt;&#34;</span> | sudo tee /var/www/eknath.dev/html/index.html
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&lt;h1&gt;Welcome to Eknath&#39;s Blog&lt;/h1&gt;&#34;</span> | sudo tee /var/www/blog.eknath.dev/html/index.html
</span></span></code></pre></div><h3 id="step-2-create-nginx-server-blocks">Step 2: Create Nginx Server Blocks</h3>
<p>Now, we create a configuration file for each site in <code>sites-available</code>.</p>
<p><strong>For the main domain (<code>eknath.dev</code>):</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-bash" data-lang="bash"><span style="display:flex;"><span>sudo nano /etc/nginx/sites-available/eknath.dev
</span></span></code></pre></div><p>Add the following server block:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">eknath.dev</span> <span style="color:#e6db74">www.eknath.dev</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/eknath.dev/html</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span> <span style="color:#e6db74">index.php</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">try_files</span> $uri $uri/ =<span style="color:#ae81ff">404</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>For the blog subdomain (<code>blog.eknath.dev</code>):</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-bash" data-lang="bash"><span style="display:flex;"><span>sudo nano /etc/nginx/sites-available/blog.eknath.dev
</span></span></code></pre></div><p>Add this configuration:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">blog.eknath.dev</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">root</span> <span style="color:#e6db74">/var/www/blog.eknath.dev/html</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">index</span> <span style="color:#e6db74">index.html</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">try_files</span> $uri $uri/ =<span style="color:#ae81ff">404</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="step-3-configure-the-reverse-proxy-for-the-api">Step 3: Configure the Reverse Proxy for the API</h3>
<p>For <code>api.eknath.dev</code>, we&rsquo;ll proxy requests to our Node.js app, which we assume is running on <code>http://127.0.0.1:3000</code>.</p>
<p>Create the configuration file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nano /etc/nginx/sites-available/api.eknath.dev
</span></span></code></pre></div><p>Add the reverse proxy configuration:</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-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">server</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#ae81ff">80</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">listen</span> <span style="color:#e6db74">[::]:80</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">server_name</span> <span style="color:#e6db74">api.eknath.dev</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">location</span> <span style="color:#e6db74">/</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_pass</span> <span style="color:#e6db74">http://127.0.0.1:3000</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">Host</span> $host;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Real-IP</span> $remote_addr;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-For</span> $proxy_add_x_forwarded_for;
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">proxy_set_header</span> <span style="color:#e6db74">X-Forwarded-Proto</span> $scheme;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="step-4-enable-the-sites-and-test">Step 4: Enable the Sites and Test</h3>
<p>Now, link these configurations into <code>sites-enabled</code> to activate them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo ln -s /etc/nginx/sites-available/eknath.dev /etc/nginx/sites-enabled/
</span></span><span style="display:flex;"><span>sudo ln -s /etc/nginx/sites-available/blog.eknath.dev /etc/nginx/sites-enabled/
</span></span><span style="display:flex;"><span>sudo ln -s /etc/nginx/sites-available/api.eknath.dev /etc/nginx/sites-enabled/
</span></span></code></pre></div><p>Test the Nginx configuration for syntax errors:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo nginx -t
</span></span></code></pre></div><p>If it&rsquo;s successful, restart Nginx to apply the changes:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl restart nginx
</span></span></code></pre></div><p>Check the status to ensure it restarted correctly.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo systemctl status nginx
</span></span></code></pre></div><p>After setting up your DNS records, you&rsquo;ll be able to access each service through its unique subdomain.</p>
<h2 id="5-securing-your-sites-with-ssl-https">5. Securing Your Sites with SSL (HTTPS)</h2>
<p>We&rsquo;ll use <strong>Let&rsquo;s Encrypt</strong>, a free and automated Certificate Authority, and <strong>Certbot</strong>, a tool that makes managing SSL/TLS certificates effortless.</p>
<h3 id="step-1-install-certbot">Step 1: Install Certbot</h3>
<p>Certbot has a dedicated Nginx plugin that automates the process.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install Certbot and its Nginx plugin</span>
</span></span><span style="display:flex;"><span>sudo apt install certbot python3-certbot-nginx -y
</span></span></code></pre></div><h3 id="step-2-obtain-and-install-the-ssl-certificates">Step 2: Obtain and Install the SSL Certificates</h3>
<p>With your server blocks already configured, running Certbot is incredibly simple.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Run Certbot to get certificates for all configured domains</span>
</span></span><span style="display:flex;"><span>sudo certbot --nginx
</span></span></code></pre></div><p>Certbot will guide you through a few simple steps:</p>
<ol>
<li><strong>Enter your email address.</strong></li>
<li><strong>Agree to the Terms of Service.</strong></li>
<li><strong>Choose domains</strong> from the list Certbot finds.</li>
<li><strong>Choose to redirect HTTP to HTTPS.</strong> This is highly recommended.</li>
</ol>
<h3 id="step-3-verify-the-new-configuration">Step 3: Verify the New Configuration</h3>
<p>Certbot automatically modifies your Nginx files to enable HTTPS. You can check the new configuration by running <code>sudo nginx -t</code> and then reloading Nginx with <code>sudo systemctl reload nginx</code>.</p>
<h3 id="step-4-understanding-automatic-renewal">Step 4: Understanding Automatic Renewal</h3>
<p>Let&rsquo;s Encrypt certificates are valid for 90 days. The Certbot package automatically sets up a task to renew them. You can test the renewal process with a dry run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>sudo certbot renew --dry-run
</span></span></code></pre></div><p>If this command runs without errors, your auto-renewal is set up correctly.</p>
<h2 id="conclusion">Conclusion</h2>
<p>You have now transformed a basic VPS into a sophisticated, multi-tenant hosting platform. By leveraging Nginx&rsquo;s server blocks and reverse proxy capabilities, you can host and manage numerous projects, each on its own subdomain, from a single server. Happy hosting!</p>
]]></content:encoded></item><item><title>The Journey of Commercial Computers: From ENIAC to Neuromorphic Chips</title><link>https://md.eknath.dev/posts/software-development/the-journey-of-commercial-computers/</link><pubDate>Sat, 23 Aug 2025 14:30:00 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/the-journey-of-commercial-computers/</guid><description>&lt;p>Computing didn&amp;rsquo;t start with your MacBook or the latest smartphone. It began in the 1940s with room-sized monsters that consumed more power than a small town. This is the story of how we got from there to here – a journey through seven decades of human ingenuity, each leap forward reshaping not just technology, but society itself.&lt;/p>
&lt;hr>
&lt;h2 id="the-dawn-eniac-and-the-electronic-age-1940s">The Dawn: ENIAC and the Electronic Age (1940s)&lt;/h2>
&lt;p>Picture this: 1946, University of Pennsylvania. A machine the size of a smand adall house, weighing 30 tons, consuming 150 kilowatts of power. This was &lt;strong>ENIAC&lt;/strong> – the Electronic Numerical Integrator and Computer.&lt;/p></description><content:encoded><![CDATA[<p>Computing didn&rsquo;t start with your MacBook or the latest smartphone. It began in the 1940s with room-sized monsters that consumed more power than a small town. This is the story of how we got from there to here – a journey through seven decades of human ingenuity, each leap forward reshaping not just technology, but society itself.</p>
<hr>
<h2 id="the-dawn-eniac-and-the-electronic-age-1940s">The Dawn: ENIAC and the Electronic Age (1940s)</h2>
<p>Picture this: 1946, University of Pennsylvania. A machine the size of a smand adall house, weighing 30 tons, consuming 150 kilowatts of power. This was <strong>ENIAC</strong> – the Electronic Numerical Integrator and Computer.</p>
<p>ENIAC was a beast. It used 17,468 vacuum tubes, each the size of a light bulb, generating so much heat that the room needed industrial cooling. Programming it meant physically rewiring connections – imagine debugging by crawling around inside your computer with a soldering iron.</p>
<p>But here&rsquo;s the magic: for the first time in human history, we had a machine that could perform calculations faster than any human mathematician. What took days by hand took minutes on ENIAC. The computing revolution had begun.</p>
<p><strong>The Challenge</strong>: Vacuum tubes burned out constantly. ENIAC operators became experts at hunting down failed tubes in a machine that looked more like a power plant than what we&rsquo;d recognize as a computer today.</p>
<hr>
<h2 id="the-revolution-transistors-change-everything-1950s-1960s">The Revolution: Transistors Change Everything (1950s-1960s)</h2>
<p>Then came 1947, and three guys at Bell Labs – Bardeen, Brattain, and Shockley – invented something that would change the world: the <strong>transistor</strong>.</p>
<p>Think of a vacuum tube as a traffic light that needs a massive power plant to operate. A transistor is like a tiny switch that runs on a battery. Suddenly, computers didn&rsquo;t need to be room-sized monsters anymore.</p>
<p>The <strong>IBM 7090</strong> in 1959 was a glimpse of the future. It was faster, more reliable, and – crucially – much smaller than its vacuum tube predecessors. But we were just getting started.</p>
<p><strong>The Breakthrough</strong>: Transistors were not only smaller and more efficient, but they were also incredibly reliable. Where vacuum tubes lasted months, transistors lasted years.</p>
<hr>
<h2 id="the-acceleration-integrated-circuits-1960s-1970s">The Acceleration: Integrated Circuits (1960s-1970s)</h2>
<p>Here&rsquo;s where things get interesting. Robert Noyce and Jack Kilby had the same brilliant idea around the same time: why put one transistor on a chip when you could put many?</p>
<p>The <strong>integrated circuit</strong> was born, and with it, Moore&rsquo;s Law – the observation that computing power doubles roughly every two years. Suddenly, we weren&rsquo;t just making computers smaller; we were making them exponentially more powerful.</p>
<p>Companies like <strong>Fairchild Semiconductor</strong> and later <strong>Intel</strong> began cramming more and more transistors onto single chips. The computer was shrinking from room-size to refrigerator-size to eventually&hellip; desk-size.</p>
<p><strong>The Vision</strong>: Gordon Moore&rsquo;s prediction wasn&rsquo;t just about more transistors – it was about unleashing computing power that would eventually fit in our pockets while being more powerful than ENIAC ever dreamed of being.</p>
<hr>
<h2 id="the-personal-revolution-microprocessors-1970s-1990s">The Personal Revolution: Microprocessors (1970s-1990s)</h2>
<p>1971: Intel releases the <strong>4004</strong>, the first commercial microprocessor. Four bits of processing power on a single chip smaller than your thumbnail. It sounds primitive now, but it was revolutionary.</p>
<p>The 4004 led to the 8008, then the 8080, and eventually to chips that powered the first personal computers. By the 1980s, companies like <strong>Apple</strong>, <strong>IBM</strong>, and <strong>Commodore</strong> were putting computers on desks in homes and offices worldwide.</p>
<p>This wasn&rsquo;t just a technological shift – it was a philosophical one. Computers went from being tools for scientists and governments to being personal companions. The phrase &ldquo;personal computer&rdquo; captured something profound: computing power was becoming democratized.</p>
<p><strong>The Transformation</strong>: The microprocessor didn&rsquo;t just make computers smaller; it made them personal. For the first time, individuals could own the computational power that once belonged only to universities and corporations.</p>
<hr>
<h2 id="the-mobile-era-systems-on-chips-2000s-2010s">The Mobile Era: Systems on Chips (2000s-2010s)</h2>
<p>Fast forward to the 2000s, and we faced a new challenge: how do you fit desktop-level performance into something that fits in your pocket and runs all day on a battery?</p>
<p>Enter <strong>systems-on-chip (SoCs)</strong> and mobile processors. Companies like <strong>ARM</strong> revolutionized computing by designing chips that sipped power rather than guzzling it. Apple&rsquo;s A-series chips, Qualcomm&rsquo;s Snapdragons, and Samsung&rsquo;s Exynos processors brought desktop-class performance to devices we carry everywhere.</p>
<p>Suddenly, the computer in your pocket was more powerful than the room-sized machines of the 1960s. We&rsquo;d come full circle – from massive power consumption to incredible efficiency.</p>
<p><strong>The Paradigm Shift</strong>: Mobile computing didn&rsquo;t just miniaturize computers; it reimagined what computing could be. Always-on, always-connected, always-in-your-pocket computing became the new normal.</p>
<hr>
<h2 id="the-quantum-leap-quantum-computing-2010s-present">The Quantum Leap: Quantum Computing (2010s-Present)</h2>
<p>But even as traditional computing reached incredible heights, we began bumping against physical limits. Transistors can only get so small before quantum effects start interfering with their operation.</p>
<p>So we decided to embrace those quantum effects instead.</p>
<p><strong>Quantum computing</strong> works on principles that seem to defy common sense. Where traditional computers use bits that are either 0 or 1, quantum computers use <strong>qubits</strong> that can be both 0 and 1 simultaneously – a property called superposition.</p>
<p>Companies like <strong>IBM</strong>, <strong>Google</strong>, and <strong>IonQ</strong> are building quantum computers that can solve certain problems exponentially faster than any classical computer. Google&rsquo;s <strong>Sycamore</strong> processor achieved &ldquo;quantum supremacy&rdquo; in 2019, performing a calculation in 200 seconds that would take the world&rsquo;s fastest supercomputer 10,000 years.</p>
<p><strong>The Promise</strong>: Quantum computers won&rsquo;t replace your laptop, but they could revolutionize drug discovery, financial modeling, and cryptography – problems that require exploring vast solution spaces simultaneously.</p>
<hr>
<h2 id="the-brain-inspired-future-neuromorphic-computing-present-future">The Brain-Inspired Future: Neuromorphic Computing (Present-Future)</h2>
<p>Here&rsquo;s where it gets really interesting. What if, instead of making computers faster, we made them smarter? What if we designed chips that work more like brains than calculators?</p>
<p><strong>Neuromorphic chips</strong> mimic the structure and function of biological neural networks. Companies like <strong>Intel</strong> with their Loihi processor and <strong>IBM</strong> with TrueNorth are creating chips that don&rsquo;t just process information – they learn and adapt.</p>
<p>Traditional computers are incredibly fast but energy-hungry. Your brain, running on about 20 watts (less than a light bulb), can recognize faces, understand language, and make complex decisions in milliseconds. Neuromorphic chips aim to capture that efficiency.</p>
<p><strong>The Vision</strong>: Imagine devices that learn your patterns, adapt to your needs, and operate for months on a single charge. Neuromorphic computing could enable truly intelligent edge devices – from smart prosthetics to autonomous robots that think more like humans.</p>
<hr>
<h2 id="the-pattern-each-eras-gift-to-the-next">The Pattern: Each Era&rsquo;s Gift to the Next</h2>
<p>Looking back, each era of computing didn&rsquo;t just improve on the last – it enabled entirely new possibilities:</p>
<ul>
<li><strong>ENIAC</strong> proved electronic computation was possible</li>
<li><strong>Transistors</strong> made it reliable and efficient</li>
<li><strong>Integrated circuits</strong> made it scalable</li>
<li><strong>Microprocessors</strong> made it personal</li>
<li><strong>Mobile chips</strong> made it ubiquitous</li>
<li><strong>Quantum computers</strong> make impossible problems solvable</li>
<li><strong>Neuromorphic chips</strong> make computers truly intelligent</li>
</ul>
<hr>
<h2 id="what-this-means-for-us">What This Means for Us</h2>
<p>We&rsquo;re living through the most exciting period in computing history. The smartphone in your pocket contains billions of transistors working in harmony, capable of tasks that would have seemed like magic to ENIAC&rsquo;s operators.</p>
<p>But we&rsquo;re just getting started. The convergence of quantum computing, neuromorphic processing, and traditional silicon is creating possibilities we&rsquo;re only beginning to imagine.</p>
<p>The next chapter is being written right now, in labs and garages and coffee shops around the world. And if history is any guide, it&rsquo;s going to be more revolutionary than anything we&rsquo;ve seen before.</p>
<p><strong>The question isn&rsquo;t what computers will be able to do – it&rsquo;s what problems will we choose to solve with them.</strong></p>
<hr>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>From room-sized vacuum tube monsters to neuromorphic chips that think like brains, computing has been humanity&rsquo;s greatest amplifier of intelligence. Each generation of technologists has stood on the shoulders of those who came before, pushing the boundaries of what&rsquo;s possible.</p>
<p>The journey from ENIAC to quantum computers is really a story about human ambition – our relentless drive to build tools that make us more capable, more connected, and more creative. And if the past seven decades are any indication, we&rsquo;re nowhere near finished.</p>
<p>The next breakthrough might come from a small team in a garage, just like Apple and HP did decades ago. Or it might emerge from a quantum lab pushing the boundaries of physics itself.</p>
<p>Either way, one thing is certain: the best is yet to come.</p>
<hr>
<p><em>What aspect of computing history fascinates you most? Have you worked with any of these technologies, or are you building the next chapter yourself? I&rsquo;d love to hear your thoughts.</em></p>
]]></content:encoded></item><item><title>Setting Up Claude Code Review Before Push</title><link>https://md.eknath.dev/posts/software-development/pre-push-code-review-with-claude/</link><pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate><guid>https://md.eknath.dev/posts/software-development/pre-push-code-review-with-claude/</guid><description>&lt;h1 id="setting-up-claude-code-review-before-push">Setting Up Claude Code Review Before Push&lt;/h1>
&lt;p>This guide will help you set up an automated code review system using Claude Code that runs before every git push, with the option to skip when needed.&lt;/p>
&lt;h2 id="prerequisites">Prerequisites&lt;/h2>
&lt;ul>
&lt;li>Claude Code CLI installed and configured&lt;/li>
&lt;li>Git repository with proper remote setup&lt;/li>
&lt;li>Terminal access&lt;/li>
&lt;/ul>
&lt;h2 id="step-1-create-the-pre-push-hook-script">Step 1: Create the Pre-Push Hook Script&lt;/h2>
&lt;p>Create a git hook that will run before every push:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Navigate to your project root&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd /path/to/your/project
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Create the hooks directory if it doesn&amp;#39;t exist&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mkdir -p .git/hooks
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Create the pre-push hook file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>touch .git/hooks/pre-push
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Make it executable&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>chmod +x .git/hooks/pre-push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="step-2-generated-review-reports">Step 2: Generated Review Reports&lt;/h2>
&lt;p>Every code review automatically generates a timestamped markdown report with complete analysis:&lt;/p></description><content:encoded><![CDATA[<h1 id="setting-up-claude-code-review-before-push">Setting Up Claude Code Review Before Push</h1>
<p>This guide will help you set up an automated code review system using Claude Code that runs before every git push, with the option to skip when needed.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>Claude Code CLI installed and configured</li>
<li>Git repository with proper remote setup</li>
<li>Terminal access</li>
</ul>
<h2 id="step-1-create-the-pre-push-hook-script">Step 1: Create the Pre-Push Hook Script</h2>
<p>Create a git hook that will run before every push:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Navigate to your project root</span>
</span></span><span style="display:flex;"><span>cd /path/to/your/project
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create the hooks directory if it doesn&#39;t exist</span>
</span></span><span style="display:flex;"><span>mkdir -p .git/hooks
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create the pre-push hook file</span>
</span></span><span style="display:flex;"><span>touch .git/hooks/pre-push
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Make it executable</span>
</span></span><span style="display:flex;"><span>chmod +x .git/hooks/pre-push
</span></span></code></pre></div><h2 id="step-2-generated-review-reports">Step 2: Generated Review Reports</h2>
<p>Every code review automatically generates a timestamped markdown report with complete analysis:</p>
<h3 id="report-features">Report Features</h3>
<ul>
<li><strong>Filename</strong>: <code>claude-review-report_2025-08-20_14-30-45.md</code></li>
<li><strong>Complete History</strong>: All commits since main branch</li>
<li><strong>File Changes</strong>: List of all modified files</li>
<li><strong>Project Checks</strong>: Linting and type checking results</li>
<li><strong>Claude Analysis</strong>: Detailed security, bug, and quality review</li>
<li><strong>Action Checklist</strong>: Checkboxes for addressing issues</li>
</ul>
<h3 id="sample-report-structure">Sample Report Structure</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-markdown" data-lang="markdown"><span style="display:flex;"><span># Claude Code Review Report
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Generated:**</span> 2025-08-20 14:30:45
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Branch:**</span> feature/new-ui
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Compared Against:**</span> origin/main  
</span></span><span style="display:flex;"><span><span style="font-weight:bold">**Review Status:**</span> ✅ Passed
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Commits Reviewed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> feat: add new user interface
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> fix: resolve navigation bug
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> test: add unit tests for new component
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Files Changed
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> src/components/UserInterface.tsx
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> src/navigation/Router.tsx  
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> tests/UserInterface.test.tsx
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Project Checks
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">### Linting
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>✅ <span style="font-weight:bold">**Passed**</span> - No linting issues found
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">### Type Checking
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>⚠️ <span style="font-weight:bold">**Issues Found**</span> - See type checking output
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Claude Code Review Results
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>[Claude&#39;s detailed analysis here]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Quick Actions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">### If Issues Found:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">- [ ]</span> Fix security vulnerabilities
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">- [ ]</span> Resolve critical bugs
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">- [ ]</span> Address performance issues
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><h2 id="step-3-write-the-pre-push-hook">Step 3: Write the Pre-Push Hook</h2>
<p>Add the following content to <code>.git/hooks/pre-push</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-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Colors for output</span>
</span></span><span style="display:flex;"><span>RED<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;\033[0;31m&#39;</span>
</span></span><span style="display:flex;"><span>GREEN<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;\033[0;32m&#39;</span>
</span></span><span style="display:flex;"><span>YELLOW<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;\033[1;33m&#39;</span>
</span></span><span style="display:flex;"><span>NC<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;\033[0m&#39;</span> <span style="color:#75715e"># No Color</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check if SKIP_CLAUDE_REVIEW environment variable is set</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$SKIP_CLAUDE_REVIEW<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;true&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">⚠️  Claude Code review skipped (SKIP_CLAUDE_REVIEW=true)</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check if claude command is available</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> ! command -v claude &amp;&gt; /dev/null; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>RED<span style="color:#e6db74">}</span><span style="color:#e6db74">❌ Claude Code CLI not found. Install it first or skip with: SKIP_CLAUDE_REVIEW=true git push</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">🤖 Running Claude Code review before push...</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Get the current branch</span>
</span></span><span style="display:flex;"><span>current_branch<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>git branch --show-current<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Find the main/master branch</span>
</span></span><span style="display:flex;"><span>main_branch<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> git show-ref --verify --quiet refs/heads/main; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    main_branch<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;main&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> git show-ref --verify --quiet refs/heads/master; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    main_branch<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;master&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> git show-ref --verify --quiet refs/remotes/origin/main; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    main_branch<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;origin/main&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> git show-ref --verify --quiet refs/remotes/origin/master; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    main_branch<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;origin/master&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">⚠️  Could not find main/master branch, comparing with HEAD~1</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    main_branch<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;HEAD~1&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check if there are any changes to review since main branch</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$main_branch<span style="color:#e6db74">&#34;</span> !<span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HEAD~1&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> git diff --quiet <span style="color:#e6db74">&#34;</span>$main_branch<span style="color:#e6db74">&#34;</span>..HEAD 2&gt;/dev/null; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GREEN<span style="color:#e6db74">}</span><span style="color:#e6db74">✅ No changes detected since </span>$main_branch<span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$main_branch<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HEAD~1&#34;</span> <span style="color:#f92672">]</span> <span style="color:#f92672">&amp;&amp;</span> git diff --quiet HEAD~1 HEAD 2&gt;/dev/null; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GREEN<span style="color:#e6db74">}</span><span style="color:#e6db74">✅ No changes detected since last commit</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Show what&#39;s being reviewed</span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">📋 Reviewing changes in branch: </span>$current_branch<span style="color:#e6db74"> (since </span>$main_branch<span style="color:#e6db74">)</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> <span style="color:#e6db74">&#34;</span>$main_branch<span style="color:#e6db74">&#34;</span> !<span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;HEAD~1&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>BLUE<span style="color:#e6db74">}</span><span style="color:#e6db74">Commits to be reviewed:</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    git log --oneline <span style="color:#e6db74">&#34;</span>$main_branch<span style="color:#e6db74">&#34;</span>..HEAD
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>BLUE<span style="color:#e6db74">}</span><span style="color:#e6db74">Files changed:</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    git diff --name-only <span style="color:#e6db74">&#34;</span>$main_branch<span style="color:#e6db74">&#34;</span>..HEAD
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    git log --oneline -5
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">📝 Running automated code review...</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Run project-specific checks first</span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>BLUE<span style="color:#e6db74">}</span><span style="color:#e6db74">🔧 Running project checks...</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Linting</span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">  → Running linter...</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> npm run lint <span style="color:#f92672">||</span> yarn lint 2&gt;/dev/null; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GREEN<span style="color:#e6db74">}</span><span style="color:#e6db74">    ✅ Linting passed</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">    ⚠️  Linting issues found (will be reviewed by Claude)</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Type checking</span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">  → Running type check...</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> npm run typecheck <span style="color:#f92672">||</span> yarn typecheck <span style="color:#f92672">||</span> tsc --noEmit 2&gt;/dev/null; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GREEN<span style="color:#e6db74">}</span><span style="color:#e6db74">    ✅ Type checking passed</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">    ⚠️  Type issues found (will be reviewed by Claude)</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Create a temporary file for the review prompt</span>
</span></span><span style="display:flex;"><span>review_file<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>mktemp<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>cat &gt; <span style="color:#e6db74">&#34;</span>$review_file<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&lt;&lt; &#39;EOF&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Please perform a comprehensive code review of ALL changes in this branch before I push to the remote repository.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">IMPORTANT: Review ALL commits since the main branch, not just the latest commit. 
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Use &#39;git diff main..HEAD&#39; and &#39;git log main..HEAD&#39; to see all changes.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">The following commits will be pushed:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">$(git log --oneline main..HEAD 2&gt;/dev/null || git log --oneline -5)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Files that have been changed:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">$(git diff --name-only main..HEAD 2&gt;/dev/null || git diff --name-only HEAD~1 HEAD)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Comprehensive review including security, bugs, performance, code quality, test coverage, and best practices. 
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Please also run these project-specific commands and report any issues:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">1. Linting: npm run lint || yarn lint
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">2. Type checking: npm run typecheck || yarn typecheck || tsc --noEmit
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">3. Tests: npm test || yarn test
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Focus areas:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">1. **Security issues** - Check for exposed secrets, SQL injection, XSS vulnerabilities
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">2. **Bug potential** - Logic errors, null pointer exceptions, edge cases  
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">3. **Code quality** - Following project conventions, proper error handling
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">4. **Performance** - Inefficient operations, memory issues
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">5. **All commits** - Review every commit since main branch, not just the latest one
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">If you find any critical issues, please list them clearly with file locations. 
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">If the code looks good to push, respond with: &#34;✅ Code review passed - safe to push&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Please review the ENTIRE diff since main branch, including all commits and file changes.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">EOF</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">📝 Running automated code review...</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Run Claude Code review</span>
</span></span><span style="display:flex;"><span>claude code &lt; <span style="color:#e6db74">&#34;</span>$review_file<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>review_exit_code<span style="color:#f92672">=</span>$?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Clean up</span>
</span></span><span style="display:flex;"><span>rm <span style="color:#e6db74">&#34;</span>$review_file<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check if Claude review was successful</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> $review_exit_code -ne <span style="color:#ae81ff">0</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>RED<span style="color:#e6db74">}</span><span style="color:#e6db74">❌ Claude Code review failed</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">💡 To skip this review: SKIP_CLAUDE_REVIEW=true git push</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GREEN<span style="color:#e6db74">}</span><span style="color:#e6db74">🎉 Claude Code review completed!</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">💡 To skip future reviews: SKIP_CLAUDE_REVIEW=true git push</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Ask user if they want to proceed</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>read -p <span style="color:#e6db74">&#34;Do you want to proceed with the push? (y/N): &#34;</span> -n <span style="color:#ae81ff">1</span> -r
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> $REPLY <span style="color:#f92672">=</span>~ ^<span style="color:#f92672">[</span>Yy<span style="color:#f92672">]</span>$ <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>GREEN<span style="color:#e6db74">}</span><span style="color:#e6db74">✅ Proceeding with push...</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">0</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>
</span></span><span style="display:flex;"><span>    echo -e <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>YELLOW<span style="color:#e6db74">}</span><span style="color:#e6db74">🛑 Push cancelled by user</span><span style="color:#e6db74">${</span>NC<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>    exit <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><h2 id="step-3-configure-your-project">Step 3: Configure Your Project</h2>
<p>Add these commands to your <code>CLAUDE.md</code> file (create if it doesn&rsquo;t exist):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span># Project Code Review Commands
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Lint Commands
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`npm run lint`</span> or <span style="color:#e6db74">`yarn lint`</span> - Run ESLint
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`npm run lint:fix`</span> or <span style="color:#e6db74">`yarn lint:fix`</span> - Fix linting issues
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Type Checking
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`npm run typecheck`</span> or <span style="color:#e6db74">`yarn typecheck`</span> - Run TypeScript checking
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`tsc --noEmit`</span> - Alternative TypeScript check
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Testing
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`npm test`</span> or <span style="color:#e6db74">`yarn test`</span> - Run all tests
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`npm run test:coverage`</span> - Run tests with coverage
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Build
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">-</span> <span style="color:#e6db74">`npm run build`</span> or <span style="color:#e6db74">`yarn build`</span> - Build the project
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Pre-push Checklist
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>Before pushing code, ensure:
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">1.</span> All tests pass
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">2.</span> No linting errors
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">3.</span> No type errors
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">4.</span> Build succeeds
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">5.</span> No security vulnerabilities
</span></span></code></pre></div><h2 id="step-4-test-the-setup">Step 4: Test the Setup</h2>
<p>Test your setup with these commands:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Make a small change and commit it</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;// Test comment&#34;</span> &gt;&gt; src/test-file.js
</span></span><span style="display:flex;"><span>git add .
</span></span><span style="display:flex;"><span>git commit -m <span style="color:#e6db74">&#34;test: add test comment for hook testing&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Try to push (this should trigger Claude review)</span>
</span></span><span style="display:flex;"><span>git push origin your-branch-name
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Test skipping the review</span>
</span></span><span style="display:flex;"><span>SKIP_CLAUDE_REVIEW<span style="color:#f92672">=</span>true git push origin your-branch-name
</span></span></code></pre></div><h2 id="usage-examples">Usage Examples</h2>
<h3 id="normal-push-with-review">Normal push with review:</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git push origin feature-branch
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This will automatically run Claude Code review</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Generates: claude-review-report_YYYY-MM-DD_HH-MM-SS.md</span>
</span></span></code></pre></div><h3 id="skip-review-when-needed">Skip review when needed:</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># For urgent hotfixes or when review isn&#39;t needed</span>
</span></span><span style="display:flex;"><span>SKIP_CLAUDE_REVIEW<span style="color:#f92672">=</span>true git push origin hotfix-branch
</span></span></code></pre></div><h3 id="set-up-permanent-skip-not-recommended">Set up permanent skip (not recommended):</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Add to your shell profile (.bashrc, .zshrc, etc.)</span>
</span></span><span style="display:flex;"><span>export SKIP_CLAUDE_REVIEW<span style="color:#f92672">=</span>true
</span></span></code></pre></div><h2 id="advanced-configuration">Advanced Configuration</h2>
<h3 id="custom-review-prompts">Custom Review Prompts</h3>
<p>Create different review prompts for different scenarios by modifying the hook:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># In your pre-push hook, you can customize based on branch</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> $current_branch <span style="color:#f92672">==</span> hotfix* <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Use lighter review for hotfixes</span>
</span></span><span style="display:flex;"><span>    review_prompt<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Quick security and critical bug check only&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">elif</span> <span style="color:#f92672">[[</span> $current_branch <span style="color:#f92672">==</span> feature* <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># Full review for features</span>
</span></span><span style="display:flex;"><span>    review_prompt<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Comprehensive code review including performance and best practices&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span></code></pre></div><h3 id="integration-with-cicd">Integration with CI/CD</h3>
<p>You can also integrate this into your CI/CD pipeline:</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># .github/workflows/code-review.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#ae81ff">Claude Code Review</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">on</span>: [<span style="color:#ae81ff">pull_request]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">review</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">ubuntu-latest</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">uses</span>: <span style="color:#ae81ff">actions/checkout@v3</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Setup Claude Code</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          # Install Claude Code CLI
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          pip install claude-code</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">name</span>: <span style="color:#ae81ff">Run Code Review</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">run</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">          claude code &#34;Please review this PR for security, bugs, and code quality issues&#34;</span>
</span></span></code></pre></div><h2 id="troubleshooting">Troubleshooting</h2>
<h3 id="hook-not-running">Hook not running?</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Check if hook is executable</span>
</span></span><span style="display:flex;"><span>ls -la .git/hooks/pre-push
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Make it executable if needed</span>
</span></span><span style="display:flex;"><span>chmod +x .git/hooks/pre-push
</span></span></code></pre></div><h3 id="claude-code-not-found">Claude Code not found?</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install Claude Code CLI</span>
</span></span><span style="display:flex;"><span>pip install anthropic-claude-code
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Or using npm</span>
</span></span><span style="display:flex;"><span>npm install -g @anthropic-ai/claude-code
</span></span></code></pre></div><h3 id="want-to-modify-the-hook">Want to modify the hook?</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Edit the hook file</span>
</span></span><span style="display:flex;"><span>nano .git/hooks/pre-push
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Or use your preferred editor</span>
</span></span><span style="display:flex;"><span>code .git/hooks/pre-push
</span></span></code></pre></div><h2 id="key-features">Key Features</h2>
<p>✅ <strong>Full Branch Review</strong>: Reviews ALL commits since branching from main, not just the latest commit<br>
✅ <strong>Smart Branch Detection</strong>: Automatically finds main/master/origin/main/origin/master<br>
✅ <strong>Auto Project Detection</strong>: Identifies JavaScript/TypeScript, Python, Java/Android projects automatically<br>
✅ <strong>Timestamped Reports</strong>: Generates permanent markdown reports with complete analysis<br>
✅ <strong>Comprehensive Analysis</strong>: Reviews security, performance, bugs, and code quality<br>
✅ <strong>Project-Specific Checks</strong>: Runs your lint, typecheck, and test commands<br>
✅ <strong>Action Checklists</strong>: Provides clear tasks for addressing any issues found<br>
✅ <strong>Detailed Reporting</strong>: Shows exactly which commits and files will be reviewed<br>
✅ <strong>Flexible</strong>: Can be skipped when needed for urgent pushes</p>
<h2 id="benefits">Benefits</h2>
<p>✅ <strong>Automated Quality Control</strong>: Catches issues before they reach the remote repository<br>
✅ <strong>Full Change Visibility</strong>: Reviews entire diff since branching from main<br>
✅ <strong>Permanent Documentation</strong>: Timestamped reports track all reviews and action items<br>
✅ <strong>Issue Tracking</strong>: Action checklists help systematically address problems<br>
✅ <strong>Educational</strong>: Learn best practices from Claude&rsquo;s feedback<br>
✅ <strong>Team Consistency</strong>: Ensures all team members follow the same review standards<br>
✅ <strong>Historical Record</strong>: Keep track of code quality improvements over time</p>
<h2 id="team-setup">Team Setup</h2>
<p>Share this setup with your team by:</p>
<ol>
<li>Adding the pre-push hook to your repository (in a <code>scripts/</code> folder)</li>
<li>Creating a setup script that copies it to <code>.git/hooks/</code></li>
<li>Documenting the process in your project&rsquo;s README</li>
<li>Adding the CLAUDE.md configuration to your repository</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># scripts/setup-hooks.sh</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash</span>
</span></span><span style="display:flex;"><span>cp scripts/pre-push .git/hooks/pre-push
</span></span><span style="display:flex;"><span>chmod +x .git/hooks/pre-push
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;✅ Claude Code review hook installed!&#34;</span>
</span></span></code></pre></div><p>Now your code will be automatically reviewed by Claude before every push, helping maintain high code quality while giving you the flexibility to skip when needed!</p>
]]></content:encoded></item><item><title>We Are Not Just Lame Developers, We Are Solutionists</title><link>https://md.eknath.dev/posts/software-development/solutionist/</link><pubDate>Sat, 16 Aug 2025 04:47:13 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/solutionist/</guid><description>&lt;h2 id="the-developers-box">The Developer&amp;rsquo;s Box&lt;/h2>
&lt;p>For too long, we&amp;rsquo;ve confined ourselves to a title: &amp;ldquo;developer.&amp;rdquo; We write code, we fix bugs, we build features. We are the architects and construction workers of the digital world. And while that is a noble and essential craft, the label itself can become a box. It can narrow our focus to the &lt;em>how&lt;/em> – the languages, the frameworks, the systems – and make us lose sight of the &lt;em>why&lt;/em>.&lt;/p></description><content:encoded><![CDATA[<h2 id="the-developers-box">The Developer&rsquo;s Box</h2>
<p>For too long, we&rsquo;ve confined ourselves to a title: &ldquo;developer.&rdquo; We write code, we fix bugs, we build features. We are the architects and construction workers of the digital world. And while that is a noble and essential craft, the label itself can become a box. It can narrow our focus to the <em>how</em> – the languages, the frameworks, the systems – and make us lose sight of the <em>why</em>.</p>
<p>We get caught up in debates about which programming language is superior, which framework is the most scalable, and which cloud provider is the most cost-effective. We specialize, we become experts in our niche, and in doing so, we sometimes build walls around ourselves. We become a &ldquo;Java developer,&rdquo; a &ldquo;frontend engineer,&rdquo; a &ldquo;mobile expert.&rdquo; These labels, while useful for a resume, can inadvertently limit our potential.</p>
<h2 id="the-call-of-the-solutionist">The Call of the Solutionist</h2>
<p>But what if we shed these self-imposed boundaries? What if we saw ourselves not as developers, but as <strong>Solutionists</strong>?</p>
<p>A Solutionist is not defined by the tools they use, but by the problems they solve. A Solutionist is language-agnostic, framework-flexible, and system-aware. Their loyalty is not to a specific technology stack, but to the most elegant, efficient, and impactful solution.</p>
<p>Being a Solutionist means stepping back from the keyboard and looking at the bigger picture. It means asking the right questions before writing a single line of code:</p>
<ul>
<li>What is the real problem we are trying to solve?</li>
<li>Who are we solving it for?</li>
<li>What is the most direct path to a solution, even if it means not writing any code at all?</li>
</ul>
<h2 id="beyond-the-code">Beyond the Code</h2>
<p>The journey from developer to Solutionist is a shift in mindset. It&rsquo;s about embracing a broader skillset and a deeper curiosity. It&rsquo;s about understanding that code is just one tool in our problem-solving arsenal.</p>
<p>Here&rsquo;s what it means to be a Solutionist:</p>
<ul>
<li><strong>Embrace Empathy:</strong> A Solutionist starts with the user. They strive to understand their needs, their frustrations, and their goals. They are part designer, part psychologist, part anthropologist.</li>
<li><strong>Think in Systems:</strong> A Solutionist sees the interconnectedness of things. They understand that a small change in one part of a system can have a ripple effect elsewhere. They think about scalability, maintainability, and the long-term impact of their decisions.</li>
<li><strong>Become a Perpetual Learner:</strong> A Solutionist is a voracious learner. They are not afraid to venture into unfamiliar territory, whether it&rsquo;s a new programming language, a different cloud platform, or a completely new domain of knowledge. They know that the best solution might lie just outside their comfort zone.</li>
<li><strong>Master the Art of Communication:</strong> A Solutionist can articulate complex technical ideas to a non-technical audience. They can collaborate effectively with designers, product managers, and business stakeholders. They are storytellers, translators, and bridge-builders.</li>
</ul>
<h2 id="the-joy-of-creation-and-the-age-of-ai">The Joy of Creation and the Age of AI</h2>
<p>Do you remember the first time you built something that worked? That spark of joy when your code compiled, your app launched, or your script ran without a hitch? There&rsquo;s an immense, selfless pleasure in creating something that can help you or others. It&rsquo;s a feeling of pure creation, of bringing an idea to life.</p>
<p>But somewhere along the way, the industry seems to have lost some of that magic. The joy of creation can get buried under layers of process, tight deadlines, and the pressure to specialize. We become so focused on our small part of a massive machine that we forget the thrill of building something whole.</p>
<p>Now, with the rapid advancement of AI, we have a unique opportunity to reclaim that joy. AI is not here to replace us; it&rsquo;s here to augment us. It can be our tireless coding partner, our creative collaborator, and our personal tutor. With AI handling the repetitive and mundane tasks, we are free to focus on what truly matters: the creative process of problem-solving.</p>
<p>This is the perfect moment to embrace the &ldquo;Solutionist&rdquo; mindset. With AI, a single person with a vision can now design, build, and deploy sophisticated applications. If you have a solid understanding of concepts like auto-scaling servers and security, you can create personal projects that can have a global reach. The barrier to entry has never been lower.</p>
<p>Of course, this doesn&rsquo;t mean that large-scale, global applications will be built by a single person. Those will always require the expertise of a dedicated team, rigorous testing, and robust security measures. But for our personal projects, for the ideas that we are passionate about, the constraints are melting away.</p>
<h2 id="the-future-is-for-solutionists">The Future is for Solutionists</h2>
<p>The world is facing a myriad of complex challenges, from climate change and healthcare to education and economic inequality. Solving these problems isn&rsquo;t just the job of large corporations; it&rsquo;s our responsibility too. A small project, a simple app, a clever script built by one of us could be the spark that inspires a breakthrough, pushing larger organizations to crack the code faster and deliver solutions to the masses. These problems will not be solved by developers who are content to stay within their boxes. They will be solved by Solutionists who are willing to think differently, to challenge the status quo, and to use technology as a force for good.</p>
<p>So, let&rsquo;s break free from the &ldquo;developer&rdquo; label. Let&rsquo;s embrace the identity of a Solutionist. Let&rsquo;s go out and solve the problems that matter. The world is waiting.</p>
]]></content:encoded></item><item><title>Vibecoding: My Experience Building Dhanika</title><link>https://md.eknath.dev/posts/software-development/vibecoding_experience_building_dhanika/</link><pubDate>Sat, 02 Aug 2025 00:00:00 +0000</pubDate><guid>https://md.eknath.dev/posts/software-development/vibecoding_experience_building_dhanika/</guid><description>&lt;p>Software development is often seen as a discipline of pure logic and rigid structures. We talk about architecture, frameworks, and design patterns – the building blocks of a rational and predictable process. But what if there&amp;rsquo;s another side to it? A more intuitive, artistic, and dare I say, &lt;em>philosophical&lt;/em> approach to building things? This is the story of my journey with &amp;ldquo;vibecoding&amp;rdquo; while building a personal project called &lt;a href="https://dhanika.eknath.dev">Dhanika&lt;/a>.&lt;/p>
&lt;h3 id="what-is-vibecoding">What is Vibecoding?&lt;/h3>
&lt;p>The term &amp;ldquo;vibecoding,&amp;rdquo; recently popularized by &lt;a href="https://x.com/karpathy/status/1886192184808149383?lang=en">Andrej Karpathy&lt;/a>, describes a software development approach that is less about rigid plans and more about the “vibe” of the project. It&amp;rsquo;s about letting your intuition and the feel of the product guide the development process. Instead of detailed specs and roadmaps, you work with a general idea, a vision, and you let the project evolve organically.&lt;/p></description><content:encoded><![CDATA[<p>Software development is often seen as a discipline of pure logic and rigid structures. We talk about architecture, frameworks, and design patterns – the building blocks of a rational and predictable process. But what if there&rsquo;s another side to it? A more intuitive, artistic, and dare I say, <em>philosophical</em> approach to building things? This is the story of my journey with &ldquo;vibecoding&rdquo; while building a personal project called <a href="https://dhanika.eknath.dev">Dhanika</a>.</p>
<h3 id="what-is-vibecoding">What is Vibecoding?</h3>
<p>The term &ldquo;vibecoding,&rdquo; recently popularized by <a href="https://x.com/karpathy/status/1886192184808149383?lang=en">Andrej Karpathy</a>, describes a software development approach that is less about rigid plans and more about the “vibe” of the project. It&rsquo;s about letting your intuition and the feel of the product guide the development process. Instead of detailed specs and roadmaps, you work with a general idea, a vision, and you let the project evolve organically.</p>
<p>It&rsquo;s a dance between logic and intuition, a form of mindfulness where you are fully present with the code and the project, responding to its needs in the moment. This might sound chaotic, and in some ways it is. But it&rsquo;s a controlled chaos. It&rsquo;s about being in tune with the project&rsquo;s needs, the user&rsquo;s needs, and your own creative flow.</p>
<h3 id="the-dhanika-project">The Dhanika Project</h3>
<p>Dhanika is a personal finance tracker I built for myself. I&rsquo;ve tried many finance apps, but none of them felt quite right. They were either too complex or too simple. I wanted something that was tailored to my specific needs and my way of thinking about money.</p>
<p>The vision for Dhanika was simple: a clean, intuitive interface to track my income and expenses, with a focus on simplicity and mindfulness about my spending habits.</p>
<p>What started as a personal tool has since been shared with some of my friends, who have started using it to gain clarity on their own financial posture. They&rsquo;ve found the ability to visualize their finances with graphs and charts particularly helpful. This has been incredibly rewarding and has reinforced the value of building something with a clear and simple vision.</p>
<h3 id="the-vibecoding-journey-with-dhanika">The Vibecoding Journey with Dhanika</h3>
<p>To be honest, the initial phase of this journey was daunting. I felt a profound sense of imposter syndrome. Was this “vibecoding” just an excuse for not having a proper plan? Was I doing things “the wrong way”? The feeling was akin to standing still while the world moved on. I had to have a serious talk with myself. I realized that the future of creation is fluid and intuitive, and clinging to old, rigid methods was like trying to carry heavy luggage onto a moving train. Once I broke free from that guilt and embraced the uncertainty, everything changed. The project took off, and I truly started to vibe with the code.</p>
<p>I started Dhanika with no grand design document. I had a single-sentence vision: &ldquo;A beautiful and simple finance tracker that feels good to use.&rdquo;</p>
<p>The first step was to create the most basic feature: adding an expense. I didn&rsquo;t worry about the database schema or the overall architecture. I just wanted to get something on the screen that worked.</p>
<p>As I used the app myself, I started to get a feel for what was missing. I&rsquo;d think, &ldquo;It would be cool if I could see a chart of my spending this month.&rdquo; So, I&rsquo;d add a chart. Then I&rsquo;d think, &ldquo;I wish I could categorize my expenses.&rdquo; So, I&rsquo;d add categories.</p>
<p>The development process was a continuous conversation between me, the user, and the code. The &ldquo;vibe&rdquo; of the project guided my decisions. If a feature felt too complicated or didn&rsquo;t align with the core vision of simplicity, I&rsquo;d scrap it.</p>
<p>Git was a magic tool in this process. Before each commit, I had to make sure that none of the existing features would break and the design on web and mobile was not compromised. This also taught me how to incrementally build a basic working PWA, then focus on the UI, then the UX, and finally other details. This iterative process of building, testing, and committing was crucial to the project&rsquo;s success.</p>
<p><strong>Pros of Vibecoding:</strong></p>
<ul>
<li><strong>Creative Freedom:</strong> It&rsquo;s incredibly liberating to not be constrained by a rigid plan. You can follow your creative instincts and explore new ideas as they come up.</li>
<li><strong>Organic Evolution:</strong> The product evolves in a very natural way, based on real usage and feedback.</li>
<li><strong>High Motivation:</strong> Working on what feels right at the moment is a great way to stay motivated and engaged.</li>
</ul>
<p><strong>Cons of Vibecoding:</strong></p>
<ul>
<li><strong>Lack of Predictability:</strong> It&rsquo;s hard to estimate timelines or even what the final product will look like. This makes it unsuitable for projects with strict deadlines or multiple stakeholders.</li>
<li><strong>Risk of Scope Creep:</strong> Without a clear plan, it&rsquo;s easy to keep adding features and never reach a &ldquo;finished&rdquo; state.</li>
<li><strong>Potential for Architectural Issues:</strong> Making design decisions on the fly can lead to a messy codebase if you&rsquo;re not careful. It requires a good sense of when to refactor and clean up.</li>
<li><strong>Risk of Project Reset:</strong> The fluid nature of vibecoding can be dangerous. If you&rsquo;re not careful, especially when using powerful CLIs, you might accept permissions that could reset your work. While Git is a lifesaver, it&rsquo;s a critical point to be mindful of.</li>
</ul>
<h3 id="tips-for-successful-vibecoding">Tips for Successful Vibecoding</h3>
<ul>
<li><strong>Embrace Version Control:</strong> Use Git religiously and commit often. It’s your best friend and safety net in a fluid development process.</li>
<li><strong>Start with a Core Idea:</strong> You don&rsquo;t need a detailed plan, but have a clear, simple vision for what you want to build.</li>
<li><strong>Listen to Your Gut:</strong> If a feature feels wrong or overly complicated, it probably is. Trust your intuition to guide you.</li>
<li><strong>Refactor Fearlessly:</strong> Vibecoding can get messy. Don&rsquo;t be afraid to pause and clean up your code when things start to feel disorganized.</li>
<li><strong>Stay Mindful and Present:</strong> Focus on the task at hand and be fully engaged with the code. The best ideas often come when you&rsquo;re in a state of flow.</li>
<li><strong>Keep Documentation Updated:</strong> Whether it&rsquo;s a <code>README.md</code> or an <code>agent.md</code> for AI collaborators, keep documentation current. Since AI models have token limitations, this helps them clearly understand the project&rsquo;s structure, philosophy, and goals, no matter which model you use.</li>
</ul>
<h3 id="lessons-learned-the-philosophy-of-vibecoding">Lessons Learned: The Philosophy of Vibecoding</h3>
<p>Vibecoding is not for every project or every person. It&rsquo;s best suited for personal projects, prototypes, or early-stage startups where the product vision is still evolving.</p>
<p>Whether a non-technical person can build complex systems with vibecoding is a valid doubt, and the reasons often go beyond the code itself. The building process might be manageable, but the technical hurdles of hosting, configuring domains, understanding DNS for services like GitHub Pages, and other deployment intricacies require a solid technical foundation. While there are excellent platforms that eliminate these complexities, they often come at a cost. This project, for instance, cost me nothing but my time, largely because I could navigate these technical challenges and utilize free tools like the Gemini CLI. This highlights a key aspect of vibecoding: the more technical skills you have, the more freedom and economic flexibility you have to bring your vision to life. It also reinforces the idea that in vibecoding, the developer is not just an engineer, but also a designer, a product manager, and a DevOps specialist, all rolled into one.</p>
<p>This approach challenges the traditional separation of roles and encourages a more holistic way of creating. It&rsquo;s about embracing the uncertainty and imperfection of the creative process. It&rsquo;s a reminder that sometimes, the most beautiful things are not planned, but discovered.</p>
<p>Another profound lesson is that when you are vibecoding, you are not just a coder; you are the manager. You have to know what model to use and when, how to order the tasks so they don&rsquo;t break the entire codebase, and when to take control and enforce some structure, even in a fluid process. This includes the often-neglected task of documentation. In a typical software job, you might not get to wear all these hats due to company policies and defined roles. But here, on your own project, you are the boss. You are the founder, the manager, the coder, and so much more. This level of ownership and responsibility is both daunting and incredibly empowering.</p>
<p>I am certain that with this vibecoding experience, I will be able to solve day-to-day problems with technology much faster than before. It has honed my ability to quickly prototype and iterate on ideas, to trust my gut, and to listen to the voice of the project itself.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Building Dhanika with the vibecoding approach was a deeply rewarding experience. It allowed me to create a product that is not only functional but also has a personality, a &ldquo;vibe&rdquo; that is uniquely its own. It reminded me that software development can be as much an art as it is a science. It&rsquo;s a journey of discovery, a conversation with the digital canvas, and a testament to the power of creative intuition.</p>
]]></content:encoded></item><item><title>Mastering Grep: Your Guide to Efficient Text Searching</title><link>https://md.eknath.dev/posts/shell/grep-cmd-tool/</link><pubDate>Thu, 17 Jul 2025 22:46:54 +0530</pubDate><guid>https://md.eknath.dev/posts/shell/grep-cmd-tool/</guid><description>&lt;p>In the world of the command line, &lt;code>grep&lt;/code> is a tool you&amp;rsquo;ll find indispensable. It stands for &amp;ldquo;global regular expression print,&amp;rdquo; and it&amp;rsquo;s your go-to for searching text within files. Whether you&amp;rsquo;re a developer, a system administrator, or just someone who loves the terminal, mastering &lt;code>grep&lt;/code> will significantly boost your productivity. This article, inspired by the style of my &lt;a href="./basic-bash-commands.md">Basic Bash Commands&lt;/a> reference, will guide you through the essentials and advanced uses of &lt;code>grep&lt;/code>.&lt;/p></description><content:encoded><![CDATA[<p>In the world of the command line, <code>grep</code> is a tool you&rsquo;ll find indispensable. It stands for &ldquo;global regular expression print,&rdquo; and it&rsquo;s your go-to for searching text within files. Whether you&rsquo;re a developer, a system administrator, or just someone who loves the terminal, mastering <code>grep</code> will significantly boost your productivity. This article, inspired by the style of my <a href="./basic-bash-commands.md">Basic Bash Commands</a> reference, will guide you through the essentials and advanced uses of <code>grep</code>.</p>
<h2 id="what-is-grep">What is <code>grep</code>?</h2>
<p>At its core, <code>grep</code> is a command-line utility that searches for a specific pattern of text in a file or a stream of data. If it finds a match, it will print the line containing that pattern to the console. Its power lies in its simplicity and its support for regular expressions, which allows for incredibly flexible and powerful search queries.</p>
<h2 id="basic-syntax">Basic Syntax</h2>
<p>The basic syntax for <code>grep</code> is straightforward:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#f92672">[</span>options<span style="color:#f92672">]</span> pattern <span style="color:#f92672">[</span>file...<span style="color:#f92672">]</span>
</span></span></code></pre></div><ul>
<li><code>[options]</code>: These are flags that modify the behavior of <code>grep</code>.</li>
<li><code>pattern</code>: This is the text or regular expression you are searching for.</li>
<li><code>[file...]</code>: This is the file or files you want to search in. If no file is specified, <code>grep</code> will search the standard input.</li>
</ul>
<h2 id="daily-use-cases">Daily Use Cases</h2>
<p>Here are some of the most common ways you&rsquo;ll use <code>grep</code> in your day-to-day tasks:</p>
<h3 id="simple-text-search">Simple Text Search</h3>
<p>The most basic use of <code>grep</code> is to search for a specific word in a file.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#e6db74">&#34;error&#34;</span> log.txt
</span></span></code></pre></div><p>This command will search for the word &ldquo;error&rdquo; in the <code>log.txt</code> file and print all lines that contain it.</p>
<h3 id="case-insensitive-search">Case-Insensitive Search</h3>
<p>If you want to ignore the case of the text you&rsquo;re searching for, use the <code>-i</code> option.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep -i <span style="color:#e6db74">&#34;error&#34;</span> log.txt
</span></span></code></pre></div><p>This will find &ldquo;error&rdquo;, &ldquo;Error&rdquo;, &ldquo;ERROR&rdquo;, and so on.</p>
<h3 id="searching-in-multiple-files">Searching in Multiple Files</h3>
<p>You can search for a pattern in multiple files by listing them after the pattern.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#e6db74">&#34;api_key&#34;</span> config.yml settings.py
</span></span></code></pre></div><h3 id="recursive-search">Recursive Search</h3>
<p>To search for a pattern in all files within a directory and its subdirectories, use the <code>-r</code> option.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep -r <span style="color:#e6db74">&#34;database_url&#34;</span> .
</span></span></code></pre></div><p>This is incredibly useful for finding where a particular variable or function is used in a large project.</p>
<h2 id="medium-complexity-use-cases">Medium Complexity Use Cases</h2>
<p>Once you&rsquo;re comfortable with the basics, you can start using <code>grep</code> for more complex tasks.</p>
<h3 id="inverting-the-search">Inverting the Search</h3>
<p>If you want to find all the lines that <em>don&rsquo;t</em> contain a pattern, use the <code>-v</code> option.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep -v <span style="color:#e6db74">&#34;success&#34;</span> log.txt
</span></span></code></pre></div><p>This is useful for filtering out noise from log files.</p>
<h3 id="counting-matches">Counting Matches</h3>
<p>To count the number of lines that match a pattern, use the <code>-c</code> option.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep -c <span style="color:#e6db74">&#34;warning&#34;</span> log.txt
</span></span></code></pre></div><h3 id="showing-line-numbers">Showing Line Numbers</h3>
<p>To display the line number of each match, use the <code>-n</code> option.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep -n <span style="color:#e6db74">&#34;TODO&#34;</span> *.py
</span></span></code></pre></div><p>This helps you quickly jump to the relevant line in your code editor.</p>
<h2 id="advanced-grep-with-regular-expressions">Advanced <code>grep</code> with Regular Expressions</h2>
<p>The true power of <code>grep</code> is unlocked when you use it with regular expressions. Here are a few examples:</p>
<h3 id="matching-the-start-and-end-of-a-line">Matching the Start and End of a Line</h3>
<p>You can use <code>^</code> to match the beginning of a line and <code>$</code> to match the end.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#e6db74">&#34;^import&#34;</span> *.py  <span style="color:#75715e"># Find all lines that start with &#34;import&#34;</span>
</span></span><span style="display:flex;"><span>grep <span style="color:#e6db74">&#34;)</span>$<span style="color:#e6db74">&#34;</span> *.js      <span style="color:#75715e"># Find all lines that end with &#34;)&#34;</span>
</span></span></code></pre></div><h3 id="matching-any-character">Matching Any Character</h3>
<p>The <code>.</code> character in a regular expression matches any single character.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#e6db74">&#34;gr.p&#34;</span> words.txt <span style="color:#75715e"># Matches &#34;grep&#34;, &#34;grip&#34;, &#34;grap&#34;, etc.</span>
</span></span></code></pre></div><h3 id="using-character-classes">Using Character Classes</h3>
<p>You can use character classes to match a set of characters.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#e6db74">&#34;[aeiou]&#34;</span> text.txt <span style="color:#75715e"># Find all lines with at least one vowel</span>
</span></span></code></pre></div><h2 id="combining-grep-with-other-commands">Combining <code>grep</code> with Other Commands</h2>
<p><code>grep</code> is often used with other commands to create powerful command-line pipelines.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ps aux | grep <span style="color:#e6db74">&#34;nginx&#34;</span> <span style="color:#75715e"># Find all running processes with &#34;nginx&#34; in their name</span>
</span></span></code></pre></div><p>This command takes the output of <code>ps aux</code> and uses <code>grep</code> to filter it.</p>
<h2 id="conclusion">Conclusion</h2>
<p><code>grep</code> is a versatile and powerful tool that is essential for anyone who works with the command line. From simple text searches to complex pattern matching with regular expressions, <code>grep</code> can handle it all.</p>
<p>Thank you</p>
]]></content:encoded></item><item><title>Making Your Terminal Fancy on Linux and macOS</title><link>https://md.eknath.dev/posts/shell/fancy-terminal-ui/</link><pubDate>Mon, 23 Jun 2025 10:00:00 +0530</pubDate><guid>https://md.eknath.dev/posts/shell/fancy-terminal-ui/</guid><description>&lt;blockquote>
&lt;p>&amp;ldquo;My terminal looks really bad, while some people have a really cool CLI starting page with their name etc. What should I do so mine looks cool too?&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>If you&amp;rsquo;re spending a lot of time in your terminal, why not make it a pleasant and productive environment? A well-customized terminal can not only look cool but also boost your productivity. This guide will walk you through various ways to transform your bland terminal into a powerful and visually appealing workspace on Linux and macOS.&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>&ldquo;My terminal looks really bad, while some people have a really cool CLI starting page with their name etc. What should I do so mine looks cool too?&rdquo;</p>
</blockquote>
<p>If you&rsquo;re spending a lot of time in your terminal, why not make it a pleasant and productive environment? A well-customized terminal can not only look cool but also boost your productivity. This guide will walk you through various ways to transform your bland terminal into a powerful and visually appealing workspace on Linux and macOS.</p>
<h2 id="1-the-shell-prompt-ps1">1. The Shell Prompt (PS1)</h2>
<p>The shell prompt is the first thing you see. Customizing it can provide useful information at a glance.</p>
<h3 id="basic-customization">Basic Customization</h3>
<p>You can customize your prompt by modifying the <code>PS1</code> environment variable in your shell&rsquo;s configuration file (<code>~/.bashrc</code> for Bash, <code>~/.zshrc</code> for Zsh).</p>
<p>For example, to show your username, hostname, and current working directory, you can add this to your config file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>export PS1<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;\u@\h:\w\$ &#34;</span>
</span></span></code></pre></div><h3 id="advanced-prompt-customization-with-starship">Advanced Prompt Customization with Starship</h3>
<p>For a more powerful and visually rich prompt, you can use tools like <a href="https://starship.rs/">Starship</a>. Starship is a cross-shell prompt that is fast, customizable, and works on Linux, macOS, and Windows.</p>
<p><strong>Installation:</strong></p>
<ul>
<li><strong>Linux (and macOS with Homebrew):</strong>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew install starship
</span></span></code></pre></div></li>
<li><strong>Other Linux:</strong>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -sS https://starship.rs/install.sh | sh
</span></span></code></pre></div></li>
</ul>
<p><strong>Configuration:</strong></p>
<p>Add the following to the end of your <code>~/.bashrc</code> or <code>~/.zshrc</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-bash" data-lang="bash"><span style="display:flex;"><span>eval <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>starship init bash<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#75715e"># for Bash</span>
</span></span><span style="display:flex;"><span>eval <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>starship init zsh<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>  <span style="color:#75715e"># for Zsh</span>
</span></span></code></pre></div><p>Starship is highly configurable. You can find more information in the <a href="https://starship.rs/config/">official documentation</a>.</p>
<h2 id="2-terminal-emulators">2. Terminal Emulators</h2>
<p>The terminal emulator is the application you use to interact with the shell. While the default terminal emulators on Linux and macOS are functional, there are better alternatives with more features and customization options.</p>
<ul>
<li><strong>iTerm2 (macOS):</strong> A powerful replacement for the default Terminal app on macOS. It offers features like split panes, search, and extensive customization.</li>
<li><strong>Alacritty:</strong> A fast, cross-platform, OpenGL terminal emulator.</li>
<li><strong>Kitty:</strong> A feature-rich and hackable GPU-based terminal emulator.</li>
</ul>
<h2 id="3-color-schemes">3. Color Schemes</h2>
<p>A good color scheme can reduce eye strain and make your terminal more readable.</p>
<ul>
<li><strong>Dracula:</strong> A popular dark theme for many applications, including terminals.</li>
<li><strong>Solarized:</strong> A theme with both light and dark variants, designed for readability.</li>
<li><strong>Nord:</strong> A clean and elegant theme with a focus on clarity.</li>
</ul>
<p>Most terminal emulators have built-in support for changing color schemes. You can also find collections of themes online, like <a href="https://iterm2colorschemes.com/">iTerm2 Color Schemes</a>.</p>
<h2 id="4-fonts-with-ligatures">4. Fonts with Ligatures</h2>
<p>Using a font designed for programming can improve readability. Fonts with ligatures combine multiple characters into a single symbol, which can make your code look cleaner.</p>
<ul>
<li><strong>Fira Code:</strong> A popular free monospaced font with programming ligatures.</li>
<li><strong>JetBrains Mono:</strong> A free and open-source font for developers.</li>
<li><strong>Cascadia Code:</strong> A fun, new monospaced font from Microsoft that includes programming ligatures.</li>
</ul>
<p>After installing a font, you&rsquo;ll need to configure your terminal emulator to use it.</p>
<h2 id="5-cool-cli-tools">5. Cool CLI Tools</h2>
<p>Here are some tools that can make your terminal more informative and user-friendly:</p>
<ul>
<li>
<p><strong>Neofetch:</strong> A command-line system information tool that displays a logo of your OS along with system information.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># macOS</span>
</span></span><span style="display:flex;"><span>brew install neofetch
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Linux (Debian/Ubuntu)</span>
</span></span><span style="display:flex;"><span>sudo apt-get install neofetch
</span></span></code></pre></div><p>Add <code>neofetch</code> to the end of your <code>~/.bashrc</code> or <code>~/.zshrc</code> to see it every time you open a new terminal.</p>
</li>
<li>
<p><strong>lsd or exa:</strong> Modern replacements for the <code>ls</code> command with more features and better defaults.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># lsd (macOS)</span>
</span></span><span style="display:flex;"><span>brew install lsd
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># exa (macOS)</span>
</span></span><span style="display:flex;"><span>brew install exa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># lsd (Linux)</span>
</span></span><span style="display:flex;"><span>sudo apt-get install lsd
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># exa (Linux)</span>
</span></span><span style="display:flex;"><span>sudo apt-get install exa
</span></span></code></pre></div><p>You can alias <code>ls</code> to <code>lsd</code> or <code>exa</code> in your shell&rsquo;s configuration file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alias ls<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;lsd&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># or</span>
</span></span><span style="display:flex;"><span>alias ls<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;exa&#39;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>bat:</strong> A <code>cat</code> clone with syntax highlighting and Git integration.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># macOS</span>
</span></span><span style="display:flex;"><span>brew install bat
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Linux</span>
</span></span><span style="display:flex;"><span>sudo apt-get install bat
</span></span></code></pre></div><p>You can alias <code>cat</code> to <code>bat</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-bash" data-lang="bash"><span style="display:flex;"><span>alias cat<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;bat&#39;</span>
</span></span></code></pre></div></li>
<li>
<p><strong>zoxide:</strong> A smarter <code>cd</code> command that learns your habits.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># macOS</span>
</span></span><span style="display:flex;"><span>brew install zoxide
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Linux</span>
</span></span><span style="display:flex;"><span>curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash
</span></span></code></pre></div><p>Add the following to your <code>~/.bashrc</code> or <code>~/.zshrc</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-bash" data-lang="bash"><span style="display:flex;"><span>eval <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>zoxide init bash<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span> <span style="color:#75715e"># for Bash</span>
</span></span><span style="display:flex;"><span>eval <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>zoxide init zsh<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>  <span style="color:#75715e"># for Zsh</span>
</span></span></code></pre></div></li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>By combining these tools and techniques, you can create a terminal environment that is not only beautiful but also tailored to your workflow. Experiment with different tools and configurations to find what works best for you. A personalized terminal can make your command-line experience much more enjoyable and productive.</p>
]]></content:encoded></item><item><title>Abstraction in Software Development</title><link>https://md.eknath.dev/posts/software-development/abstractions-and-its-responsibility/</link><pubDate>Sun, 22 Jun 2025 10:25:15 +0530</pubDate><guid>https://md.eknath.dev/posts/software-development/abstractions-and-its-responsibility/</guid><description>&lt;p>In the world of software development, abstraction is one of the most powerful tools at our disposal. Yet, it is often taken for granted—seen merely as a technical concept or a convenience. In truth, abstraction is much more than that; I agree it&amp;rsquo;s a confusing topic for some, but it represents a currency of trust and a gateway to creative problem-solving.&lt;/p>
&lt;h2 id="abstraction-as-trust">Abstraction as Trust&lt;/h2>
&lt;p>At its core, every abstraction is built on trust. When we build or use a library, framework, API, or even a programming language, we trust the people or systems who designed it. We trust that these abstractors have already handled the intricate details—the &amp;ldquo;hows&amp;rdquo;—so that we don’t have to.&lt;/p></description><content:encoded><![CDATA[<p>In the world of software development, abstraction is one of the most powerful tools at our disposal. Yet, it is often taken for granted—seen merely as a technical concept or a convenience. In truth, abstraction is much more than that; I agree it&rsquo;s a confusing topic for some, but it represents a currency of trust and a gateway to creative problem-solving.</p>
<h2 id="abstraction-as-trust">Abstraction as Trust</h2>
<p>At its core, every abstraction is built on trust. When we build or use a library, framework, API, or even a programming language, we trust the people or systems who designed it. We trust that these abstractors have already handled the intricate details—the &ldquo;hows&rdquo;—so that we don’t have to.</p>
<p>Just as currency enables transactions by guaranteeing value, abstraction enables development by guaranteeing that, if we use it correctly, it will do its part. Each layer of abstraction makes an implicit promise:</p>
<blockquote>
<p>“I will handle this piece of complexity for you. You can rely on me.”</p>
</blockquote>
<p>This trust extends even to our own past work. If you designed a library years ago with effective separation of concerns, you don&rsquo;t need to recall its minute details to use it today. This promise allows developers to stand on the shoulders of those who came before, building ever more complex and capable systems without getting lost in the weeds of low-level implementation.</p>
<h2 id="abstraction-frees-the-mind">Abstraction Frees the Mind</h2>
<p>The true beauty of abstraction lies in its ability to liberate the developer’s mind. Instead of worrying about how a particular task is implemented under the hood—how data is stored, how a network packet is transmitted, how a sorting algorithm works—we can focus on higher-level concerns:</p>
<ul>
<li>What problem am I solving?</li>
<li>What is the best design for this situation?</li>
<li>How can I create value for the user?</li>
</ul>
<p>This mental freedom accelerates development time. By outsourcing the details to well-designed abstractions, we create space and set priority for creativity, strategy, and architectural thinking.</p>
<h2 id="examples-all-around-us">Examples All Around Us</h2>
<p>Abstraction is everywhere in software:</p>
<ul>
<li><strong>Operating systems abstract hardware complexities</strong> so we don’t write programs in assembly language for each device.</li>
<li><strong>The Retrofit HTTP client for Android</strong> takes away overhead complexities like opening and closing socket connections, formatting headers, handling encryption, and parsing responses.</li>
<li><strong>Database Object-Relational Mappers (ORMs)</strong> abstract SQL so we can think in terms of objects or models.</li>
</ul>
<p>And abstraction exists beyond software:</p>
<ul>
<li>We trust that flipping a light switch will illuminate a room without understanding the engineering behind electrical circuits, springs, or materials.</li>
<li>We drive cars without needing to know how combustion engines or electric motors work.</li>
<li>We post letters following established protocols, and the rest is handled by the postal system; we need not worry about how it&rsquo;s moved from one place to another, where it stops, or other intricacies.</li>
</ul>
<h2 id="the-developers-responsibility">The Developer’s Responsibility</h2>
<p>Of course, with trust comes responsibility. Every time we use an abstraction, we are relying on someone else’s work to solve part of our problem—and that trust should not be blind. As developers, we must approach abstractions thoughtfully and skillfully.</p>
<p>Here’s what that means in practice:</p>
<ul>
<li>Choose abstractions wisely.</li>
<li>Understand their limitations and guarantees.</li>
<li>Respect the contract they offer and use them as intended.</li>
<li><strong>Know when to peek beneath the abstraction.</strong></li>
</ul>
<p>Sometimes, it is necessary to peek beneath an abstraction’s surface, especially when debugging or optimizing. But most of the time, abstraction serves as the foundation that allows us to build faster, smarter, and better.</p>
<h2 id="not-all-interfaces-are-abstractions--differentiating-abstractions">Not all interfaces are abstractions — differentiating abstractions</h2>
<p>When I first learned about abstraction, I assumed that every interface represented an abstraction. But over time, I realized that’s not always the case.</p>
<ul>
<li>An interface is simply a contract—a way to define what methods or properties should exist.</li>
<li>Abstraction, on the other hand, goes beyond that. It hides internal complexity and provides a simpler way to achieve a goal.</li>
</ul>
<p>For something to qualify as a meaningful abstraction, it must do more than just define methods. It must:</p>
<ul>
<li>Fulfill a purpose in the easiest way possible</li>
<li>Hide unnecessary complexity</li>
<li>Offer a clean and reliable interface that lets developers focus on what they want to achieve, not how it’s implemented</li>
</ul>
<p>Take libraries like <strong>Retrofit</strong> or <strong>Ktor</strong> as examples.
These libraries have been thoughtfully designed to abstract away the complexities of network communication—things like:</p>
<ul>
<li>Opening and managing connections</li>
<li>Formatting HTTP requests and parsing responses</li>
<li>Handling retries, timeouts, redirects, and errors</li>
<li>Managing threading and background execution</li>
</ul>
<p>As app developers, we don’t need to worry about these internal details. We simply declare what API we want to talk to, and the library takes care of the rest.</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">@GET</span>(<span style="color:#e6db74">&#34;users&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getUsers</span>(): List&lt;User&gt;
</span></span></code></pre></div><p>That’s abstraction at work: you focus on what you need (the list of users) and trust that the library handles the how (HTTP calls, JSON parsing, etc.).</p>
<p>Not all interfaces are abstractions, but every good abstraction provides an interface—one that lets you work at a higher level of thinking, free from unnecessary details.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Abstraction is more than a coding technique—it is a profound enabler of progress. By trusting in the work of others and leveraging the abstractions they create, we free ourselves to focus on what truly matters: solving problems, creating value, and pushing the boundaries of what software can do.</p>
]]></content:encoded></item><item><title>Ollama &amp; Goose cli - Offline Agent setup</title><link>https://md.eknath.dev/posts/ai-ml/local-coding-agents/</link><pubDate>Fri, 20 Jun 2025 19:47:13 +0530</pubDate><guid>https://md.eknath.dev/posts/ai-ml/local-coding-agents/</guid><description>&lt;h2 id="fair-warning">Fair Warning&lt;/h2>
&lt;p>Make sure you are using a capable system — ideally with a powerful CPU, GPU, and adequate cooling — before running large language models locally. LLMs can consume significant resources, generate substantial heat, and may cause system instability or damage if your hardware isn’t up to the task. Please proceed with caution!&lt;/p>
&lt;p>This guide walks you through setting up Ollama (with deepseek-r1-goose) and Goose CLI.&lt;/p>
&lt;h2 id="step-1-download--install-ollama">Step 1: Download &amp;amp; Install Ollama&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>curl -fsSL https://ollama.com/install.sh | sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or, if you prefer to download manually then checkout:&lt;a href="https://ollama.com/download">https://ollama.com/download&lt;/a>
&lt;a href="https://md.eknath.dev/posts/shell/command-line-tools/#ollama---local-and-opensource-llms">Ollama-SetUpGuide&lt;/a>&lt;/p></description><content:encoded><![CDATA[<h2 id="fair-warning">Fair Warning</h2>
<p>Make sure you are using a capable system — ideally with a powerful CPU, GPU, and adequate cooling — before running large language models locally. LLMs can consume significant resources, generate substantial heat, and may cause system instability or damage if your hardware isn’t up to the task. Please proceed with caution!</p>
<p>This guide walks you through setting up Ollama (with deepseek-r1-goose) and Goose CLI.</p>
<h2 id="step-1-download--install-ollama">Step 1: Download &amp; Install Ollama</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -fsSL https://ollama.com/install.sh | sh
</span></span></code></pre></div><p>Or, if you prefer to download manually then checkout:<a href="https://ollama.com/download">https://ollama.com/download</a>
<a href="https://md.eknath.dev/posts/shell/command-line-tools/#ollama---local-and-opensource-llms">Ollama-SetUpGuide</a></p>
<h2 id="step-2pull-and-run-the-model">Step 2:Pull and Run the model</h2>
<p>The following model is optimized for the agent we are going to install next so lets pull and run the model:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ollama run michaelneale/deepseek-r1-goose
</span></span></code></pre></div><p>more info about the model is available here <a href="https://www.ollama.com/michaelneale/deepseek-r1-goose">https://www.ollama.com/michaelneale/deepseek-r1-goose</a></p>
<h2 id="step-3-download-and-configure-goose-cli">Step 3: Download And configure Goose-cli</h2>
<p>Download the goose via HomeBrew if you don&rsquo;t have it installed please check this article <a href="https://md.eknath.dev/posts/shell/command-line-tools/#homebrew">HomeBrew-SetUpGuide</a></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew install block-goose-cli
</span></span></code></pre></div><p>now you can run the goose</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>goose 
</span></span></code></pre></div><p>Running after the first installation, the configure menu will be shown, make sure you select <code>Ollama</code> as the model provider, you can navigate by up and down arrow and hit return/enter to select the option.</p>
<p>After the model provider, next comes the model selection option, just type <code>michaelneale/deepseek-r1-goose</code> and hit return/enter.</p>
<p>Later if you want to change the model you can always run <code>bash goose configure</code>, i would recommend you use this model others are not working as expected this is already slow.</p>
<p>You can stop goose by given <code>/exit</code> command.</p>
<h2 id="where-offline-agents-work-best">Where Offline Agents Work Best</h2>
<ul>
<li>The task is narrow, well-defined, and focused.</li>
<li>You want fast, private processing without sending data to the cloud.</li>
<li>You are working with small to moderate inputs and outputs, as local models may struggle with large contexts or long conversations on limited hardware.</li>
<li>Context limit required is less than 32K tokens</li>
</ul>
<p>Again, always monitor your system’s health, and don’t hesitate to stop the model if things heat up!</p>
]]></content:encoded></item><item><title>Recommended Command Line Tools</title><link>https://md.eknath.dev/posts/shell/command-line-tools/</link><pubDate>Tue, 17 Jun 2025 22:46:54 +0530</pubDate><guid>https://md.eknath.dev/posts/shell/command-line-tools/</guid><description>&lt;p>The terminal is already a powerful tool, but the right command-line utilities can make it even better. Here are a few essential tools I use daily that you might find helpful too.&lt;/p>
&lt;h2 id="homebrew">HomeBrew&lt;/h2>
&lt;p>Homebrew is a package manager for macOS (and Linux) that lets you easily install, update, and manage software and developer tools from the command line.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"># Installation command&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>/bin/bash -c &lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span style="color:#66d9ef">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After running the command, restart your terminal, and you&amp;rsquo;re good to go. Before we start installing, let&amp;rsquo;s understand the difference between a &amp;ldquo;formula&amp;rdquo; and a &amp;ldquo;cask&amp;rdquo; in Homebrew.&lt;/p></description><content:encoded><![CDATA[<p>The terminal is already a powerful tool, but the right command-line utilities can make it even better. Here are a few essential tools I use daily that you might find helpful too.</p>
<h2 id="homebrew">HomeBrew</h2>
<p>Homebrew is a package manager for macOS (and Linux) that lets you easily install, update, and manage software and developer tools from the command line.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Installation command</span>
</span></span><span style="display:flex;"><span>/bin/bash -c <span style="color:#e6db74">&#34;</span><span style="color:#66d9ef">$(</span>curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>After running the command, restart your terminal, and you&rsquo;re good to go. Before we start installing, let&rsquo;s understand the difference between a &ldquo;formula&rdquo; and a &ldquo;cask&rdquo; in Homebrew.</p>
<p><strong>Formula:</strong> Command-line tools, libraries, and languages (e.g., Kotlin, Go). No special flags are needed when using <code>brew</code> commands with formulas. <a href="https://formulae.brew.sh/formula/">All Formulas</a></p>
<p><strong>Cask:</strong> GUI applications like Chrome, VSCode, etc. These require a <code>--cask</code> flag for <code>brew</code> operations. <a href="https://formulae.brew.sh/cask/">Cask Directory</a></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew install git             <span style="color:#75715e"># Installing a formula</span>
</span></span><span style="display:flex;"><span>brew install --cask firefox  <span style="color:#75715e"># Installing a cask</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>brew uninstall foo           <span style="color:#75715e"># Remove a package</span>
</span></span><span style="display:flex;"><span>brew upgrade foo             <span style="color:#75715e"># Upgrade a specific package</span>
</span></span><span style="display:flex;"><span>brew list                    <span style="color:#75715e"># See all installed packages</span>
</span></span><span style="display:flex;"><span>brew search foo              <span style="color:#75715e"># Find available packages</span>
</span></span><span style="display:flex;"><span>brew info foo                <span style="color:#75715e"># Get details about a package</span>
</span></span><span style="display:flex;"><span>brew update                  <span style="color:#75715e"># Update Homebrew&#39;s package list</span>
</span></span><span style="display:flex;"><span>brew upgrade                 <span style="color:#75715e"># Upgrade all outdated packages</span>
</span></span></code></pre></div><p>🔗 <a href="https://brew.sh/">HomeBrew-Official</a></p>
<h2 id="ollama---local-and-opensource-llms">Ollama - Local and OpenSource LLMs</h2>
<p>This is an incredibly useful tool that I find myself using constantly. Local models are fantastic for answering general or timeless questions where you need accuracy without requiring up-to-the-minute data. They’re fast, private, work offline, and have another big advantage: 🌱 a reduced environmental impact.</p>
<p><strong>Instructions:</strong></p>
<ol>
<li>Install Ollama by running the official script in your terminal. If you prefer, you can also download it manually from the <a href="https://ollama.com/download">official site</a>.
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -fsSL https://ollama.com/install.sh | sh
</span></span></code></pre></div></li>
<li>Browse the <a href="https://ollama.com/search">Model Library</a> to find a model. Keep an eye on the size, as some models can be several gigabytes.</li>
<li>Once you&rsquo;ve picked a model, pull it using the <code>pull</code> command. For example, to get the Llama 3 model:
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ollama pull llama3
</span></span></code></pre></div></li>
<li>After the download is complete, you can run the model to start a chat session. To exit, just type <code>/bye</code>.
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ollama run llama3
</span></span></code></pre></div></li>
</ol>
<p>That&rsquo;s it! Now, anytime you have a question, you can just run <code>ollama run &lt;model_name&gt;</code> to interact with your local LLM.</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ollama serve</code></td>
          <td>Starts the Ollama server (for API access).</td>
      </tr>
      <tr>
          <td><code>ollama create &lt;model&gt;</code></td>
          <td>Creates a new model from a Modelfile.</td>
      </tr>
      <tr>
          <td><code>ollama show &lt;model&gt;</code></td>
          <td>Displays details about a model.</td>
      </tr>
      <tr>
          <td><code>ollama run &lt;model&gt;</code></td>
          <td>Runs a model for interactive chat.</td>
      </tr>
      <tr>
          <td><code>ollama pull &lt;model&gt;</code></td>
          <td>Downloads a model from the library.</td>
      </tr>
      <tr>
          <td><code>ollama list</code></td>
          <td>Lists all downloaded models.</td>
      </tr>
      <tr>
          <td><code>ollama ps</code></td>
          <td>Shows currently running models.</td>
      </tr>
      <tr>
          <td><code>ollama stop &lt;model&gt;</code></td>
          <td>Stops a running model.</td>
      </tr>
      <tr>
          <td><code>ollama rm &lt;model&gt;</code></td>
          <td>Removes a model from your system.</td>
      </tr>
  </tbody>
</table>
<p>Here is an example of how you can download and run another model in one line:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ollama pull mistral <span style="color:#f92672">&amp;&amp;</span> ollama run mistral
</span></span></code></pre></div><p>🔗 <a href="https://ollama.com/">Ollama</a></p>
<h2 id="gemini-cli">Gemini-cli</h2>
<p>Gemini CLI is an open-source tool that brings the power of Google&rsquo;s Gemini models directly into your terminal. It provides lightweight, direct access to the API, making it a versatile utility for a wide range of tasks, from coding and content generation to problem-solving and research.</p>
<h3 id="installation">Installation</h3>
<ol>
<li>Ensure you have Node.js (version 18+). You can check with <code>node -v</code>. If you don&rsquo;t have it, install it with Homebrew:
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew install node
</span></span></code></pre></div></li>
<li>Install the Gemini CLI globally using npm:
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install -g @google/gemini-cli
</span></span></code></pre></div></li>
<li>Run the setup and authentication command:
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>gemini
</span></span></code></pre></div>Follow the prompts to choose a theme and log in with your Google account.</li>
</ol>
<p><strong>Troubleshooting Tip:</strong> On some systems, the authentication flow in the terminal might not complete. If this happens, run <code>gemini</code> again, and then check your default web browser for the Google authentication page to complete the login.</p>
<p>⚠️ Unlike Ollama, only the <em>tool</em> is open-source, not the model. Your prompts and data will be processed by Google&rsquo;s servers.</p>
<p>⚠️ The free tier has a rate limit (e.g., 60 requests per minute). You can upgrade to a paid plan to overcome this.</p>
<h3 id="common-commands">Common Commands</h3>
<ul>
<li><code>/chat save &lt;name&gt;</code>: Saves your current chat with a memorable name.</li>
<li><code>/chat list</code>: Lists all your saved chats.</li>
<li><code>/chat resume &lt;name&gt;</code>: Resumes a saved chat session.</li>
<li><code>/compress</code>: Replaces the current chat context with a summary to save tokens.</li>
<li><code>/auth</code>: Manages your authentication settings.</li>
<li><code>/quit</code> or <code>/exit</code>: Exits the tool.</li>
</ul>
<h3 id="switching-to-shell-mode">Switching to Shell Mode</h3>
<p>You can input <code>!</code> to toggle shell mode. For example, type <code>!pwd</code> to see your current directory. The theme will change to indicate you&rsquo;re in shell mode. To return to the chat, just input <code>!</code> again.</p>
<h3 id="providing-context">Providing Context</h3>
<p>Use the <code>@</code> symbol to provide file or directory context for your prompts.</p>
<ul>
<li><code>@path/to/your/file.txt Explain this text file.</code></li>
<li><code>@src/my_project/ Summarize the code in this directory.</code></li>
</ul>
<p>You can find more commands in the <a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/cli/commands.md">official documentation</a>.</p>
<p><a href="https://github.com/google-gemini/gemini-cli?tab=readme-ov-file">Gemini CLI-Github repo</a></p>
<h2 id="duckduckgo">DuckDuckGo</h2>
<p>Sometimes you just need to verify things quickly with a search engine. <code>ddgr</code> brings the DuckDuckGo search engine to your CLI, delivering fast results.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Installation</span>
</span></span><span style="display:flex;"><span>brew install ddgr
</span></span></code></pre></div><h3 id="common-commands-1">Common Commands</h3>
<p>These are the commands you&rsquo;ll likely use most often:</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ddgr &lt;query&gt;</code></td>
          <td>Perform a search.</td>
      </tr>
      <tr>
          <td><code>ddgr -n &lt;num&gt; &lt;query&gt;</code></td>
          <td>Limit the number of search results.</td>
      </tr>
      <tr>
          <td><code>ddgr -j &lt;query&gt;</code></td>
          <td>Open the first result directly in the browser.</td>
      </tr>
      <tr>
          <td><code>ddgr -w &lt;site&gt; &lt;query&gt;</code></td>
          <td>Restrict the search to a specific site.</td>
      </tr>
      <tr>
          <td><code>ddgr -s &lt;region&gt; &lt;query&gt;</code></td>
          <td>Set the search region (e.g., <code>us-en</code>).</td>
      </tr>
      <tr>
          <td><code>ddgr -x &lt;query&gt;</code></td>
          <td>Display URLs only, without opening a browser.</td>
      </tr>
      <tr>
          <td><code>ddgr -C &lt;query&gt;</code></td>
          <td>Colorize the output for easier reading.</td>
      </tr>
      <tr>
          <td><code>ddgr -l</code></td>
          <td>List your search history.</td>
      </tr>
      <tr>
          <td><code>ddgr -c</code></td>
          <td>Clear your search history.</td>
      </tr>
      <tr>
          <td><code>ddgr --disable-safe</code></td>
          <td>Disable safe search filtering.</td>
      </tr>
      <tr>
          <td><code>ddgr -h</code></td>
          <td>Show the help menu.</td>
      </tr>
  </tbody>
</table>
<h3 id="navigation-commands">navigation Commands</h3>
<p>When results are displayed, you can use these keys to navigate:</p>
<table>
  <thead>
      <tr>
          <th>Shortcut / Action</th>
          <th>What it does</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>[number]</code></td>
          <td>Open the result with that number in your browser.</td>
      </tr>
      <tr>
          <td><code>n</code> or <code>N</code></td>
          <td>Show the next page of results.</td>
      </tr>
      <tr>
          <td><code>p</code> or <code>P</code></td>
          <td>Show the previous page of results.</td>
      </tr>
      <tr>
          <td><code>o [numbers]</code></td>
          <td>Open multiple results (e.g., <code>o 1 3 5</code>).</td>
      </tr>
      <tr>
          <td><code>q</code></td>
          <td>Quit ddgr.</td>
      </tr>
  </tbody>
</table>
<p>Here’s an example I use extensively:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Shows 5 results from GitHub for &#34;awesome shell scripts&#34;</span>
</span></span><span style="display:flex;"><span>ddgr -n <span style="color:#ae81ff">5</span> -w github.com <span style="color:#e6db74">&#34;awesome shell scripts&#34;</span>
</span></span></code></pre></div><h3 id="profile-configuration">Profile Configuration</h3>
<p>You can customize <code>ddgr</code>&rsquo;s default behavior by editing the <code>~/.ddgrrc</code> file (<code>vi ~/.ddgrrc</code>). Here are some of the options you can configure:</p>
<table>
  <thead>
      <tr>
          <th>Option</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>-n &lt;number&gt;</code></td>
          <td>Set the default number of search results per page.</td>
      </tr>
      <tr>
          <td><code>-C</code></td>
          <td>Enable colorized output.</td>
      </tr>
      <tr>
          <td><code>--disable-safe</code></td>
          <td>Disable safe search.</td>
      </tr>
      <tr>
          <td><code>-x</code></td>
          <td>Show URLs only by default.</td>
      </tr>
      <tr>
          <td><code>-r &lt;browser&gt;</code></td>
          <td>Set the browser for opening results (e.g., <code>firefox</code>).</td>
      </tr>
      <tr>
          <td><code>-s &lt;region&gt;</code></td>
          <td>Set the default search region (e.g., <code>us-en</code>).</td>
      </tr>
      <tr>
          <td><code>-w &lt;site&gt;</code></td>
          <td>Restrict searches to a specific site by default.</td>
      </tr>
      <tr>
          <td><code>--json</code></td>
          <td>Output results in JSON format (useful for scripting).</td>
      </tr>
  </tbody>
</table>
<p>🔗 <a href="https://github.com/jarun/ddgr">ddgr-Repo</a></p>
<h2 id="git-cli">Git CLI</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew install git
</span></span></code></pre></div><p>I&rsquo;m sure you&rsquo;re familiar with the basic git commands, but you can create powerful aliases to make your workflow faster. For example, you can run <code>git st</code> instead of <code>git status</code>.</p>
<ol>
<li>Find your global git config file by running <code>git config --list --show-origin</code>.</li>
<li>Open that file (e.g., <code>vi ~/.gitconfig</code>) and add your aliases.</li>
</ol>
<p>Here is an example <code>~/.gitconfig</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[credential]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">helper</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">store</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[user]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">Eganathan R
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    email = md-email@gmail.com</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[filter &#34;lfs&#34;]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">smudge</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">git-lfs smudge -- %f
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    process = git-lfs filter-process
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    required = true
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    clean = git-lfs clean -- %f</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[init]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">defaultBranch</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">master</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[http]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">proxy</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">http://127.0.0.1:xxxx</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[alias]</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">st</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">status
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    co = checkout
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    br = branch
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    ci = commit
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    lg = log --oneline --graph --decorate # Pretty and compact log view</span>
</span></span></code></pre></div><h2 id="podman-optional">Podman (Optional)</h2>
<p>Podman is a powerful, daemonless(It does not require a background service) container engine for developing, managing, and running containers. It provides a command-line interface that is compatible with Docker, making it an excellent alternative for container management without requiring a central daemon, i most use podman only for experimentation.</p>
<h3 id="installation-1">Installation</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew install podman
</span></span></code></pre></div><p>After installation, you may need to initialize a Podman machine, which is a lightweight virtual machine for running containers on macOS.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>podman machine init
</span></span><span style="display:flex;"><span>podman machine start
</span></span></code></pre></div><h3 id="common-commands-2">Common Commands</h3>
<p>Many Podman commands are identical to their Docker counterparts, so you can often use <code>podman</code> as a drop-in replacement for <code>docker</code>.</p>
<table>
  <thead>
      <tr>
          <th>Command</th>
          <th>Description</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>podman pull &lt;image&gt;</code></td>
          <td>Pull an image from a container registry.</td>
      </tr>
      <tr>
          <td><code>podman push &lt;image&gt;</code></td>
          <td>Push an image to a container registry.</td>
      </tr>
      <tr>
          <td><code>podman build -t &lt;tag&gt; .</code></td>
          <td>Build an image from a Dockerfile.</td>
      </tr>
      <tr>
          <td><code>podman images</code></td>
          <td>List all local images.</td>
      </tr>
      <tr>
          <td><code>podman run &lt;image&gt;</code></td>
          <td>Run a command in a new container.</td>
      </tr>
      <tr>
          <td><code>podman ps</code></td>
          <td>List all running containers.</td>
      </tr>
      <tr>
          <td><code>podman ps -a</code></td>
          <td>List all containers (running and stopped).</td>
      </tr>
      <tr>
          <td><code>podman stop &lt;container&gt;</code></td>
          <td>Stop one or more running containers.</td>
      </tr>
      <tr>
          <td><code>podman rm &lt;container&gt;</code></td>
          <td>Remove one or more containers.</td>
      </tr>
      <tr>
          <td><code>podman rmi &lt;image&gt;</code></td>
          <td>Remove one or more images.</td>
      </tr>
      <tr>
          <td><code>podman machine list</code></td>
          <td>List available Podman virtual machines.</td>
      </tr>
      <tr>
          <td><code>podman machine stop</code></td>
          <td>Stop the Podman virtual machine.</td>
      </tr>
  </tbody>
</table>
<p>🔗 <a href="https://podman.io/">Podman-Official</a></p>
<p>I&rsquo;m certain this will help you. There are many other interesting command-line tools out there (like <code>starship</code>, <code>tmux</code>), but I consider them more for customization.</p>
<p>Thanks for reading, and have a great day ☺️</p>
]]></content:encoded></item><item><title>Basic Bash Commands</title><link>https://md.eknath.dev/posts/shell/basic-bash-commands/</link><pubDate>Wed, 11 Jun 2025 20:46:54 +0530</pubDate><guid>https://md.eknath.dev/posts/shell/basic-bash-commands/</guid><description>&lt;p>One of my mentors, &lt;a href="https://linktr.ee/rwxrob">RWX-Rob&lt;/a>, runs online bootcamps called Boost, where he shares tech industry standards. A key lesson he emphasizes is the importance of learning Linux and working with its Bash command-line. Mastering the terminal has helped me save time and stay focused. These are my quick reference notes — not an exhaustive list, but the commands I use most often and im sure it will be useful for you too.&lt;/p></description><content:encoded><![CDATA[<p>One of my mentors, <a href="https://linktr.ee/rwxrob">RWX-Rob</a>, runs online bootcamps called Boost, where he shares tech industry standards. A key lesson he emphasizes is the importance of learning Linux and working with its Bash command-line. Mastering the terminal has helped me save time and stay focused. These are my quick reference notes — not an exhaustive list, but the commands I use most often and im sure it will be useful for you too.</p>
<p>btw BASH is short for Bourne Again SHell, just in case some one asks, so lets move it:</p>
<h2 id="mac-switching-between-zsh-and-bash">Mac Switching between Zsh and Bash</h2>
<p>As a software developer, choose <strong>Bash</strong> if you&rsquo;re new to shell scripting due to its familiarity and abundance of online resources, while opting for <strong>Zsh</strong> for improved performance, customization, and
security features, keeping in mind that switching to Zsh might require adapting to some differences when working with Linux distributions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chsh -s /bin/bash  <span style="color:#75715e"># switch to bash</span>
</span></span><span style="display:flex;"><span>chsh -s /bin/zsh   <span style="color:#75715e"># Switch to zsh</span>
</span></span></code></pre></div><h2 id="identity">Identity</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pwd             <span style="color:#75715e"># Print current working directory</span>
</span></span><span style="display:flex;"><span>whoami          <span style="color:#75715e"># Show current user</span>
</span></span><span style="display:flex;"><span>clear           <span style="color:#75715e"># Clear the terminal screen</span>
</span></span><span style="display:flex;"><span>history         <span style="color:#75715e"># Show command history</span>
</span></span></code></pre></div><h2 id="file--directory-navigation">File &amp; Directory Navigation</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>ls              <span style="color:#75715e"># List files</span>
</span></span><span style="display:flex;"><span>ls -la          <span style="color:#75715e"># List all files with details</span>
</span></span><span style="display:flex;"><span>cd /path/to/dir <span style="color:#75715e"># Change directory</span>
</span></span><span style="display:flex;"><span>cd ..           <span style="color:#75715e"># Go up one directory</span>
</span></span><span style="display:flex;"><span>cd -            <span style="color:#75715e"># Go to previous directory</span>
</span></span></code></pre></div><h2 id="file-operations">File Operations</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>touch file.txt              <span style="color:#75715e"># Create a new empty file</span>
</span></span><span style="display:flex;"><span>mkdir folder                <span style="color:#75715e"># Create a new directory</span>
</span></span><span style="display:flex;"><span>cp file1.txt file2.txt      <span style="color:#75715e"># Copy file or dir</span>
</span></span><span style="display:flex;"><span>mv file1.txt file2.txt      <span style="color:#75715e"># Rename or move file or dir</span>
</span></span><span style="display:flex;"><span>rm file.txt                 <span style="color:#75715e"># Delete file </span>
</span></span><span style="display:flex;"><span>rm -r folder                <span style="color:#75715e"># Delete directory recursively</span>
</span></span></code></pre></div><h2 id="searching--finding">Searching &amp; Finding</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>grep <span style="color:#e6db74">&#34;text&#34;</span> file.txt        <span style="color:#75715e"># Search for text in a file</span>
</span></span><span style="display:flex;"><span>grep -r <span style="color:#e6db74">&#34;text&#34;</span> .            <span style="color:#75715e"># Recursive search in directory</span>
</span></span><span style="display:flex;"><span>find . -name <span style="color:#e6db74">&#34;*.sh&#34;</span>         <span style="color:#75715e"># Find all .sh files</span>
</span></span></code></pre></div><h2 id="editing-files">Editing Files</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>vi file.txt                 <span style="color:#75715e"># Open file in Vim editor (i don&#39;t like nano sorry!) </span>
</span></span><span style="display:flex;"><span>cat file.txt                <span style="color:#75715e"># Print file content</span>
</span></span><span style="display:flex;"><span>less file.txt               <span style="color:#75715e"># Scroll through file</span>
</span></span><span style="display:flex;"><span>head file.txt               <span style="color:#75715e"># First 10 lines</span>
</span></span><span style="display:flex;"><span>tail file.txt               <span style="color:#75715e"># Last 10 lines</span>
</span></span></code></pre></div><ul>
<li>Vim Editor has its own commands and pallets, will add my reference here.</li>
</ul>
<h2 id="manual">Manual</h2>
<p>The <code>man</code> command provides access to the manual pages for other commands, offering detailed information on their usage and options.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>man ls <span style="color:#75715e"># show the manual for ls command</span>
</span></span></code></pre></div><h2 id="permissions">Permissions</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>chmod +x script.sh          <span style="color:#75715e"># Make script executable</span>
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">755</span> file              <span style="color:#75715e"># Set permissions (owner rwx, others rx)</span>
</span></span><span style="display:flex;"><span>chown user:group file       <span style="color:#75715e"># Change ownership</span>
</span></span><span style="display:flex;"><span>ls -l file                  <span style="color:#75715e"># Get info of file permissions and owner etc</span>
</span></span><span style="display:flex;"><span>ls -ld folder               <span style="color:#75715e"># Get info of folder permissions and owner etc</span>
</span></span></code></pre></div><h3 id="permissions-string-quick-reference">Permissions String Quick Reference</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>-rw-r--r-- <span style="color:#ae81ff">1</span> user group <span style="color:#ae81ff">1234</span> Jun <span style="color:#ae81ff">16</span> 19:00 myfile.txt <span style="color:#75715e"># Example output for ls -l file check the table for ref</span>
</span></span><span style="display:flex;"><span>drwxr-xr-x <span style="color:#ae81ff">2</span> user group <span style="color:#ae81ff">4096</span> Jun <span style="color:#ae81ff">16</span> 19:00 mydir <span style="color:#75715e"># Example output for ls -ld folder check the table for ref</span>
</span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>Position</th>
          <th>Meaning</th>
          <th>Example</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1st char</td>
          <td>File type (<code>-</code> file, <code>d</code> directory, <code>l</code> symlink)</td>
          <td><code>-</code> = file, <code>d</code> = directory</td>
      </tr>
      <tr>
          <td>2-4</td>
          <td>Owner permissions (read <code>r</code>, write <code>w</code>, execute <code>x</code>)</td>
          <td><code>rwx</code> = owner can read, write, execute</td>
      </tr>
      <tr>
          <td>5-7</td>
          <td>Group permissions</td>
          <td><code>r-x</code> = group can read, execute</td>
      </tr>
      <tr>
          <td>8-10</td>
          <td>Others permissions</td>
          <td><code>r--</code> = others can only read</td>
      </tr>
  </tbody>
</table>
<h2 id="scripts--variables">Scripts &amp; Variables</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>echo <span style="color:#e6db74">&#34;Hello, </span>$USER<span style="color:#e6db74">&#34;</span>         <span style="color:#75715e"># Sample Bash script</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Variables</span>
</span></span><span style="display:flex;"><span>name<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;Eganathan&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Hi, </span>$name<span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><h2 id="loops--conditions">Loops &amp; Conditions</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># If</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[</span> -f <span style="color:#e6db74">&#34;file.txt&#34;</span> <span style="color:#f92672">]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;Exists&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># For loop</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> f in *.txt; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>  echo <span style="color:#e6db74">&#34;</span>$f<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><h2 id="time-savers">Time Savers</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>!!              <span style="color:#75715e"># Repeat last command</span>
</span></span><span style="display:flex;"><span>!abc            <span style="color:#75715e"># Run last command starting with &#39;abc&#39;</span>
</span></span><span style="display:flex;"><span>Ctrl + R        <span style="color:#75715e"># Reverse search command history</span>
</span></span><span style="display:flex;"><span>Ctrl + L        <span style="color:#75715e"># Clear screen (same as `clear`)</span>
</span></span><span style="display:flex;"><span>Ctrl + A / E    <span style="color:#75715e"># Move to beginning / end of line</span>
</span></span></code></pre></div><h2 id="date-time">Date Time</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>date <span style="color:#75715e"># Print current date and time</span>
</span></span><span style="display:flex;"><span>date +<span style="color:#e6db74">&#34;%T&#34;</span>        <span style="color:#75715e"># Print current time in 24-hour format (HH:MM:SS)</span>
</span></span><span style="display:flex;"><span>date +<span style="color:#e6db74">&#34;%r&#34;</span>        <span style="color:#75715e"># Print current time in 12-hour format with AM/PM</span>
</span></span><span style="display:flex;"><span>date +<span style="color:#e6db74">&#34;%F&#34;</span>        <span style="color:#75715e"># Print current date in YYYY-MM-DD format</span>
</span></span><span style="display:flex;"><span>date +<span style="color:#e6db74">&#34;%d-%m-%Y&#34;</span>  <span style="color:#75715e"># Print current date in custom format: Day Month Year</span>
</span></span><span style="display:flex;"><span>date -u           <span style="color:#75715e"># Print current UTC time</span>
</span></span></code></pre></div><h2 id="use-alias-to-create-shortcuts">Use alias to create shortcuts</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alias gs<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;git status&#34;</span> <span style="color:#75715e"># hope you have git installed</span>
</span></span><span style="display:flex;"><span>alias ..<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;cd ..&#34;</span> <span style="color:#75715e"># this have saved a quite a lot of time for me</span>
</span></span></code></pre></div><h2 id="combine-commands">Combine commands</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>command1 <span style="color:#f92672">&amp;&amp;</span> command2  <span style="color:#75715e"># Run command2 only if command1 succeeds</span>
</span></span><span style="display:flex;"><span>command1 <span style="color:#f92672">||</span> command2  <span style="color:#75715e"># Run command2 only if command1 fails</span>
</span></span></code></pre></div><h2 id="copy-file-contents">copy file contents</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cat file.txt | pbcopy
</span></span></code></pre></div><h2 id="cleaning">Cleaning</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>df -h           <span style="color:#75715e"># Disk usage</span>
</span></span><span style="display:flex;"><span>du -sh *        <span style="color:#75715e"># Folder sizes</span>
</span></span><span style="display:flex;"><span>top             <span style="color:#75715e"># Real-time process list</span>
</span></span><span style="display:flex;"><span>ps aux | grep xyz  <span style="color:#75715e"># Check if a process is running</span>
</span></span></code></pre></div><h2 id="configuration-files">Configuration Files</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Bash</span>
</span></span><span style="display:flex;"><span>cat ~/.bash_profile   <span style="color:#75715e"># Main configuration file</span>
</span></span><span style="display:flex;"><span>cat ~/.bashrc         <span style="color:#75715e"># Profile configuration file</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Zsh (Mac)</span>
</span></span><span style="display:flex;"><span>cat ~/.zshrc          <span style="color:#75715e"># Main configuration file</span>
</span></span><span style="display:flex;"><span>cat ~/.zprofile       <span style="color:#75715e"># Profile configuration file</span>
</span></span></code></pre></div><p>These files persist aliases or environment variables and your personal scripts, this is really helpful to customize the shell for your taste</p>
<p>✅ Use the Main configuration file for environment variables like JAVA PATH and others.
✅ Use the Profile Configuration File for aliases and other similar settings.</p>
<p>⚠️ Once you add your alias or update the profile configuration, you will need to re-start the terminal for the new configuration to come into effect.</p>
<p>I keep a copy of the profile configuration in git so i have access to my configurations both on my work and other systems if i need em.</p>
<h2 id="common-commandline-tool-that-is-used-often-by-me">Common commandline tool that is used often by me</h2>
<ul>
<li>homebrew - package manager like npm</li>
<li>ddgr - search from the commandline (DuckDuckGo)</li>
<li>ollama - for local offline AI models for simple tasks and quires.</li>
<li>zip/unzip – Compress/uncompress</li>
<li>curl - API testing and others</li>
</ul>
<blockquote>
<p>&ldquo;Warning: Terminal use may cause excessive productivity and happiness.&rdquo; - Ollama 3.5</p>
</blockquote>
]]></content:encoded></item><item><title>Google IO 2025 Updates Filtered for Android Developers</title><link>https://md.eknath.dev/posts/android/google-io-2025-updates-for-android-developers/</link><pubDate>Mon, 26 May 2025 16:01:45 +0530</pubDate><guid>https://md.eknath.dev/posts/android/google-io-2025-updates-for-android-developers/</guid><description>&lt;p>Google I/O 2025 just wrapped up, and as usual, it delivered a wave of exciting announcements and updates for Android developers. From revolutionary testing tools to enhanced IDE features and broader device support, this year&amp;rsquo;s I/O promises to streamline workflows and unlock new possibilities, if you missed and like to watch them,(here is the playlist)[https://youtube.com/playlist?list=PLWz5rJ2EKKc86SrjccwTtBzH4Ptu3Mrai&amp;amp;si=1BMDinkRFfi4dL9S]&lt;/p>
&lt;hr>
&lt;h2 id="journeys-reshaping-end-to-end-testing">Journeys: Reshaping End-to-End Testing&lt;/h2>
&lt;p>One of the standout announcements that immediately resonated with me was Journeys. This new feature in Android Studio is poised to transform how we approach end-to-end testing, making it significantly easier to write and maintain robust tests.&lt;/p></description><content:encoded><![CDATA[<p>Google I/O 2025 just wrapped up, and as usual, it delivered a wave of exciting announcements and updates for Android developers. From revolutionary testing tools to enhanced IDE features and broader device support, this year&rsquo;s I/O promises to streamline workflows and unlock new possibilities, if you missed and like to watch them,(here is the playlist)[https://youtube.com/playlist?list=PLWz5rJ2EKKc86SrjccwTtBzH4Ptu3Mrai&amp;si=1BMDinkRFfi4dL9S]</p>
<hr>
<h2 id="journeys-reshaping-end-to-end-testing">Journeys: Reshaping End-to-End Testing</h2>
<p>One of the standout announcements that immediately resonated with me was Journeys. This new feature in Android Studio is poised to transform how we approach end-to-end testing, making it significantly easier to write and maintain robust tests.</p>
<p>At its core, Journeys allows you to describe test steps and assertions using natural language. Imagine writing: &ldquo;Navigate to product details screen,&rdquo; &ldquo;Add item to cart,&rdquo; &ldquo;Verify total price is X.&rdquo; This natural language approach is a game-changer, abstracting away the complex boilerplate often associated with UI testing frameworks.</p>
<p>What&rsquo;s truly exciting is the flexibility: you can create a journey with text descriptions or simply record your steps directly within the IDE. Even better, you can record and update these journey logs on the fly. This means testing is no longer a separate, arduous task, but an integrated, fluid part of the development process. For anyone who&rsquo;s wrestled with flaky or hard-to-maintain end-to-end tests, Journeys feels like a breath of fresh air – basically, testing made easy!</p>
<p>You can learn more about this groundbreaking feature here: <a href="https://developer.android.com/studio/preview/gemini/journeys">https://developer.android.com/studio/preview/gemini/journeys</a></p>
<h2 id="agent-tab-in-android-studio-your-intelligent-coding-companion">Agent Tab in Android Studio: Your Intelligent Coding Companion</h2>
<p>Know the &ldquo;Junie&rdquo; or &ldquo;Firebender&rdquo; ? Well, Google has taken that concept and integrated it directly into Android Studio with the new Agent Tab. This is Google&rsquo;s own IDE tooling, designed to act as an intelligent coding companion, much like we&rsquo;ve seen with AI-powered assistants.</p>
<p>This integration signifies a massive leap forward in developer productivity, allowing for a more seamless and intelligent development experience. Imagine getting proactive suggestions and even automated fixes for common issues – a true time-saver!</p>
<h2 id="wireless-pairing-finally-seamless-debugging">Wireless Pairing: Finally Seamless Debugging</h2>
<p>This is one of those quality-of-life improvements that will bring a collective sigh of relief to many Android developers like me. The persistent headaches with wireless pairing in Android Studio have been addressed!</p>
<p>Now, once you turn ON wireless debugging mode on your phone and ensure both the studio and phone are connected to the same network, your device will automatically get listed within the Android Studio IDE. This means seamless connection to your mobile device for debugging, without the need for manual IP input or frustrating connection attempts. It&rsquo;s a small change, but one that drastically improves the daily workflow for many.</p>
<h2 id="inspect-play-store-policy-insights-staying-compliant-made-easier">Inspect Play Store Policy Insights: Staying Compliant Made Easier</h2>
<p>Navigating Play Store policies can sometimes feel like walking through a minefield, now with Inspect Play Store Policy Insights this headache will be simplified. While details are still emerging, this suggests new tooling or insights directly within the developer console to help us understand and adhere to Play Store policies more effectively. This could be invaluable for proactive compliance checks and avoiding costly policy violations and shadow banning.</p>
<h2 id="scaling-across-screens-the-adaptive-mindset">Scaling Across Screens: The Adaptive mindset</h2>
<p>There was an emphasis on making our Android app shine across every screen a user might encounter. This isn&rsquo;t just about phones anymore. We&rsquo;re talking about a vast and expanding ecosystem that includes:</p>
<ul>
<li>Phones (of course)</li>
<li>Innovative Foldables</li>
<li>Tablets</li>
<li>ChromeOS devices (Chromebooks)</li>
<li>Automotive displays (Android Automotive)</li>
<li>And the emerging world of XR devices</li>
</ul>
<p>This collective ecosystem now unlocks a staggering opportunity, estimated at around 500 million screens. The takeaway is clear: building adaptive experiences isn&rsquo;t just a best practice; it&rsquo;s a fundamental strategy for reaching and retaining users.</p>
<p>The overarching message was: you don&rsquo;t need to rebuild your app for each form factor. Instead, the **focus is on making small, iterative changes to your existing mobile app so it runs well across this diverse range of devices. This adaptive mindset lays a strong foundation for future devices and allows for differentiation specific to certain form factors when needed.</p>
<h3 id="firebase-streaming-expanded-device-support">Firebase Streaming: Expanded Device Support</h3>
<p>For developers relying on Firebase for real-time data streaming and debugging, there&rsquo;s good news on the device front. Firebase Streaming now officially supports a wider range of popular Android devices, including Oppo, Samsung, Xiaomi, Vivo, and OnePlus. This expanded compatibility means more developers can leverage Firebase&rsquo;s powerful streaming capabilities across a broader user base, ensuring consistent experiences and improved debugging.</p>
<h2 id="ui-designer-for-your-weekend-projects-stitch">UI Designer for Your Weekend Projects: Stitch</h2>
<p>And for those creative sparks and weekend hackathon warriors, Google introduced Stitch, a new UI designer tool. What makes Stitch particularly interesting is its ease of use for rapid prototyping and design. The best part? You can even copy your designs directly to Figma, bridging the gap between quick ideation and more polished design workflows. This is a fantastic resource for quickly bringing UI ideas to life without deep design tool expertise. Check it out here: <a href="https://stitch.withgoogle.com">https://stitch.withgoogle.com</a></p>
<h3 id="final-thoughts">Final Thoughts</h3>
<p>Google I/O 2025 has truly set the stage for an exciting year in Android development. From the revolutionary testing paradigm offered by Journeys to the intelligent assistance of the Agent Tab and the practical improvements in wireless debugging, the focus is clearly on enhancing developer productivity and streamlining workflows.</p>
<p>The broader device support for Firebase and new design tools like Stitch further empower us to build richer, more robust applications. It&rsquo;s a fantastic time to be an Android developer, and I can&rsquo;t wait to get my hands on these new tools and see what we can build!</p>
<p>Feel free to connect and share your suggestion:
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong></p>
]]></content:encoded></item><item><title>Mastering Raw Queries in Room: Why, When &amp; When Not to Use Them (#OF06)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-6/</link><pubDate>Sun, 25 May 2025 08:18:39 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-6/</guid><description>&lt;p>In the &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">previous article&lt;/a>, we explored the power and flexibility of DAOs with Room’s built-in query annotations. But what if your query needs don’t fit into Room’s constraints? Enter @RawQuery – the most flexible and dangerous tool in the Room arsenal.&lt;/p>
&lt;p>Let’s dive into what &lt;code>@RawQuery&lt;/code> is, when it shines and shuns.&lt;/p>
&lt;hr>
&lt;h2 id="what-is-rawquery">What is @RawQuery?&lt;/h2>
&lt;p>&lt;code>@RawQuery&lt;/code> allows you to execute &lt;strong>SQL statements that aren’t validated at compile time&lt;/strong>. Unlike &lt;code>@Query&lt;/code>, which Room parses and validates during compilation, raw queries are evaluated at runtime.&lt;/p></description><content:encoded><![CDATA[<p>In the <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">previous article</a>, we explored the power and flexibility of DAOs with Room’s built-in query annotations. But what if your query needs don’t fit into Room’s constraints? Enter @RawQuery – the most flexible and dangerous tool in the Room arsenal.</p>
<p>Let’s dive into what <code>@RawQuery</code> is, when it shines and shuns.</p>
<hr>
<h2 id="what-is-rawquery">What is @RawQuery?</h2>
<p><code>@RawQuery</code> allows you to execute <strong>SQL statements that aren’t validated at compile time</strong>. Unlike <code>@Query</code>, which Room parses and validates during compilation, raw queries are evaluated at runtime.</p>
<p>You typically use <code>@RawQuery</code> with either SupportSQLiteQuery (for fully dynamic queries) or plain String (though the latter is limited and discouraged).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Dao</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ProductDao</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@RawQuery</span> 
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductsWithRawQuery</span>(query: SupportSQLiteQuery): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// This will be called from the repository
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Transaction</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductsAbovePrice</span>(minPrice: Int): List&lt;Product&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> query = SimpleSQLiteQuery(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;SELECT * FROM products WHERE price &gt; ?&#34;</span>,
</span></span><span style="display:flex;"><span>        arrayOf(minPrice)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> productDao.getProductsWithRawQuery(query)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="-when-should-you-use-rawquery">✅ When Should You Use @RawQuery?</h3>
<p>Despite its risks, there are legitimate use cases for <code>@RawQuery</code> which come handy when the app is completely offline and need to do operations similar to server with available data for example sorting, filtering and searching etc.</p>
<p>There are some cases where you need advanced joins, unions, subqueries Room’s <code>@Query</code> might fall short an example i can think of are expense list screen where you need to get associated budgets,tags, users who created and approved them, etc getting this merged data specific filter and sort type will be extremely hard with conventional methods in cases like these <code>@RawQuery</code> are a boon.</p>
<p>This is one of my dao&rsquo;s functions that is triggered when the there applies a filter, since there was no network i apply the filter and show the available data assuming the user already knows he is in offline mode,we have an indicator that should handle conveying of the message. So take a look and the params and tell me can we acheve this using conventional method faster than this ? i don&rsquo;t think so but do share your views.</p>
<p>Lets check out my code for</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@RawQuery</span> <span style="color:#75715e">// Query runner
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getExpensesWithParamsQueryRunner</span>(query: SupportSQLiteQuery): List&lt;LocalExpenseWithDetails&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Transaction</span> <span style="color:#75715e">// this will be called as a fallback option when offline
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getExpensesWithParams</span>(
</span></span><span style="display:flex;"><span>        type: String?,
</span></span><span style="display:flex;"><span>        budgetId: Long?,
</span></span><span style="display:flex;"><span>        sessionId: Long?,
</span></span><span style="display:flex;"><span>        filterParam: FilterParams,
</span></span><span style="display:flex;"><span>        sortParam: SortParam,
</span></span><span style="display:flex;"><span>        searchQuery: String?,
</span></span><span style="display:flex;"><span>        page: Int
</span></span><span style="display:flex;"><span>    ): List&lt;LocalExpenseWithDetails&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">val</span> typeId = <span style="color:#66d9ef">if</span> (type <span style="color:#f92672">!=</span> <span style="color:#66d9ef">null</span> <span style="color:#f92672">&amp;&amp;</span> type <span style="color:#f92672">!=</span> <span style="color:#a6e22e">BaseExpenseType</span>.ALL) getExpenseTypeWithName(type.serverKey) <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> getExpensesWithParamsQueryRunner(
</span></span><span style="display:flex;"><span>            query = getExpenseQuery( <span style="color:#75715e">// query generator
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>                typeId = typeId,
</span></span><span style="display:flex;"><span>                budgetId = budgetId,
</span></span><span style="display:flex;"><span>                sessionId = sessionId,
</span></span><span style="display:flex;"><span>                filterParam = filterParam,
</span></span><span style="display:flex;"><span>                sortParam = sortParam,
</span></span><span style="display:flex;"><span>                search = searchQuery,
</span></span><span style="display:flex;"><span>                page = page
</span></span><span style="display:flex;"><span>            )
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div><ol start="3">
<li>Performance Testing / Debugging
During development, you might want to test different raw SQL statements quickly without baking them into DAOs.</li>
</ol>
<h3 id="-when-not-to-use-rawquery">⚠️ When NOT to Use @RawQuery</h3>
<p>Just because you can doesn’t mean you should. Raw queries come with trade-offs:</p>
<ul>
<li>❌ No Compile-Time Safety</li>
</ul>
<p>Room won’t validate your SQL. Typos, wrong column names, or invalid SQL will only fail at runtime – often with vague errors.</p>
<ul>
<li>❌ No Type Inference</li>
</ul>
<p>Unlike @Query, Room won’t know what result type to expect unless you specify it manually and correctly.</p>
<ul>
<li>❌ Risk of SQL Injection
If you’re concatenating SQL strings, you open yourself to SQL injection vulnerabilities. Always use parameterized queries or filter the vulnerable queries.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// Bad ❌
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> query = SimpleSQLiteQuery(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE name = &#39;</span><span style="color:#e6db74">$name</span><span style="color:#e6db74">&#39;&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Good ✅
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> query = SimpleSQLiteQuery(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE name = ?&#34;</span>, arrayOf(name))
</span></span></code></pre></div><h3 id="-tips-for-safely-using-raw-queries">📋 Tips for Safely Using Raw Queries</h3>
<ul>
<li>✅ Always use SimpleSQLiteQuery with parameterized arguments.</li>
<li>✅ Keep the query logic isolated and well-documented.</li>
<li>✅ Prefer @Query whenever possible.</li>
<li>✅ Avoid user-generated input directly in SQL strings.</li>
<li>✅ Write tests to validate dynamic query paths.</li>
</ul>
<h3 id="-anti-patterns-to-avoid">🚫 Anti-Patterns to Avoid</h3>
<ul>
<li>❌ Using raw queries as your primary query method.</li>
<li>❌ Skipping query reuse – dynamic doesn’t mean you can’t structure it.</li>
<li>❌ Using @RawQuery when @Query or DAO methods would suffice.</li>
<li>❌ Ignoring test coverage for raw query logic.</li>
</ul>
<p>I’ve even created my own query builder to simplify this process in my codebase. Whether you should use it or not depends on your needs, but I’m sharing it just so you know it exists because it was extremely useful for me. The code for my query builder can be found here: <a href="https://gist.github.com/Eganathan/6692e2ea77fe51daf02f9e34a036f1b5">CustomQueryBuilder</a></p>
<h2 id="-conclusion">🚀 Conclusion</h2>
<p><code>@RawQuery</code> is the escape hatch when Room’s abstraction becomes a cage. Use it when needed, but use it wisely. Think of it like the goto statement of Room – powerful but potentially dangerous if overused or misused.</p>
<p>In most offline-first app cases, well-structured DAOs using Room’s annotations will suffice. But in edge cases where flexibility is key, <code>@RawQuery</code> gives you that last-mile control.</p>
<p><strong>Next up, we’ll explore Query Optimization Tips to keep your Offline-First App lightning fast, even at scale.</strong></p>
<p><strong>Next we will explore Data Access Objects</strong>, Stay tuned for the next article in this series! 🚀</p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>Raw queries are sharp tools — excellent in skilled hands, but risky for the unprepared. If you’ve got a use case or edge case where @RawQuery saved your app or made something possible, I’d love to hear it!</p>
<p>Feel free to connect and share your stories:
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong><br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">Previous Article in this Series</a>
🚀 <strong>Stay tuned for Part 7!</strong> 🚀</p>
<!-- 🔖 [Next Article in this Series](https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-7/) -->]]></content:encoded></item><item><title>DAO: The Backbone of Offline-First Apps (#OF05)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-5/</link><pubDate>Sun, 09 Mar 2025 13:18:39 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-5/</guid><description>&lt;p>In our &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">previous article&lt;/a>, we covered &lt;strong>Entities&lt;/strong> in Room. Now, let’s explore &lt;strong>DAOs (Data Access Objects)&lt;/strong> – the bridge between your app and the database. DAOs allow you to execute queries that allow is to insert, update, and delete records efficiently and thats exactly what we are going to explore today.&lt;/p>
&lt;hr>
&lt;h2 id="what-is-a-dao">What is a DAO?&lt;/h2>
&lt;p>A DAO is an interface annotated with @Dao that defines methods for interacting with the database. The Room compiler generates the necessary implementation for these interfaces and its methods, which enables us to access the database efficient and type safe.&lt;/p></description><content:encoded><![CDATA[<p>In our <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">previous article</a>, we covered <strong>Entities</strong> in Room. Now, let’s explore <strong>DAOs (Data Access Objects)</strong> – the bridge between your app and the database. DAOs allow you to execute queries that allow is to insert, update, and delete records efficiently and thats exactly what we are going to explore today.</p>
<hr>
<h2 id="what-is-a-dao">What is a DAO?</h2>
<p>A DAO is an interface annotated with @Dao that defines methods for interacting with the database. The Room compiler generates the necessary implementation for these interfaces and its methods, which enables us to access the database efficient and type safe.</p>
<p>A typical DAO will look simar as the code below:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Dao</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">ProductDao</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAllProducts</span>(): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE id = :productId&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductById</span>(productId: Long): Product?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProducts</span>(products: List&lt;Product&gt;)
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">updateProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsertProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>While the <code>@Dao</code> interface is created by the developer the instance/Implementation of the DAO&rsquo;s are created by the compiler when the Database is created and the generated implementation file can be looked at the following path:</p>
<blockquote>
<p><code>app/build/generated/source/kapt/debug/YOUR_PACKAGE_NAME/database/ProductDao_Impl.java</code></p>
</blockquote>
<p>The file will contain the instance with <code>impl</code> as post fix and all the corresponding functions will have its own implementation accessing the SQLLite Database for example the <strong>select query</strong> annotated with <code>@Query</code> on the above <strong>DAO</strong> will generate an implementation code that is shared below, <strong>if you are to use SQLLite Directly this is the code you would have to write manually for each query</strong>:</p>
<p>The implementation of select query will look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-java" data-lang="java"><span style="display:flex;"><span><span style="color:#a6e22e">@Override</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">public</span> Object <span style="color:#a6e22e">getAllProducts</span>(Continuation<span style="color:#f92672">&lt;?</span> <span style="color:#66d9ef">super</span> List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;&gt;</span> continuation) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">final</span> String _sql <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;SELECT * FROM products&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">final</span> RoomSQLiteQuery _statement <span style="color:#f92672">=</span> RoomSQLiteQuery.<span style="color:#a6e22e">acquire</span>(_sql, 0);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> CoroutinesRoom.<span style="color:#a6e22e">execute</span>(__db, <span style="color:#66d9ef">false</span>, continuation, <span style="color:#66d9ef">new</span> Callable<span style="color:#f92672">&lt;</span>List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;&gt;</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">@Override</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;</span> <span style="color:#a6e22e">call</span>() <span style="color:#66d9ef">throws</span> Exception {
</span></span><span style="display:flex;"><span>            Cursor _cursor <span style="color:#f92672">=</span> DBUtil.<span style="color:#a6e22e">query</span>(__db, _statement, <span style="color:#66d9ef">false</span>, <span style="color:#66d9ef">null</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>                List<span style="color:#f92672">&lt;</span>Product<span style="color:#f92672">&gt;</span> _result <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> ArrayList<span style="color:#f92672">&lt;&gt;</span>();
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">while</span> (_cursor.<span style="color:#a6e22e">moveToNext</span>()) {
</span></span><span style="display:flex;"><span>                    Product _item <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Product();
</span></span><span style="display:flex;"><span>                    _item.<span style="color:#a6e22e">id</span> <span style="color:#f92672">=</span> _cursor.<span style="color:#a6e22e">getLong</span>(_cursor.<span style="color:#a6e22e">getColumnIndexOrThrow</span>(<span style="color:#e6db74">&#34;id&#34;</span>));
</span></span><span style="display:flex;"><span>                    _item.<span style="color:#a6e22e">name</span> <span style="color:#f92672">=</span> _cursor.<span style="color:#a6e22e">getString</span>(_cursor.<span style="color:#a6e22e">getColumnIndexOrThrow</span>(<span style="color:#e6db74">&#34;name&#34;</span>));
</span></span><span style="display:flex;"><span>                    _item.<span style="color:#a6e22e">price</span> <span style="color:#f92672">=</span> _cursor.<span style="color:#a6e22e">getFloat</span>(_cursor.<span style="color:#a6e22e">getColumnIndexOrThrow</span>(<span style="color:#e6db74">&#34;price&#34;</span>));
</span></span><span style="display:flex;"><span>                    _result.<span style="color:#a6e22e">add</span>(_item);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">return</span> _result;
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">finally</span> {
</span></span><span style="display:flex;"><span>                _cursor.<span style="color:#a6e22e">close</span>();
</span></span><span style="display:flex;"><span>                _statement.<span style="color:#a6e22e">release</span>();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Isn&rsquo;t it so nice how <strong>instead of writing 24 lines we just have to write 1</strong></p>
<p>🙏 <strong>Thank You, Android and Google Team for making this extremely robust and performant.</strong></p>
<p>Now that we got an intro on <strong>Dao</strong> and what happens behind the scenes, let&rsquo;s go through the supported query annotations.</p>
<h2 id="supported-query-annotations">Supported Query Annotations</h2>
<p>In the previous section of DAO example code, you might have noticed the annotations (<code>@Insert</code>,<code>@Delete</code>&hellip;) on each functions, these annotations help the compiler to generate query object on your behalf so we can smoothly interact with the database, Let&rsquo;s go through each of them to understand how and when to use them effectively.</p>
<h3 id="insert---insert-data-into-the-database">@Insert - Insert Data into the Database</h3>
<p>Used for <strong>inserting single or multiple entries to a table</strong>, this will <strong>skip validation of checking if a duplicate entry with the same primary id exists</strong>, so it&rsquo;s faster for insertion when you are clear that duplicate entries are impossible. In case <strong>you inset a duplicate entry the app will crash</strong> with the exception primary key already exists.</p>
<p>Here is an example of how the <code>@Insert</code> can be used:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Inserting single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Insert</span>(onConflict = <span style="color:#a6e22e">OnConflictStrategy</span>.REPLACE)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProduct</span>(product: Product)     
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Inserting multiple items
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insertProducts</span>(products: List&lt;Product&gt;)     
</span></span><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span></code></pre></div><h4 id="conflict-strategy">Conflict Strategy</h4>
<p>Defining the conflict strategy on the query tells the room how to handle the conflict effectively by default it <code>NONE</code>:</p>
<ul>
<li><code>NONE</code>: Default strategy when you insert duplicate it crashes the app.</li>
<li><code>REPLACE</code>: Keep the old data and ignore the new data.</li>
<li><code>ABORT</code>: Aborts the transaction entirely so none of the entries will be updated.</li>
<li><code>IGNORE</code>: Skip the entry and proceed to next</li>
</ul>
<p>You can set your preference as per your requirement, but if you want the behavior of <code>REPLACE</code> hold on there is another annotation that we will learn about soon that is a better option.</p>
<p>⚠️ Beware: inserting duplicate entries will crash the app if you don&rsquo;t provide a conflict strategy.</p>
<h3 id="update---modify-existing-data">@Update - Modify Existing Data</h3>
<p>Used for updating all columns of an existing row where the primary key matches, If the table does not contain a matching primary key, the update fails silently (no error or no exception) but the unmatched entries remain unchanged (they are not inserted or modified).</p>
<p>Like the <code>@insert</code> you can <code>@update</code> single or multiple entries, a sample code will look something like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Updating single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">updateProduct</span>(product: Product)   
</span></span><span style="display:flex;"><span> 
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Updating multiple items
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">updateProducts</span>(products: List&lt;Product&gt;)     
</span></span><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span></code></pre></div><p>The update overrides the provided value to matched entries so if you accidentally provide a null value, that is exactly what the matched row&rsquo;s column will be, for example, if you want to mark the product as sold-out you will think you have to send only a product object with <code>id</code> and the <code>sold</code> value but if you do that all other values like name, date, and other values will be set as null so ensure you provide all values.</p>
<p>⚠️ You are overriding all values, so ensure you provide all values in each entry. <br>
⚠️ Unmatched rows will not be inserted, no errors or exceptions will be thrown</p>
<h3 id="upsert---insert-or-update-in-one-call">@Upsert - Insert or Update in One Call</h3>
<p><code>@Upsert</code> is a combination of <code>@Insert</code> and <code>@Update</code>, if the entry matches the <strong>primary-key</strong> it updates them, otherwise it inserts them smoothly, you could use <code>@Insert</code> with <code>REPLACE</code> if your table has no primary key otherwise this is better.</p>
<p>This is an example of how the ``@Upsert` will be used in a dao</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// upsert single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsertProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// upsert bulk Item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsertProducts</span>(products: List&lt;Product&gt;)
</span></span></code></pre></div><p>One thing to note is that the conflict is handled only for the primary key, if your table has other columns that are set as unique the upsert may still fail if that rule is ever broken due to new data.</p>
<p>⚠️ Requires the primary key  <br>
⚠️ If your table has unique constraints on other columns, @Upsert may still fail.</p>
<h3 id="delete---remove-entries-from-the-database">@Delete - Remove Entries from the Database</h3>
<p>Enables you to delete single and multiple entities blissfully, if the row does not exist it fails silently which means that the entries that exist will be deleted and the other will not since it does not exist, here is how typical delete functions will look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#75715e">// delete single item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProduct</span>(product: Product)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// delete bulk Item
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProducts</span>(products: List&lt;Product&gt;)
</span></span></code></pre></div><p>I don&rsquo;t like to pass the entire entity for deletions so I prefer to use <code>@Query</code> for it so I can just pass the primary keys for deletion, but for simplicity, you can use the <code>@Delete</code> as well.</p>
<h3 id="query---the-most-powerful-annotation">@Query - The Most Powerful Annotation</h3>
<p>The <code>@Query</code> Swiss knife allows you to write custom SQL queries to interact with your database. It provides more flexibility than other annotations (<code>@Insert</code>, <code>@Update</code>, <code>@Delete</code>) and is essential for performing simple/complex operations depending on our use cases.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span><span style="color:#75715e">// bulk get
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAllProducts</span>(): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// single get 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE id = :productId&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getProductById</span>(productId: Long): Product?
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// search Query
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM products WHERE name LIKE &#39;%&#39; || :searchQuery || &#39;%&#39;&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">searchProducts</span>(searchQuery: String): List&lt;Product&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// deleting item with ID
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;DELETE FROM products WHERE id = :productId&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProductById</span>(productId: Long)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// delete multiple items
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;DELETE FROM products WHERE id IN (:productIds)&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteProductById</span>(productIds: List&lt;Long&gt;)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// deleting all entries (truncates)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;DELETE FROM products&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">deleteAllProducts</span>()
</span></span><span style="display:flex;"><span><span style="color:#f92672">..</span>.
</span></span></code></pre></div><p>Swiss Knife it is, you can do all kinds of operations with it, its gives us more flexibility but as we know &ldquo;With Great Power comes to Great Responsibility&rdquo;. Although <code>@Query</code> is powerful, it can fail in various ways due to syntax errors, missing data, type mismatches, database constraints, etc.. so use it cautiously and expect exceptions and app crashes.</p>
<h3 id="rawquery---fully-dynamic-sql-execution">@RawQuery - Fully Dynamic SQL Execution</h3>
<p>The <code>@RawQuery</code> annotation in Room allows you to write and execute fully dynamic SQL queries, will will explore this later in this series it breaks the simplicity boundary because there is no compile-time safety on this.</p>
<h2 id="-conclusion">🚀 Conclusion</h2>
<p>Data Access Objects <strong>(DAOs) are the backbone of efficient database operations in Room</strong>. They provide a clean, structured way to interact with your database while leveraging compile-time safety and reducing boilerplate SQL code. With annotations like @Insert, @Update, @Delete, @Upsert, and @Query, DAOs make it easier to perform CRUD operations while maintaining performance and maintainability.</p>
<p>By using DAOs, you ensure that your Offline-First App has a seamless data layer, enabling a smooth and efficient user experience.</p>
<p><strong>Next, we will discuss about @RawQuery and its pro&rsquo;s and cons&hellip;</strong></p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong><br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">Previous Article in this Series</a>
🚀 <strong>Stay tuned for Part 6!</strong> 🚀</p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Room Entity and its intricacies (#OF04)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-4/</link><pubDate>Sun, 02 Mar 2025 15:39:05 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-4/</guid><description>&lt;p>In our &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">previous article&lt;/a>, we explored the key components of Room. Now, let’s take a deep dive into Room Entities, their importance, and the various ways to customize them.&lt;/p>
&lt;p>&lt;strong>Entities&lt;/strong> are the foundation of Room—they define how your data is stored in the database. Properly structuring your entity ensures efficient querying, maintainability, and scalability. Let&amp;rsquo;s break it down! 🛠️&lt;/p>
&lt;h2 id="-what-is-an-entity">🏗️ What is an Entity?&lt;/h2>
&lt;p>An &lt;strong>Entity in Room represents a table in the database&lt;/strong>. Each instance of the entity corresponds to a row in the table, Room generates corresponding SQL table schema based on how you create an entity class.&lt;/p></description><content:encoded><![CDATA[<p>In our <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">previous article</a>, we explored the key components of Room. Now, let’s take a deep dive into Room Entities, their importance, and the various ways to customize them.</p>
<p><strong>Entities</strong> are the foundation of Room—they define how your data is stored in the database. Properly structuring your entity ensures efficient querying, maintainability, and scalability. Let&rsquo;s break it down! 🛠️</p>
<h2 id="-what-is-an-entity">🏗️ What is an Entity?</h2>
<p>An <strong>Entity in Room represents a table in the database</strong>. Each instance of the entity corresponds to a row in the table, Room generates corresponding SQL table schema based on how you create an entity class.</p>
<p><strong>Defining an Entity:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p><strong>Breaking It Down:</strong></p>
<ul>
<li><code>@Entity</code> tells Room that this class is a database table.</li>
<li><code>@PrimaryKey</code> is used to uniquely identify each row.</li>
<li><code>autoGenerate = true</code> ensures Room generates unique IDs automatically.</li>
<li><code>@Ignore</code> → Excludes a field from being stored in the database.</li>
</ul>
<p>Before we procced further, Lets check out the sql query generated by the room</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> LocalHabitTracker (
</span></span><span style="display:flex;"><span>    id INTEGER <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span> AUTOINCREMENT,
</span></span><span style="display:flex;"><span>    associatedHabitId INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    positionX INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    positionY INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    note TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span>);
</span></span></code></pre></div><p>Since its a simple entity it might look workable but think of adding indexes, relations and other complicated relations it can quickly get complicated and prone errors. That being said this is just to show you how room is doing most of the heavy lifting for you, lets move on to customizing our entities.</p>
<hr>
<h2 id="-customizing-entities">🔄 Customizing Entities</h2>
<p>Room provides several ways to customize entities to fit your data structure requirements. Let&rsquo;s explore these!</p>
<h3 id="1-custom-table-and-column-names">1️⃣ Custom Table and Column Names</h3>
<p>By default, Room uses the <strong>class name as the table name</strong> and <strong>variable names as column names</strong>. You can override this using annotations:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;habit_id&#34;</span>) <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;pos_x&#34;</span>) <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;pos_y&#34;</span>) <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>tableName changes the SQL table name while keeping the DataClass name as per your requiremnt.</li>
<li>@ColumnInfo(name = &ldquo;custom_name&rdquo;) changes the column name in the table.</li>
</ul>
<p>⚠️ <strong>These customizations are for the DataBase Tables which means when writing the query you must use the customized name for the query to work properly.</strong></p>
<p>This is extremely useful when you want to Name your tables and columns differently, in kotlin we use camelcase but on sql its common to use underscore to split worlds like <strong>habit_tracker</strong> instead of <strong>LocalHabitTracker</strong> it simplifies the query and expedites debugging process.</p>
<p>Using custom name for table and column is optional so if you are comfortable with the existing name you can use just that, next up lets see how can we ensure the entities can be indexed faster.</p>
<h3 id="2-indexing-for-faster-queries">2️⃣ Indexing for Faster Queries</h3>
<p>Indexes speed up query performance, especially for large datasets. Use @Index for frequently queried columns, for example here the <code>associated_habit_id</code> will be used most frequently by me in different queries so i am adding <code>indices</code> property on the <code>@Table</code> annotation, here is an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(
</span></span><span style="display:flex;"><span>    tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>,
</span></span><span style="display:flex;"><span>    indices = [Index(<span style="color:#66d9ef">value</span> = [<span style="color:#e6db74">&#34;associated_habit_id&#34;</span>])]
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;associated_habit_id&#34;</span>)<span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>Indexing <code>habit_id</code> speeds up lookup queries on this column.</li>
</ul>
<h4 id="when-should-you-use-an-index">When Should You Use an Index?</h4>
<p><strong>✅ Use indexing if:</strong></p>
<ul>
<li>The column is used frequently in WHERE, JOIN, or ORDER BY.</li>
<li>The column is a foreign key linking to another table.</li>
</ul>
<p><strong>🚫 Avoid indexing if:</strong></p>
<ul>
<li>The table is small (indexing overhead isn’t worth it).</li>
<li>The column has many unique values (like id, which is already indexed as PRIMARY KEY).</li>
</ul>
<h4 id="what-actually-happens">What actually happens?</h4>
<p>When you add an index to a column in Room, Room <strong>creates a separate data structure called a B-tree index</strong>. This makes queries that search for specific values in the indexed column much faster.</p>
<p>If you want to know more about <code>B-tree Index</code>: check this out <a href="https://en.wikipedia.org/wiki/B-tree">B-Tree Index</a></p>
<p>Now we have learned about indexing lets checkout how to ensure the columns stays unique</p>
<h3 id="3-unique-constraints">3️⃣ Unique Constraints</h3>
<p>Preventing duplicate values on an indexed column is sometimes necessary and we can achieve that by setting the property <code>unique = true</code> if the Index class, lets checkout an example to see how to apply this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(
</span></span><span style="display:flex;"><span>    tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>,
</span></span><span style="display:flex;"><span>    indices = [
</span></span><span style="display:flex;"><span>        Index(<span style="color:#66d9ef">value</span> = [<span style="color:#e6db74">&#34;entry_data&#34;</span>], unique = <span style="color:#66d9ef">true</span>),
</span></span><span style="display:flex;"><span>        Index(<span style="color:#66d9ef">value</span> = [<span style="color:#e6db74">&#34;associated_habit_id&#34;</span>])
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;associated_habit_id&#34;</span>)<span style="color:#66d9ef">val</span> associatedHabitId: Long,  
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@ColumnInfo</span>(name = <span style="color:#e6db74">&#34;entry_data&#34;</span>)<span style="color:#66d9ef">val</span> entryDate: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>Ensures <code>habit_id</code> remains unique across all rows.</li>
</ul>
<p>The <code>unique = true</code> ensures there is no duplicate value in that particular indexed row, this can be extremely helpful in some cases in our case we are building a habit tracking app where users log their habits daily. We want to ensure that a user cannot log the same habit more than once per day so this property help us to achieve that blissfully.</p>
<p>Lets move on to Keys and Relationships,i meant between the Entities 😜</p>
<h3 id="4-foreign-keys-for-relationships-foreignkey">4️⃣ Foreign Keys for Relationships (@ForeignKey)</h3>
<p>Define relationships between tables using <code>@ForeignKey</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(
</span></span><span style="display:flex;"><span>    tableName = <span style="color:#e6db74">&#34;habit_tracker&#34;</span>,
</span></span><span style="display:flex;"><span>    foreignKeys = [
</span></span><span style="display:flex;"><span>        ForeignKey(
</span></span><span style="display:flex;"><span>            entity = Habit<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>,
</span></span><span style="display:flex;"><span>            parentColumns = [<span style="color:#e6db74">&#34;id&#34;</span>],
</span></span><span style="display:flex;"><span>            childColumns = [<span style="color:#e6db74">&#34;habit_id&#34;</span>],
</span></span><span style="display:flex;"><span>            onDelete = <span style="color:#a6e22e">ForeignKey</span>.CASCADE
</span></span><span style="display:flex;"><span>        )
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> habit_id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>🔷 habit_id references id from the Habit table.
🔷 onDelete = CASCADE ensures that deleting a Habit deletes all related LocalHabitTracker records.</p>
<p>The <code>@ForeignKey</code> is a primary key of an associated entity in the context of current entity the associated entity is referred as parent entity, using <code>@ForeignKey</code> help us to create relations and trigger changes depending on the event triggered, most commonly used trigger event is <code>onDelete</code> the other one is <code>onUpdate</code> the actions supported are as follows:</p>
<h4 id="supported-actions"><strong>Supported Actions:</strong></h4>
<table>
  <thead>
      <tr>
          <th>Action</th>
          <th>Behavior on Delete/Update</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CASCADE</td>
          <td>Deletes/updates child rows automatically when the parent row is deleted/updated.</td>
      </tr>
      <tr>
          <td>SET NULL</td>
          <td>Sets the foreign key column in child rows to NULL when the parent row is deleted/updated.</td>
      </tr>
      <tr>
          <td>SET DEFAULT</td>
          <td>Sets the foreign key column in child rows to its default value when the parent row is deleted/updated.</td>
      </tr>
      <tr>
          <td>RESTRICT</td>
          <td>Prevents deletion or update of the parent row if child rows exist (throws an error).</td>
      </tr>
      <tr>
          <td>NO ACTION</td>
          <td>Similar to RESTRICT, but the check happens after the statement executes.</td>
      </tr>
  </tbody>
</table>
<p>This table should have given you a clear picture of what each action&rsquo;s behavior and next lets checkout how to select them and when to use them effectively:</p>
<h4 id="when-to-use-each-action"><strong>When to Use Each Action</strong></h4>
<table>
  <thead>
      <tr>
          <th>Action</th>
          <th>When to Use?</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CASCADE</td>
          <td>When child records must be removed/updated along with the parent.</td>
      </tr>
      <tr>
          <td>SET NULL</td>
          <td>When child records should remain but lose their reference to the parent.</td>
      </tr>
      <tr>
          <td>SET DEFAULT</td>
          <td>When child records should be assigned a default foreign key value (useful for fallback behaviors).</td>
      </tr>
      <tr>
          <td>RESTRICT</td>
          <td>When you want to prevent deletion or modification of a parent that still has child records.</td>
      </tr>
      <tr>
          <td>NO ACTION</td>
          <td>Similar to RESTRICT, but checks only after execution (rarely used).</td>
      </tr>
  </tbody>
</table>
<p>You can add any number of foreign keys so make use of it for simplifying and automate your preferred action when the parent entity is modified.</p>
<h3 id="5-embedded-objects-embedded">5️⃣ Embedded Objects (@Embedded)</h3>
<p>Instead of creating separate tables, you can embed objects inside an entity, this does not mean that the table will hold your embedded object as is, it basically flattens the properties into the Entity, first lets see how to use <code>@Embedded</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Position</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> x: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> y: Int
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@Embedded</span> <span style="color:#66d9ef">val</span> position: Position,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><ul>
<li>The Position object is embedded as separate columns (x, y) in LocalHabitTracker,</li>
</ul>
<p>This helps with <strong>structuring data efficiently without creating a separate table for nested objects</strong>, It is primarily used to flatten objects into separate columns within the same table this helps us to avoid using <code>TypeConverters</code>, extra Table, faster queries as we don&rsquo;t have to deal with JOINS and other complications.</p>
<p><strong>When to Use @Embedded:</strong></p>
<ol>
<li>You have small nested objects (e.g., Position, Address, Metadata).</li>
<li>You don’t need a separate table for the object.</li>
<li>You want to avoid TypeConverters for simple objects.</li>
<li>The embedded object is always used with the parent (it has no independent existence).</li>
</ol>
<p><strong>When to Avoid it:</strong></p>
<ol>
<li>The embedded object is large or frequently updated → Use a separate table.</li>
<li>The object has relationships with other entities → Use @Relation instead.</li>
<li>The object needs to be referenced from multiple entities → Use Foreign Keys.</li>
</ol>
<p>I hope this made sense, simply put the <code>@Embedded</code> is only a output structure, while creating the SQL query room will flatten the properties into the entity table, Now if you want to store complex objects like lets say List<String> event that is possible, lets see how to achieve it.</p>
<hr>
<h3 id="6-storing-objects-as-is-typeconverters">6️⃣ Storing Objects as is (TypeConverters)</h3>
<p>Room only supports primitive data types (Int, Long, String, Boolean, etc.) by default. If you want to store complex data types (e.g., List, Date, Enum), you need Type Converters to convert them into a format Room understands ie String, lets look at an example of @TypeConverter:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> androidx.room.TypeConverter
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.gson.Gson
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> com.google.gson.reflect.TypeToken
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> java.util.Date
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Converters</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert Date -&gt; Long (for storage)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">fromDate</span>(date: Date?): Long? {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> date<span style="color:#f92672">?.</span>time
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert Long -&gt; Date (for retrieval)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">toDate</span>(timestamp: Long?): Date? {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> timestamp<span style="color:#f92672">?.</span>let { Date(<span style="color:#66d9ef">it</span>) }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert List&lt;String&gt; -&gt; JSON String (for storage)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">fromStringList</span>(list: List&lt;String&gt;?): String {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Gson().toJson(list)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Convert JSON String -&gt; List&lt;String&gt; (for retrieval)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@TypeConverter</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">toStringList</span>(json: String?): List&lt;String&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Gson().fromJson(json, <span style="color:#66d9ef">object</span> <span style="color:#960050;background-color:#1e0010">: </span><span style="color:#a6e22e">TypeToken</span>&lt;List&lt;String&gt;&gt;() {}.type)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Once a converter is defined it must be added to the DataBase class,here is an example:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Database</span>(entities = [User<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>], version = <span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@TypeConverters</span>(Converters<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>)  <span style="color:#75715e">// Register converters
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">AppDatabase</span> : RoomDatabase() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">userDao</span>(): UserDao
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>now you can add it to the entity by annotating it with the the <code>@TypeConverter</code>, here is an example for it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>(tableName = <span style="color:#e6db74">&#34;users&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">User</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> name: String,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@TypeConverters</span>(Converters<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>) <span style="color:#75715e">// Apply TypeConverter here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> hobbies: List&lt;String&gt;
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The best use-case for this is when you want to store Enums, Dates and List of items, do note the values in the object will be stored but cannot be queried which means you can&rsquo;t search, sort or filter the values so ensure you use them only when absolutely need to like for Storing simple objects (e.g., <code>Address</code>, <code>Date</code>, <code>Enum</code>)</p>
<p>These cover the most important aspects of the <code>@Entity</code> and its properties and how to use them effectively, if you think i have missed any please feel free to add comment and ill definitely add them here so it can be helpful for me and others, before you go lets check the best practices.</p>
<hr>
<h2 id="-best-practices">🔍 Best Practices</h2>
<p>✅ Use <code>autoGenerate = true</code> for <code>@PrimaryKey</code> to avoid conflicts if you don&rsquo;t have a server that provides it.<br>
✅ Optimize query performance with <code>@Index</code>.<br>
✅ Define <code>@ForeignKey</code> relationships to maintain integrity but optional for complex cases.<br>
✅ Use <code>@Ignore</code> for transient fields that shouldn&rsquo;t be stored in the database.<br>
✅ Keep entity classes small and focused—avoid unnecessary logic inside them.<br>
✅ Ensure Proper Indexing: Index frequently queried columns to improve performance. <br>
✅ Use Primitive Types: Room doesn’t support custom objects directly. Use <code>@TypeConverter</code> if needed.</p>
<hr>
<h2 id="-conclusion">🚀 Conclusion</h2>
<p>Room Entities form the foundation of Android&rsquo;s database layer, allowing structured and efficient data management. By following best practices, you ensure scalable and maintainable database architectures.</p>
<p><strong>Next we will explore Data Access Objects</strong>, Stay tuned for the next article in this series! 🚀</p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong><br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">Previous Article in this Series</a>
🚀 <strong>Stay tuned for Part 5!</strong> 🚀</p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-5/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Key Components of Room &amp; Manually Creating A Database Instance(#OF03)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-3/</link><pubDate>Sat, 22 Feb 2025 11:23:17 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-3/</guid><description>&lt;h2 id="-intro">👋 Intro&lt;/h2>
&lt;p>On the &lt;a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">previous article&lt;/a> we have set-up the Room dependencies and plugins, Now lets get into the primary key components of a room library.&lt;/p>
&lt;p>There are three important components: &lt;strong>Entity&lt;/strong>, &lt;strong>DAO&lt;/strong>&amp;rsquo;s, and &lt;strong>Database&lt;/strong>.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Entity&lt;/strong> represents a single row of a table. (Table structure)&lt;/li>
&lt;li>&lt;strong>DAO&lt;/strong>&amp;rsquo;s (Data Access Objects) are interfaces to write queries and define operations.&lt;/li>
&lt;li>&lt;strong>Database&lt;/strong> is where you associate entities and include the DAO&amp;rsquo;s you&amp;rsquo;d like to access from outside.&lt;/li>
&lt;/ul>
&lt;p>Let&amp;rsquo;s check it out one by one! 🔍&lt;/p></description><content:encoded><![CDATA[<h2 id="-intro">👋 Intro</h2>
<p>On the <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">previous article</a> we have set-up the Room dependencies and plugins, Now lets get into the primary key components of a room library.</p>
<p>There are three important components: <strong>Entity</strong>, <strong>DAO</strong>&rsquo;s, and <strong>Database</strong>.</p>
<ul>
<li><strong>Entity</strong> represents a single row of a table. (Table structure)</li>
<li><strong>DAO</strong>&rsquo;s (Data Access Objects) are interfaces to write queries and define operations.</li>
<li><strong>Database</strong> is where you associate entities and include the DAO&rsquo;s you&rsquo;d like to access from outside.</li>
</ul>
<p>Let&rsquo;s check it out one by one! 🔍</p>
<hr>
<h2 id="-entities-defining-your-data-structure">🏗️ Entities: Defining Your Data Structure</h2>
<p>Any class annotated with <code>@Entity</code> is called a entity, it represents a table in a database, while designing the entity ensure you follow <a href="https://www.geeksforgeeks.org/normal-forms-in-dbms/">Normalizations Rules</a> to keep it simple and future proof. Let&rsquo;s look checkout a sample entity in a Habit Tracker app.</p>
<p><strong>Example:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Entity</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">data</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">LocalHabitTracker</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">@PrimaryKey</span>(autoGenerate = <span style="color:#66d9ef">true</span>) <span style="color:#66d9ef">val</span> id: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> associatedHabitId: Long,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionX: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> positionY: Int,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">val</span> note: String
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p><strong>Key Points:</strong></p>
<p>✅ An entity <strong>must have</strong> at least one parameter annotated with @PrimaryKey. <br>
✅ The <strong>primary key</strong> must be unique the <code>primaryKey</code> column can&rsquo;t have a duplicate key. <br>
✅ If you want Room to <strong>auto-generate</strong> the primary key, set autoGenerate = true otherwise to false. <br>
✅ Stick to <strong>primitive types</strong> like Long or Int as PrimaryKey.</p>
<p>The entity can be further customized with <strong>table names, indexing, custom column names, keys and more</strong>, which we’ll cover in next article. 🎯</p>
<h2 id="-daos-your-data-gateway">🔗 DAOs: Your Data Gateway</h2>
<p>DAOs or Data Access Objects serve as the interface between your app and the database, providing an abstraction layer. This is a bridge between you the developer and the Database all your interactions you intend to have table must be defined here later at compile time the compiler will generate the concrete classes for these at compile time, lets see an example how to define these interactions.</p>
<p><strong>✍️ Example:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Dao</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">HabitTrackerDao</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Query to get all habit trackers
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM LocalHabitTracker&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getAll</span>(): List&lt;LocalHabitTracker&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Query to get a habit tracker by ID
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Query</span>(<span style="color:#e6db74">&#34;SELECT * FROM LocalHabitTracker WHERE id = :id&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">getById</span>(id: Long): LocalHabitTracker
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Insert a new row into the table (fails if duplicate)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Insert</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">insert</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Update an existing row
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Update</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">update</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Upsert: If exists, update; otherwise, insert
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Upsert</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">upsert</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Delete a row from the table
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#a6e22e">@Delete</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">suspend</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">delete</span>(habitTracker: LocalHabitTracker)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Best Practices:</strong></p>
<p>✅ <strong>One entity per DAO:</strong> It’s best to separate interaction with each table into its own DAO for maintainability.  <br>
✅ <strong>Keep queries optimized</strong> to avoid performance issues as your app scales.  <br>
✅ <strong>Inheritance</strong> if some of the interactions are common to other Dao&rsquo;s create a new Dao with <code>Common</code> as prefix for example<code>CommonHabitTrackerDao</code>.</p>
<p>We’ll cover complex cases like @RawQuery, Junctions, and TableViews in later articles as to not over-complicate this! 🔮</p>
<h2 id="-database-the-central-hub">🏛️ Database: The Central Hub</h2>
<p>Now that we have defined the entities and DAOs, we need to create a database to integrate thee entity and Dao&rsquo;s into its domain. Now the room can effectively generate its contents when instantiated along with this the database also manages the integrity validation, migrations and works as a central hub of control for our interactions with this database and its entities.</p>
<p>Here is an <strong>example</strong> of a database class:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#a6e22e">@Database</span>(
</span></span><span style="display:flex;"><span>    entities = [LocalHabitTracker<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>], <span style="color:#75715e">// Add all your entities here
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    version = <span style="color:#ae81ff">1</span> <span style="color:#75715e">// Update this when modifying the schema
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">HabitTrackerDataBase</span> : RoomDatabase() {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Room will auto-implement this function and return the DAO implementation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">abstract</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">habitTrackerDao</span>(): HabitTrackerDao
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Important Notes:</strong></p>
<p>✅ Always increase the version when modifying entities, it enables room to validate the integrity of tables and run migrations effectively. <br>
✅ Room doesn’t know how to handle schema changes unless you define a migration strategy. <br>
✅ Multiple databases can exist in an app, so this class serves as an entry point of the particular database.</p>
<p>Migrations and schema updates are crucial for real-world apps to prevent data loss. We’ll cover on later articles. 🔄</p>
<h2 id="-manually-creating-and-using-the-database">🎛️ Manually Creating and Using the Database</h2>
<p>Now, let’s see how to manually create and interact with the database in our app.</p>
<p><strong>✍️ Example:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">MainActivity</span> : AppCompatActivity() {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">override</span> <span style="color:#66d9ef">fun</span> <span style="color:#a6e22e">onCreate</span>(savedInstanceState: Bundle?) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">super</span>.onCreate(savedInstanceState)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Creating the database manually
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> habitTrackerDB = <span style="color:#a6e22e">Room</span>.databaseBuilder(
</span></span><span style="display:flex;"><span>            context = <span style="color:#66d9ef">this</span>, <span style="color:#75715e">// ApplicationContext
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            klass = HabitTrackerDataBase<span style="color:#f92672">::</span><span style="color:#66d9ef">class</span>.java, <span style="color:#75715e">// Database abstract class
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            name = <span style="color:#e6db74">&#34;habbit_tracker&#34;</span> <span style="color:#75715e">// Custom database name
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        ).build() 
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Get the DAO implementation
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>        <span style="color:#66d9ef">val</span> habitTrackerDao = habitTrackerDB.habitTrackerDao()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        setContent {
</span></span><span style="display:flex;"><span>            AppTheme {
</span></span><span style="display:flex;"><span>                AppNav(activity = <span style="color:#66d9ef">this</span>, habitLocalService = habitTrackerDao)
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>⚡ Pro Tips:</strong></p>
<p>✅ Don’t use the UI thread for database operations; always use coroutines or background threads. <br>
✅ In real-world apps, use Dependency Injection (DI) for managing database instances efficiently.  <br>
✅ Avoid creating multiple database instances, as it can lead to memory leaks and performance issues.</p>
<p>Mostly this is enough for creating simple CRUD apps, though it looks simple the entities will be converted into a query to create tables, A concrete classes will be created for each Dao&rsquo;s and other essential tasks will be carried by room it self easing our development and debug process.</p>
<h2 id="-whats-next">🚀 What’s Next?</h2>
<p>This was a quick run through of some of the key components of room, on the next article we will dive a little deeper into <strong>@Entity</strong> its keys and customizations,as we have a larger purpose to explore them each in detail.</p>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">Previous Article in this Series</a></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-4/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Understanding Recurrent Neural Network</title><link>https://md.eknath.dev/posts/ai-ml/what-is-an-rnn/</link><pubDate>Wed, 19 Feb 2025 19:47:13 +0530</pubDate><guid>https://md.eknath.dev/posts/ai-ml/what-is-an-rnn/</guid><description>&lt;p>When diving into AI/ML, we constantly hear about &lt;strong>transformers&lt;/strong> and their revolutionary impact. But how did we get here? The journey started from &lt;code>Traditional Neural Networks&lt;/code> ➡ &lt;code>Recurrent Neural Networks (RNNs)&lt;/code> ➡ &lt;code>Attention Mechanisms&lt;/code> and finally to &lt;code>Transformers&lt;/code>.&lt;/p>
&lt;p>RNNs were a &lt;strong>significant milestone&lt;/strong> because they addressed sequential data processing but had major limitations. These limitations led to the birth of attention mechanisms and transformers. Interestingly, before &lt;strong>GPT&lt;/strong> became a household name, &lt;strong>Google extensively used RNNs&lt;/strong> in their products, including predictive text, speech recognition, and early recommendation systems.&lt;/p></description><content:encoded><![CDATA[<p>When diving into AI/ML, we constantly hear about <strong>transformers</strong> and their revolutionary impact. But how did we get here? The journey started from <code>Traditional Neural Networks</code> ➡ <code>Recurrent Neural Networks (RNNs)</code> ➡ <code>Attention Mechanisms</code> and finally to <code>Transformers</code>.</p>
<p>RNNs were a <strong>significant milestone</strong> because they addressed sequential data processing but had major limitations. These limitations led to the birth of attention mechanisms and transformers. Interestingly, before <strong>GPT</strong> became a household name, <strong>Google extensively used RNNs</strong> in their products, including predictive text, speech recognition, and early recommendation systems.</p>
<h2 id="what-is-a-recurrent-neural-network-rnn">What is a Recurrent Neural Network (RNN)?</h2>
<p>Have you ever wondered how your keyboard <strong>predicts the next word</strong> while typing? Before transformers took over, <strong>RNNs were the secret behind word suggestions</strong> and other sequential tasks.</p>
<h3 id="definition"><strong>Definition:</strong></h3>
<p>An <strong>RNN (Recurrent Neural Network)</strong> is a type of artificial neural network designed specifically for <strong>processing sequential data</strong>. Unlike traditional feed-forward networks, RNNs have <strong>loops that allow information to persist</strong>, making them well-suited for tasks where <strong>context and order matter</strong>.</p>
<h3 id="common-use-cases-of-rnns"><strong>Common Use Cases of RNNs:</strong></h3>
<p>👉 <strong>Natural Language Processing (NLP)</strong> (e.g., speech recognition, text generation, machine translation)<br>
👉 <strong>Time Series Prediction</strong> (e.g., stock price forecasting, weather prediction)<br>
👉 <strong>Sequence Modeling</strong> (e.g., handwriting recognition, music composition)</p>
<h2 id="how-does-an-rnn-work">How Does an RNN Work?</h2>
<p>Let’s use the <strong>word prediction</strong> example to understand the key stages of an RNN. When you type a sentence, the RNN processes it in three key stages:</p>
<h3 id="1-input-processing-stage"><strong>1️⃣ Input Processing Stage</strong></h3>
<p>This is the <strong>data collection stage</strong>, where the model gathers input to make predictions.</p>
<p>🔹 When you <strong>first start using a social media app</strong>, its recommendations may seem random or inaccurate. Over time, as you interact more, the app starts to understand your preferences. This happens because it continuously <strong>collects data from you</strong> to make better predictions.</p>
<p>🔹 In the <strong>keyboard analogy</strong>, as you type more, the model <strong>stores your words</strong> in memory, helping it predict what you might type next.</p>
<h3 id="2-hidden-state-update-memory-stage"><strong>2️⃣ Hidden State Update (Memory Stage)</strong></h3>
<p>At this stage, the RNN <strong>updates its hidden state</strong> based on the new input and previous memory.</p>
<p>🔹 In <strong>social media</strong>, the model remembers what you’ve previously engaged with and <strong>adjusts recommendations</strong> accordingly. If you keep watching <strong>travel vlogs</strong>, it will prioritize showing more similar content.</p>
<p>🔹 In <strong>keyboard predictions</strong>, the model updates its memory with each word you type, continuously refining its <strong>contextual understanding</strong>.</p>
<h3 id="3-output-generation-stage"><strong>3️⃣ Output Generation Stage</strong></h3>
<p>Now, the model <strong>makes a decision</strong> and produces an output based on its learning.</p>
<p>🔹 In <strong>social media</strong>, this means recommending the <strong>next video or post</strong> that best matches your interests.<br>
🔹 In <strong>typing</strong>, this means predicting and suggesting the <strong>next word</strong> in your sentence.</p>
<h3 id="4-backpropagation-through-time-bptt--training-stage"><strong>4️⃣ Backpropagation Through Time (BPTT) – Training Stage</strong></h3>
<p>This is where the model <strong>learns from past mistakes</strong> and improves over time. Think of it like a <strong>student reviewing mistakes from past exams</strong> to perform better in the next test.</p>
<p>🔹 In <strong>social media</strong>, if you suddenly stop engaging with travel vlogs and switch to fitness videos, the model realizes its past predictions were wrong and <strong>adjusts itself</strong> to reflect your new interests.</p>
<p>🔹 In <strong>keyboard predictions</strong>, if you frequently <strong>delete a suggested word and type something else</strong>, the model <strong>adapts</strong> to improve its future suggestions.</p>
<h3 id="how-does-bptt-work"><strong>How Does BPTT Work?</strong></h3>
<p>1️⃣ The model <strong>compares its predictions</strong> to actual user behavior.<br>
2️⃣ If there’s an <strong>error</strong>, it <strong>adjusts its internal weights</strong> to improve accuracy.<br>
3️⃣ This process repeats across <strong>multiple iterations</strong>, constantly fine-tuning the model.</p>
<p>This stage is <strong>crucial</strong> because it ensures that the model <strong>evolves and adapts</strong> based on real-world interactions.</p>
<hr>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>RNNs played a critical role in AI’s evolution, <strong>powering early NLP applications, recommendations, and predictive text systems</strong>. However, due to <strong>limitations like vanishing gradients and slow processing</strong>, they were eventually replaced by <strong>transformers</strong> and <strong>attention-based models</strong>.</p>
<p>Yet, understanding RNNs is essential because they paved the way for <strong>modern AI breakthroughs</strong>. Without RNNs, we wouldn’t have reached <strong>the transformer era of GPT</strong>! 🚀</p>
<hr>
<p>Your feedbacks are welcome, Thanks for Reading.</p>
]]></content:encoded></item><item><title>Static App Shortcuts in Android: A Simple Implementation Guide</title><link>https://md.eknath.dev/posts/implement-static-app-shortcuts-on-android/</link><pubDate>Sat, 15 Feb 2025 10:25:15 +0530</pubDate><guid>https://md.eknath.dev/posts/implement-static-app-shortcuts-on-android/</guid><description>&lt;p>Have you ever long-pressed an app icon and seen quick actions like &lt;strong>&amp;ldquo;Search&amp;rdquo;&lt;/strong> or &lt;strong>&amp;ldquo;New Message&amp;rdquo;&lt;/strong>?&lt;br>
These are &lt;strong>App Shortcuts&lt;/strong>, a powerful feature that allows users to interact with your app &lt;strong>faster&lt;/strong> and &lt;strong>more efficiently&lt;/strong>.&lt;/p>
&lt;p>In this guide, we&amp;rsquo;ll explore how to implement &lt;strong>static app shortcuts&lt;/strong> in Android to enhance user experience.&lt;/p>
&lt;h2 id="what-are-app-shortcuts">What Are App Shortcuts?&lt;/h2>
&lt;p>&lt;strong>App shortcuts provide quick access to common app features when the user long-presses your app icon.&lt;/strong>&lt;br>
Along with system options like &lt;strong>App Info&lt;/strong> and &lt;strong>Pause App&lt;/strong>, you can define your own &lt;strong>custom shortcuts&lt;/strong> for essential actions.&lt;/p></description><content:encoded><![CDATA[<p>Have you ever long-pressed an app icon and seen quick actions like <strong>&ldquo;Search&rdquo;</strong> or <strong>&ldquo;New Message&rdquo;</strong>?<br>
These are <strong>App Shortcuts</strong>, a powerful feature that allows users to interact with your app <strong>faster</strong> and <strong>more efficiently</strong>.</p>
<p>In this guide, we&rsquo;ll explore how to implement <strong>static app shortcuts</strong> in Android to enhance user experience.</p>
<h2 id="what-are-app-shortcuts">What Are App Shortcuts?</h2>
<p><strong>App shortcuts provide quick access to common app features when the user long-presses your app icon.</strong><br>
Along with system options like <strong>App Info</strong> and <strong>Pause App</strong>, you can define your own <strong>custom shortcuts</strong> for essential actions.</p>
<p>📌 <strong>Example use cases:</strong><br>
✔ <strong>&ldquo;Create Note&rdquo;</strong> in a notes app (bypasses unnecessary navigation)<br>
✔ <strong>&ldquo;Search&rdquo;</strong> shortcut (instantly opens search with keyboard focused)</p>
<p>Users can also <strong>pin shortcuts to their home screen</strong> for even faster access!</p>
<h3 id="how-it-looks-on-a-device"><strong>How It Looks on a Device</strong></h3>
<p><img alt="Static App Shortcut example" loading="lazy" src="/img/static-app-short-cut.jpg#center"></p>
<hr>
<h2 id="-limitations-of-static-app-shortcuts">🚧 Limitations of Static App Shortcuts</h2>
<p>🔹 <strong>Requires API level 25+ (Android 7.1 and above)</strong><br>
🔹 <strong>Maximum of 4 static shortcuts per app</strong> (to prevent misuse)</p>
<p>Since shortcuts are limited, <strong>choose only essential ones that improve UX!</strong></p>
<hr>
<h2 id="-implementation-guide">🛠 Implementation Guide</h2>
<p>The best part? <strong>Static shortcuts don’t require any dependencies</strong>—we just define them in XML.</p>
<h3 id="1-creating-shortcut-values"><strong>1️⃣ Creating Shortcut Values</strong></h3>
<ol>
<li>Switch to <strong>Project View</strong> in Android Studio.</li>
<li>Create a new folder in <code>res/</code> named <strong><code>xml-v25</code></strong>.</li>
<li>Inside <code>res/xml-v25/</code>, create a new file named <strong><code>shortcuts.xml</code></strong>.</li>
<li>Add the following shortcut definition:</li>
</ol>
<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-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;shortcuts</span> <span style="color:#a6e22e">xmlns:android=</span><span style="color:#e6db74">&#34;http://schemas.android.com/apk/res/android&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;shortcut</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:shortcutId=</span><span style="color:#e6db74">&#34;create_note_shortcut&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:enabled=</span><span style="color:#e6db74">&#34;true&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:shortcutShortLabel=</span><span style="color:#e6db74">&#34;@string/create_note&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:shortcutLongLabel=</span><span style="color:#e6db74">&#34;@string/create_note_description_for_shortcut&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:icon=</span><span style="color:#e6db74">&#34;@drawable/ic_shortcut_create_note&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;intent</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">android:action=</span><span style="color:#e6db74">&#34;android.intent.action.VIEW&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">android:targetPackage=</span><span style="color:#e6db74">&#34;com.example.myapp&#34;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">android:targetClass=</span><span style="color:#e6db74">&#34;com.example.myapp.MainActivity&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">&lt;!-- Extra key to identify the shortcut action --&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;extra</span> <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;content_key&#34;</span> <span style="color:#a6e22e">android:value=</span><span style="color:#e6db74">&#34;_ssKey_create_note&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;/intent&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/shortcut&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/shortcuts&gt;</span>
</span></span></code></pre></div><p>🔹 The <code>content_key</code> extra helps identify <strong>which shortcut was used</strong>, so we can handle it later.</p>
<p>⚠️ Since my app&rsquo;s minimum supported version is API 24, i have to add it on the <strong><code>xml-v25</code></strong> if your app&rsquo;s minimum supported version is 25 or above you can add the <strong><code>shortcuts.xml</code></strong>  directly to <strong><code>xml</code></strong> folder.</p>
<hr>
<h3 id="2-adding-to-androidmanifestxml"><strong>2️⃣ Adding to AndroidManifest.xml</strong></h3>
<p>Add the following inside your <strong><code>AndroidManifest.xml</code></strong>, <strong>before</strong> the closing <code>&lt;/activity&gt;</code> tag:</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-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;meta-data</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;android.app.shortcuts&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">android:resource=</span><span style="color:#e6db74">&#34;@xml/shortcuts&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span></code></pre></div><p>After adding this, your manifest should look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;manifest</span> <span style="color:#a6e22e">xmlns:android=</span><span style="color:#e6db74">&#34;http://schemas.android.com/apk/res/android&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;application</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:allowBackup=</span><span style="color:#e6db74">&#34;false&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:icon=</span><span style="color:#e6db74">&#34;@drawable/ic_launcher_round&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">android:label=</span><span style="color:#e6db74">&#34;@string/app_name&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;activity</span> <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;.MainActivity&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;intent-filter&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;action</span> <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;android.intent.action.MAIN&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#f92672">&lt;category</span> <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;android.intent.category.LAUNCHER&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;/intent-filter&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">&lt;!-- Registering the shortcut --&gt;</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">&lt;meta-data</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">android:name=</span><span style="color:#e6db74">&#34;android.app.shortcuts&#34;</span>
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">android:resource=</span><span style="color:#e6db74">&#34;@xml/shortcuts&#34;</span><span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&lt;/activity&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/application&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/manifest&gt;</span>
</span></span></code></pre></div><hr>
<h3 id="3-handling-shortcut-logic-in-code"><strong>3️⃣ Handling Shortcut Logic in Code</strong></h3>
<p>When the user taps a shortcut, it launches the <strong>MainActivity</strong> with an extra <code>content_key</code>.<br>
We can handle this in Kotlin:</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> activity = <span style="color:#a6e22e">LocalContext</span>.current <span style="color:#66d9ef">as</span> MainActivity
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Check which shortcut was used
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">val</span> shortcutCode = <span style="color:#66d9ef">when</span> {
</span></span><span style="display:flex;"><span>    activity.intent.getStringExtra(<span style="color:#e6db74">&#34;content_key&#34;</span>)<span style="color:#f92672">?.</span>contains(<span style="color:#e6db74">&#34;create_note&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#66d9ef">true</span> <span style="color:#f92672">-&gt;</span> <span style="color:#ae81ff">1</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">else</span> <span style="color:#f92672">-&gt;</span> <span style="color:#ae81ff">0</span>  <span style="color:#75715e">// Default: No shortcut used
</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">// Handle shortcut action
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>LaunchedEffect(shortcutCode) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> (shortcutCode <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span>) {
</span></span><span style="display:flex;"><span>        navigateToNoteCreationScreen()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>📌 <strong>Shortcut Flow:</strong><br>
1️⃣ User <strong>long-presses</strong> the app icon and selects &ldquo;Create Note&rdquo;.<br>
2️⃣ App opens <strong>directly in note creation mode</strong> (keyboard preloaded).<br>
3️⃣ Saves <strong>3+ clicks</strong> compared to manual navigation! 🚀</p>
<hr>
<h2 id="-key-takeaways">✅ Key Takeaways</h2>
<p>✔ <strong>App Shortcuts improve UX</strong> by reducing navigation steps.<br>
✔ <strong>Maximum of 4 static shortcuts per app</strong> (API 25+ required).<br>
✔ <strong>Define shortcuts in <code>res/xml-v25/shortcuts.xml</code></strong>.<br>
✔ <strong>Use <code>content_key</code> in the intent</strong> to determine the shortcut action.</p>
<h3 id="-implementation-in-my-app"><strong>🔗 Implementation in My App</strong></h3>
<p><a href="https://github.com/Eganathan/jotters-space-android/commit/6f0070aef1c0cd53b0d72450121c77a2edf38482">📌 Commit with Static App Shortcuts on Jot Notes</a></p>
<p>Hope this guide helps! Feel free to share your thoughts via my social handles. 😊</p>
<hr>
<p><strong>Thank you for reading!</strong> 🎉</p>
]]></content:encoded></item><item><title>Setting-up Room Library and its dependencies(#OF02)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-2/</link><pubDate>Mon, 10 Feb 2025 10:02:29 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-2/</guid><description>&lt;p>Before we proceed with the setup, let’s quickly recap &lt;strong>why&lt;/strong> we chose Room:&lt;/p>
&lt;p>✅ &lt;strong>Compile-time SQL Validation&lt;/strong> – Catches errors early by verifying SQL queries at compile time.&lt;br>
✅ &lt;strong>Kotlin-first Approach&lt;/strong> – Supports coroutines, Flow, and LiveData natively.&lt;br>
✅ &lt;strong>Less Boilerplate Code&lt;/strong> – Simplifies database interactions while maintaining SQLite’s power.&lt;br>
✅ &lt;strong>Seamless Migration Handling&lt;/strong> – Built-in support for database migrations.&lt;/p>
&lt;p>For a more detailed explanation, refer to the &lt;a href="https://developer.android.com/training/data-storage/room">official Android documentation&lt;/a>.&lt;/p></description><content:encoded><![CDATA[<p>Before we proceed with the setup, let’s quickly recap <strong>why</strong> we chose Room:</p>
<p>✅ <strong>Compile-time SQL Validation</strong> – Catches errors early by verifying SQL queries at compile time.<br>
✅ <strong>Kotlin-first Approach</strong> – Supports coroutines, Flow, and LiveData natively.<br>
✅ <strong>Less Boilerplate Code</strong> – Simplifies database interactions while maintaining SQLite’s power.<br>
✅ <strong>Seamless Migration Handling</strong> – Built-in support for database migrations.</p>
<p>For a more detailed explanation, refer to the <a href="https://developer.android.com/training/data-storage/room">official Android documentation</a>.</p>
<hr>
<h2 id="official-documentation">Official Documentation</h2>
<p>Google keeps its documentation up to date with a simple setup guide. However, you might wonder why we need this article if the documentation is available.</p>
<p>Google’s documentation caters to <strong>both Java and Kotlin</strong> developers, but since most of us have migrated to <strong>Kotlin</strong>, we don’t need Java-specific instructions. This article focuses on a <strong>Kotlin-first approach</strong> to setting up Room.</p>
<p>📖 <a href="https://developer.android.com/jetpack/androidx/releases/room">Official Guide on Setting up Room Dependencies and Plugins</a>.</p>
<hr>
<h2 id="setting-up-dependencies">Setting up Dependencies</h2>
<h3 id="1-configure-project-level-buildgradlekts">1️⃣ Configure Project-Level <code>build.gradle.kts</code></h3>
<p>Ensure your project-level <code>build.gradle.kts</code> file includes <strong>Google’s Maven repository</strong> and <strong>Maven Central</strong>, as these are where Room dependencies are hosted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>buildscript {
</span></span><span style="display:flex;"><span>    repositories {
</span></span><span style="display:flex;"><span>        google()
</span></span><span style="display:flex;"><span>        mavenCentral()
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="2-add-room-dependencies">2️⃣ Add Room Dependencies</h3>
<p>Head to your module-level build.gradle.kts file and add only the required Room dependencies inside the dependencies block:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>dependencies {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Other dependencies 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Room Dependencies
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">val</span> room_version = <span style="color:#e6db74">&#34;2.6.1&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Room Database Core
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    implementation(<span style="color:#e6db74">&#34;androidx.room:room-runtime:</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Annotation Processor (KSP)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    ksp(<span style="color:#e6db74">&#34;androidx.room:room-compiler:</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Kotlin Extensions and Coroutines support for Room (Optional)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    implementation(<span style="color:#e6db74">&#34;androidx.room:room-ktx:</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>🔎 Check for Latest Versions
Always use the latest stable version of Room and KSP:</p>
<p>📌 <a href="https://mvnrepository.com/artifact/androidx.room/room-runtime">Maven Central - Room Runtime</a>
📌 <a href="https://github.com/google/ksp/releases">kotlin-ksp-releases</a></p>
<h3 id="3-add-the-room-plugin">3️⃣ Add the Room Plugin</h3>
<p><strong>Project-Level build.gradle.kts</strong>
Add the Room plugin reference to the project-level build.gradle.kts file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>plugins {
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;androidx.room&#34;</span>) version <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$room</span><span style="color:#e6db74">_version&#34;</span> apply <span style="color:#66d9ef">false</span> 
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;com.google.devtools.ksp&#34;</span>) version <span style="color:#e6db74">&#34;2.0.21-1.0.27&#34;</span> apply <span style="color:#66d9ef">false</span> 
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Other plugins...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>}
</span></span></code></pre></div><p><strong>Module-Level build.gradle.kts</strong>
Add this to the module-level <code>build.gradle</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>plugins {
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;androidx.room&#34;</span>)
</span></span><span style="display:flex;"><span>    id(<span style="color:#e6db74">&#34;com.google.devtools.ksp&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="4-configure-schema-directory">4️⃣ Configure Schema Directory</h3>
<p>Room allows schema export for better version control and database migrations. Let’s specify a schema directory inside the android {} block:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-kotlin" data-lang="kotlin"><span style="display:flex;"><span>android {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">..</span>.
</span></span><span style="display:flex;"><span>    room {
</span></span><span style="display:flex;"><span>        schemaDirectory(<span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$projectDir</span><span style="color:#e6db74">/schemas&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="sync--verify-installation">Sync &amp; Verify Installation</h3>
<p>Once you’ve added the dependencies and plugin, sync your Gradle project. If everything is set up correctly, you should have Room ready for use in our project! 🎉</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>./gradlew build 
</span></span></code></pre></div><hr>
<h3 id="sample-code-for-a-quick-reference">Sample Code for a quick reference</h3>
<p>I hope you had no issues setting up the dependencies, if you need a sample app to compare the change-set feel free to checkout this <a href="https://github.com/Eganathan/jotters-space-android/commit/92613b0b3f1e08709b94752a5276d6031466e16a">commit</a>.</p>
<hr>
<h2 id="whats-next"><strong>What’s Next?</strong></h2>
<p>This short article we have learned how to set-up the room dependencies to our projects and hopefully this was helpful:
📌 <strong>In the next part of this series, we’ll dive into:</strong></p>
<p>1️⃣ <strong>Key Components of Room</strong></p>
<ul>
<li><strong>@Entity</strong>: Defines how <strong>data is stored</strong> in the table.</li>
<li><strong>@Dao</strong>: Specifies <strong>how to interact</strong> with the table (CRUD operations).</li>
<li><strong>@Database</strong>: Configures the database and <strong>associates Entities and DAOs</strong>.</li>
</ul>
<p>2️⃣ <strong>How to create, access and interact with a simple database</strong>.</p>
<hr>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong><br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong>    <br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-1/">Previous Article in this Series</a></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-3/">Next Article in this Series</a></p>
]]></content:encoded></item><item><title>Upgrading Your App to Offline First With Room (#OF01)</title><link>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-1/</link><pubDate>Sun, 09 Feb 2025 09:23:57 +0530</pubDate><guid>https://md.eknath.dev/posts/offline-first-series/upgrading-your-app-to-offline-first-with-room-part-1/</guid><description>&lt;h2 id="why-should-you-care">Why Should You Care?&lt;/h2>
&lt;p>Making your app &lt;strong>offline-first&lt;/strong> is essential if you want to provide the &lt;strong>best user experience&lt;/strong>. It makes your app significantly &lt;strong>faster&lt;/strong> by reducing the number of network calls, which in turn also &lt;strong>reduces server costs&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="what-does-offline-first-mean">What does Offline-First mean?&lt;/h2>
&lt;p>The core idea is to &lt;strong>persist/store remote-fetched data on the device&lt;/strong>. This allows users to access the data &lt;strong>instantly&lt;/strong>, skipping unnecessary network requests—until the data expires or is invalidated.&lt;/p></description><content:encoded><![CDATA[<h2 id="why-should-you-care">Why Should You Care?</h2>
<p>Making your app <strong>offline-first</strong> is essential if you want to provide the <strong>best user experience</strong>. It makes your app significantly <strong>faster</strong> by reducing the number of network calls, which in turn also <strong>reduces server costs</strong>.</p>
<hr>
<h2 id="what-does-offline-first-mean">What does Offline-First mean?</h2>
<p>The core idea is to <strong>persist/store remote-fetched data on the device</strong>. This allows users to access the data <strong>instantly</strong>, skipping unnecessary network requests—until the data expires or is invalidated.</p>
<p>If your app has this feature then your app is offline-first if it does not then lets make it one.</p>
<hr>
<p>While offline-first apps come with some complexities, the benefits far outweigh the challenges. but before we start a small disclaimer.</p>
<h2 id="a-quick-disclaimer">A Quick Disclaimer</h2>
<p>There are <strong>multiple ways</strong> to approach offline-first implementation. What I discuss here is <strong>my approach</strong>, tailored to my requirements. There might be <strong>better methods</strong> for different scenarios so take these as <strong>suggestions and not rules</strong>.</p>
<p>If you have suggestions, feedback, or alternative approaches, I’d love to discuss them! Let’s learn and refine this together. As I explore further, I will <strong>actively update</strong> these articles with new insights/approaches/strategies.</p>
<p>Now, let’s get started. 🚀</p>
<hr>
<h2 id="offline-strategies-partial-vs-full-offline-mode">Offline Strategies: Partial vs. Full Offline Mode</h2>
<p>There are <strong>two main strategies</strong> for implementing offline-first functionality:</p>
<ol>
<li><strong>Fully Offline Mode</strong></li>
<li><strong>Partial Offline Mode</strong></li>
</ol>
<p>The <strong>main difference</strong> between them is whether the user can modify their data offline.</p>
<h3 id="1-fully-offline-mode-crud-operations-offline">1️⃣ Fully Offline Mode (CRUD Operations Offline)</h3>
<p>In this approach, users can:<br>
✅ Create, Read, Update, and Delete (CRUD) <strong>without an internet connection</strong>.<br>
✅ Sync data to the cloud <strong>when online</strong>.</p>
<p>🔴 <strong>Major Challenge</strong>:</p>
<ul>
<li>If a user <strong>forgets to sync</strong> data from one device (e.g., a tablet) and later accesses their account from another device (e.g., a phone), they might think the data is lost.</li>
<li>This can <strong>frustrate users</strong>, leading to complaints or abandoning our app.</li>
</ul>
<p>🔵 <strong>Idea Use case</strong>:</p>
<p>Best option for apps that does not support multi user and has single device login, but ⚠️ The data loss % is still significantly high if user forgets to connect online after the initial login and loses his device etc, but thats a tradeoff you have to live with.</p>
<p>While this approach offers <strong>true offline functionality</strong>, it introduces <strong>complex synchronization issues</strong>, making it harder to maintain <strong>data consistency across devices</strong>.</p>
<h3 id="2-partial-offline-mode-read-only-when-offline">2️⃣ Partial Offline Mode (Read-Only When Offline)</h3>
<p>In this approach, users can:<br>
✅ <strong>View/Read data</strong> offline.<br>
❌ <strong>Modify data (Create, Update, Delete) only when online</strong>.</p>
<p>🔹 <strong>Why We Chose This Approach</strong>:</p>
<ul>
<li><strong>Multi-device users won’t face sync issues</strong> since data is always available in cloud.</li>
<li><strong>Less complexity &amp; better error handling &amp; almost no possibility of data loss</strong>.</li>
<li><strong>Data remains clean and consistent</strong> with the server.</li>
</ul>
<p>🔵 <strong>Idea Use case</strong>:</p>
<p>Best option for most use-cases simple or complex or extremely complex data set apps, with or without <strong>multi-user</strong> or <strong>multiple-device-logins</strong> and what not.</p>
<p>This is most preferred due to its <strong>data-consistency</strong> point and much simpler to implement, Now checkout the next one.</p>
<h3 id="3-hybrid-offline-mode-partially-restricted-write-operations">3️⃣ Hybrid Offline Mode (Partially-restricted Write operations)</h3>
<p>The Hybrid mode is using both discussed strategies depending on the use cases, for example you may allow the user to update their profile details or personal notes etc but not the public contents that can cause issue with other uses on the same org/team.</p>
<p>✅ <strong>View/Read data</strong> offline. <br>
✅ <strong>Modify some data even if offline</strong>.<br>
❌ <strong>Modify most data only if online</strong>.</p>
<p>🔵 <strong>Idea Use case:</strong></p>
<p>Best option for most simple or complex apps, with or without multi-user but <strong>strictly allow only single-device-login</strong>, this enables the user to do write-operations to user specific data while restricting the same for other parts of the app, there are tradeoffs here as well but only a particular user will be affected in the org/team.</p>
<p>For me the <strong>partial offline mode</strong> stands out as best option and would suggest the same for most use-cases, we will be sticking with this through out this journey.</p>
<p>Now, let’s dive into <strong>how we fetch and synchronize data</strong>.</p>
<hr>
<h2 id="data-synchronization-strategies"><strong>Data Synchronization Strategies</strong></h2>
<p>Once we are decided on <strong>partial offline mode</strong>, the next question is:<br>
👉 <strong>How do we fetch data from the server and keep it updated?</strong></p>
<p>There are two main synchronization approaches:</p>
<h3 id="1-on-demand-pull-based-synchronization">1️⃣ On-Demand (Pull-Based Synchronization)</h3>
<ul>
<li><strong>Data is fetched only when the user requests it.</strong></li>
<li>Data is <strong>invalidated</strong> when it <strong>expires</strong> or is <strong>manually deleted</strong>.</li>
<li><strong>Lightweight &amp; user-centric</strong>—fetches only relevant data.</li>
</ul>
<h3 id="2-clone-push-based-synchronization">2️⃣ Clone (Push-Based Synchronization)</h3>
<ul>
<li>The local database <strong>mirrors the entire remote database</strong>.</li>
<li><strong>Auto-updates</strong> when the server sends a flag indicating it was modified.</li>
<li><strong>Heavy &amp; resource-intensive</strong>, as it may download <strong>unnecessary</strong> data.</li>
</ul>
<p>🔹 <strong>Which One Did We Pick?</strong><br>
We chose <strong>On-Demand Synchronization</strong> since it’s:<br>
✅ <strong>Efficient</strong> (fetches only what’s needed).<br>
✅ <strong>Faster &amp; lightweight</strong> (minimizes unnecessary data transfers).</p>
<p>However, we use mox of both lets call it a <strong>hybrid approach</strong>, we loaded some data that user will need on the navigating screen to ensure a better user experience.  After that, everything is <strong>strictly on-demand</strong> the data is synced only based on user.</p>
<hr>
<h2 id="which-local-database-library-should-you-choose"><strong>Which Local Database Library Should You Choose?</strong></h2>
<p>There are several <strong>local database solutions</strong> available, but we chose <strong>Google’s Room Persistence Library</strong>. Here’s why:</p>
<p>✅ <strong>Built on SQLite</strong> – but with a modern, developer-friendly API.<br>
✅ <strong>Works seamlessly with Kotlin</strong> (supports data classes).<br>
✅ <strong>Compile-time SQL query verification</strong> (reduces errors).<br>
✅ <strong>Built-in support for LiveData, Flow, and Paging</strong>.<br>
✅ <strong>Faster development</strong> (less boilerplate, easy migrations).</p>
<p>Using <strong>Room</strong> significantly <strong>accelerates development</strong> while maintaining a structured and reliable database. It’s my <strong>go-to solution</strong>, and I highly recommend it for <strong>Android and Kotlin Multiplatform (KMP) projects</strong>.</p>
<hr>
<h2 id="whats-next"><strong>What’s Next?</strong></h2>
<p>This article laid the foundation for an <strong>offline-first architecture</strong> and explained <strong>why we chose partial offline mode &amp; on-demand synchronization</strong>.</p>
<p>📌 <strong>In the next part of this series, we’ll dive into:</strong></p>
<ul>
<li><strong>Setting up Room in an Android</strong>.</li>
<li><strong>Exploring Key Components of Room</strong>.</li>
</ul>
<hr>
<h2 id="final-thoughts"><strong>Final Thoughts</strong></h2>
<p>This is my journey in <strong>building an offline-first app</strong>. I’d love to hear your feedback, suggestions, or questions!</p>
<p>Feel free to connect with me on:<br>
📩 <strong><a href="mailto:mail@eknath.dev">Email</a></strong>   <br>
🌍 <strong><a href="https://eknath.dev">Website</a></strong>     <br>
💫 <strong><a href="https://www.linkedin.com/posts/eganathan_offlinefirstandroid-offlinefirst-android-activity-7294912159627546624-TG77?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABYcOpgBgvDfy-0uUjfX0HTNqzzLfKZQAQU">LinkedIn-Post for comments and feedbacks</a></strong></p>
<p>🔖 <a href="https://md.eknath.dev/posts/upgrading-your-app-to-offline-first-with-room-part-2/">Next Article in this Series</a></p>
]]></content:encoded></item></channel></rss>