<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: A Bit Above Bytes</title>
    <description>The latest articles on DEV Community by A Bit Above Bytes (@a_bitabovebytes_bd82186).</description>
    <link>https://dev.to/a_bitabovebytes_bd82186</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3835924%2F17de2fdb-510f-44ce-b5e8-711603666ecc.png</url>
      <title>DEV Community: A Bit Above Bytes</title>
      <link>https://dev.to/a_bitabovebytes_bd82186</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/a_bitabovebytes_bd82186"/>
    <language>en</language>
    <item>
      <title>5 Things I Wish I Knew Before Building a Document Scanner App for Android</title>
      <dc:creator>A Bit Above Bytes</dc:creator>
      <pubDate>Tue, 24 Mar 2026 09:11:24 +0000</pubDate>
      <link>https://dev.to/a_bitabovebytes_bd82186/5-things-i-wish-i-knew-before-building-a-document-scanner-app-for-android-361n</link>
      <guid>https://dev.to/a_bitabovebytes_bd82186/5-things-i-wish-i-knew-before-building-a-document-scanner-app-for-android-361n</guid>
      <description>&lt;p&gt;After 3 months and 21,000 lines of Kotlin, here are the lessons I learned building a production document scanner for Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. CameraX Will Fight You on Frame Stability
&lt;/h2&gt;

&lt;p&gt;Users tap the shutter while their hand is still moving. You need to build your own frame stability detection -- compare consecutive frames using RMS difference and only allow capture below a threshold. Without this, expect 30-40% blurry scans.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. OCR Text Positioning Matters More Than OCR Accuracy
&lt;/h2&gt;

&lt;p&gt;ML Kit's text recognition is good enough out of the box. What most devs get wrong is where they put the recognized text in the PDF. If you dump it at the bottom of the page, users can't select individual words. Position each text block at its exact bounding box coordinates for a truly searchable PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Image Enhancement is Not Optional
&lt;/h2&gt;

&lt;p&gt;Users scan documents under fluorescent lights, in dim rooms, at weird angles. You need at least: auto brightness/contrast, Otsu's thresholding for B&amp;amp;W conversion, and a sharpening filter. I ended up building 6 enhancement modes before users stopped complaining.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Monetization Architecture Needs to Be Built In From Day 1
&lt;/h2&gt;

&lt;p&gt;Retrofitting a paywall into an existing app is painful. I used RevenueCat with Firebase Remote Config for A/B testing 4 different paywall variants. The metered free tier (limited scans per month) converts better than a hard paywall -- users need to experience the quality first.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Distribution is Harder Than Development
&lt;/h2&gt;

&lt;p&gt;I can build a CameraX pipeline with frame stability detection in my sleep. But writing a product headline that converts? Running Twitter marketing? Getting my first sale? That's a completely different skill set, and I'm still figuring it out.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Shortcut
&lt;/h2&gt;

&lt;p&gt;I packaged everything I built into &lt;strong&gt;ScanVault Pro&lt;/strong&gt; -- a complete, production-ready Android document scanner template. 110 Kotlin files, clean architecture, every feature listed above already implemented.&lt;/p&gt;

&lt;p&gt;If you're thinking about building a scanner app, you can skip the 3 months I spent and start from a working codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early bird price: just $29 (normally $149). First 10 buyers only.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://kahlon71.gumroad.com/l/scanvault-pro" rel="noopener noreferrer"&gt;Get ScanVault Pro on Gumroad →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;$29. Less than a pizza dinner for 21K lines of production code. 30-day money-back guarantee.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>I Spent 3 Months Building an Android Scanner App So You Don't Have To</title>
      <dc:creator>A Bit Above Bytes</dc:creator>
      <pubDate>Mon, 23 Mar 2026 22:22:39 +0000</pubDate>
      <link>https://dev.to/a_bitabovebytes_bd82186/i-spent-3-months-building-an-android-scanner-app-so-you-dont-have-to-59kl</link>
      <guid>https://dev.to/a_bitabovebytes_bd82186/i-spent-3-months-building-an-android-scanner-app-so-you-dont-have-to-59kl</guid>
      <description>&lt;p&gt;Three months ago, I set out to build a document scanner for Android. Not a tutorial project -- a real, production-ready app that could compete on the Play Store.&lt;/p&gt;

&lt;p&gt;110 Kotlin files and 21,000 lines of code later, I had something I was proud of. But I also had a realization: &lt;strong&gt;most Android devs who need document scanning in their app don't want to build this from scratch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I'm selling the complete source code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Took 3 Months (And Why)
&lt;/h2&gt;

&lt;p&gt;Building a scanner sounds simple until you actually do it. Here's what ate most of the time:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. CameraX Frame Stability
&lt;/h3&gt;

&lt;p&gt;Users tap "capture" while their hand is moving. Every. Single. Time. I had to build an RMS-based frame stability analyzer that compares consecutive camera frames and only allows capture when motion drops below a threshold. Without this, 40% of scans were blurry.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. ML Kit OCR With Invisible Text Layers
&lt;/h3&gt;

&lt;p&gt;Getting OCR text from ML Kit is the easy part. The hard part? Positioning that text at the exact bounding box coordinates inside a PDF so users can select individual words in any PDF reader. Most scanner apps dump all OCR text at the bottom of the page. Mine places it precisely where it belongs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Image Enhancement Pipeline
&lt;/h3&gt;

&lt;p&gt;Six different enhancement modes including Otsu's thresholding for converting color scans to clean black-and-white documents. Plus a real-time ColorMatrix-based enhancement system that runs before the user even taps save.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Monetization That Actually Works
&lt;/h3&gt;

&lt;p&gt;RevenueCat integration with 4 paywall variants, A/B tested via Firebase Remote Config. Metered free tier. Weekly/Monthly/Annual plans. Product flavors for free and premium builds with different scan limits. This alone took 2 weeks to get right.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Everything Else
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AES-256-GCM document encryption&lt;/li&gt;
&lt;li&gt;eSign with draw, type, and image signatures&lt;/li&gt;
&lt;li&gt;Google Drive backup with OAuth&lt;/li&gt;
&lt;li&gt;QR/Barcode scanner&lt;/li&gt;
&lt;li&gt;DOCX export (no Adobe SDK needed)&lt;/li&gt;
&lt;li&gt;Biometric app lock&lt;/li&gt;
&lt;li&gt;ZIP batch export&lt;/li&gt;
&lt;li&gt;3-page animated onboarding&lt;/li&gt;
&lt;li&gt;GitHub Actions CI/CD pipeline&lt;/li&gt;
&lt;li&gt;Fastlane deployment config&lt;/li&gt;
&lt;li&gt;Privacy Policy + Terms of Service&lt;/li&gt;
&lt;li&gt;Play Store listing copy&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Tech Stack
&lt;/h2&gt;

&lt;p&gt;Kotlin 2.0, Compose BOM 2024, CameraX 1.4, ML Kit, Room 2.6, Hilt 2.51, RevenueCat 8.3, Firebase, Coil, Lottie, WorkManager. Clean Architecture with MVVM + StateFlow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;If you're an Android developer who wants to ship a scanner app without 3 months of boilerplate, this template saves you 400+ hours of work.&lt;/p&gt;

&lt;p&gt;A freelance Android dev costs $80-150/hr. This template is $149. Do the math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get It
&lt;/h2&gt;

&lt;p&gt;I'm selling ScanVault Pro on Gumroad for &lt;strong&gt;$149&lt;/strong&gt; (normally $249 -- launch pricing).&lt;/p&gt;

&lt;p&gt;Use code &lt;strong&gt;LAUNCH50&lt;/strong&gt; for 50% off -- only 5 codes available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://kahlon71.gumroad.com/l/scanvault-pro" rel="noopener noreferrer"&gt;Get ScanVault Pro →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One-time purchase. Unlimited projects. 30-day money-back guarantee.&lt;/p&gt;

&lt;p&gt;Questions? I built every line of this code and I'm happy to walk you through it.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>startup</category>
    </item>
    <item>
      <title>Stop Taking Blurry Scans: How I Built Frame Stability Detection for Android with CameraX</title>
      <dc:creator>A Bit Above Bytes</dc:creator>
      <pubDate>Sat, 21 Mar 2026 19:38:37 +0000</pubDate>
      <link>https://dev.to/a_bitabovebytes_bd82186/stop-taking-blurry-scans-how-i-built-frame-stability-detection-for-android-with-camerax-2jh5</link>
      <guid>https://dev.to/a_bitabovebytes_bd82186/stop-taking-blurry-scans-how-i-built-frame-stability-detection-for-android-with-camerax-2jh5</guid>
      <description>&lt;p&gt;Every document scanner app has the same problem: users tap the capture button while their hand is still moving, and the scan comes out blurry.&lt;/p&gt;

&lt;p&gt;I spent two weeks solving this while building a production Android scanner app (21K lines of Kotlin). Here's the approach that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;CameraX gives you a camera preview, but it has no concept of "is the frame stable enough to capture?" You need to build that yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: RMS-Based Frame Stability
&lt;/h2&gt;

&lt;p&gt;Instead of complex optical flow or gyroscope fusion, I used a simple but effective approach: compare consecutive frames using Root Mean Square (RMS) difference.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FrameStabilityAnalyzer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ImageAnalysis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Analyzer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;lastFrameBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;isStable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;STABILITY_THRESHOLD&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt; &lt;span class="c1"&gt;// Tune this value&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageProxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ImageProxy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;currentBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imageProxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;planes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toByteArray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;lastFrameBytes&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;rms&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateRMS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currentBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;isStable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rms&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nc"&gt;STABILITY_THRESHOLD&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;lastFrameBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentBytes&lt;/span&gt;
        &lt;span class="n"&gt;imageProxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;calculateRMS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ByteArray&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;minOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;sumSquares&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;until&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;diff&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toInt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toInt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;sumSquares&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sumSquares&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grab the luminance plane&lt;/strong&gt; from each camera frame (Y plane in YUV)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare it to the previous frame&lt;/strong&gt; pixel by pixel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calculate the RMS difference&lt;/strong&gt; -- low values mean the camera isn't moving&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate the capture button&lt;/strong&gt; -- only allow capture when &lt;code&gt;isStable == true&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Key Insight
&lt;/h2&gt;

&lt;p&gt;The threshold value (&lt;code&gt;STABILITY_THRESHOLD = 5.0&lt;/code&gt;) is critical. Too low and the user can never capture. Too high and you still get blurry scans.&lt;/p&gt;

&lt;p&gt;I found &lt;strong&gt;5.0 works well for document scanning&lt;/strong&gt; where you want sharp text. For general photos, you might go up to 8-10.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring It Up with CameraX
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;stabilityAnalyzer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FrameStabilityAnalyzer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;imageAnalysis&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ImageAnalysis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setBackpressureStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ImageAnalysis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;STRATEGY_KEEP_ONLY_LATEST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;also&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAnalyzer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stabilityAnalyzer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// In your UI, observe stabilityAnalyzer.isStable&lt;/span&gt;
&lt;span class="c1"&gt;// Show a green border when stable, red when shaking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Pro Tips
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only analyze a downsampled version&lt;/strong&gt; of the frame for performance. You don't need full resolution to detect motion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a debounce&lt;/strong&gt; -- require 3 consecutive stable frames before allowing capture. This prevents false positives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show visual feedback&lt;/strong&gt; -- users need to know WHY the button is grayed out. A subtle "hold steady" indicator works better than nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;After implementing this, the percentage of blurry scans in my app dropped by roughly 80%. Users don't even notice the gating -- they naturally hold still for a fraction of a second, and the capture fires automatically.&lt;/p&gt;




&lt;p&gt;This is one of those details that separates a good scanner from a great one. If you're building anything with CameraX that involves document capture, frame stability detection is worth the effort.&lt;/p&gt;

&lt;p&gt;I'm currently building this as part of a full production scanner app template. If you're interested in skipping the months of work, check out my profile for more details.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Production Android Document Scanner in Kotlin — The Hard Parts Nobody Talks About</title>
      <dc:creator>A Bit Above Bytes</dc:creator>
      <pubDate>Sat, 21 Mar 2026 16:48:30 +0000</pubDate>
      <link>https://dev.to/a_bitabovebytes_bd82186/how-i-built-a-production-android-document-scanner-in-kotlin-the-hard-parts-nobody-talks-about-4dj3</link>
      <guid>https://dev.to/a_bitabovebytes_bd82186/how-i-built-a-production-android-document-scanner-in-kotlin-the-hard-parts-nobody-talks-about-4dj3</guid>
      <description>&lt;p&gt;I spent months building a complete document scanner app in Kotlin with Jetpack Compose. 110 files. 21,000+ lines of code. Along the way I hit problems that no tutorial prepared me for. Here are the hard parts and how I solved them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. CameraX Frame Stability Detection
&lt;/h2&gt;

&lt;p&gt;The "auto-capture" feature sounds simple: detect when the document is steady and snap. In reality, you need frame-to-frame stability analysis. My approach: calculate an RMS difference between consecutive preview frames. If the RMS stays below a threshold for N consecutive frames, the document is stable. The key insight: sample every 10th pixel. Processing every pixel kills frame rate. Sampling gives you 95% accuracy at 10% of the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Invisible OCR Text Layer in PDFs
&lt;/h2&gt;

&lt;p&gt;ML Kit gives you OCR text, but positioning it correctly inside a PDF so it is selectable but invisible? That is where tutorials stop and real engineering begins. ML Kit returns bounding boxes for each text block. You map those coordinates from image space to PDF page space, then draw the text with zero opacity at the exact positions. Result: a PDF that looks like a scanned image but has fully searchable, selectable text underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Image Enhancement Without OpenCV
&lt;/h2&gt;

&lt;p&gt;Most tutorials say "just use OpenCV." But adding OpenCV means 30MB+ of native libraries. For a scanner app, you only need a few operations. I implemented Otsu's thresholding for B&amp;amp;W conversion and convolution kernel sharpening in pure Kotlin. No native libraries. Works on every device.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. RevenueCat Paywall with A/B Testing
&lt;/h2&gt;

&lt;p&gt;I implemented 4 paywall variants controlled by Firebase Remote Config: comparison table, demo video, single price (no choice paralysis), and urgency countdown. The A/B test selection happens at app launch, and RevenueCat handles the actual subscription logic. This separation means you can test pricing psychology without touching payment code.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. PDF Encryption Done Right
&lt;/h2&gt;

&lt;p&gt;AES-256-GCM encryption with PBKDF2 key derivation at 120K iterations. Not 1000. Not 10000. 120K, because that is what OWASP recommends.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Kotlin 2.0, Jetpack Compose, Material 3, CameraX 1.4, ML Kit (Document Scanner + Text Recognition + Barcode), Room 2.6, Hilt DI, RevenueCat 8.3, Firebase (Analytics + Crashlytics + Remote Config), WorkManager, Coil, Lottie.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Building the app was the fun part. The real challenge? Getting anyone to look at it. I packaged the entire codebase as a template on &lt;a href="https://kahlon71.gumroad.com/l/scanvault-pro" rel="noopener noreferrer"&gt;Gumroad&lt;/a&gt; so other Android devs can skip the 3-month build and ship in days.&lt;/p&gt;

&lt;p&gt;If you are building a scanner app, a document management tool, or anything with CameraX + ML Kit, this might save you real time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What is the hardest "simple-sounding" feature you have had to build? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>I Built a 21K-Line Android Scanner App in One Day — Here's the Full Template</title>
      <dc:creator>A Bit Above Bytes</dc:creator>
      <pubDate>Fri, 20 Mar 2026 18:22:07 +0000</pubDate>
      <link>https://dev.to/a_bitabovebytes_bd82186/i-built-a-21k-line-android-scanner-app-in-one-day-heres-the-full-template-2675</link>
      <guid>https://dev.to/a_bitabovebytes_bd82186/i-built-a-21k-line-android-scanner-app-in-one-day-heres-the-full-template-2675</guid>
      <description>&lt;p&gt;Every document scanner app on the Play Store looks the same. They all use CameraX, ML Kit, and Room — yet developers keep rebuilding the same architecture from scratch.&lt;/p&gt;

&lt;p&gt;So I built the template once, properly, and I'm sharing the full source.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;92 Kotlin files. 21,000+ lines. Production-ready architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scanner Engine&lt;/strong&gt; — CameraX + ML Kit Document Scanner with auto-capture, frame stability detection, 6 scan modes (Document, ID Card, Passport, Receipt, Whiteboard, Book)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF Engine&lt;/strong&gt; — Generate, merge, split PDFs with invisible OCR text layer at 300 DPI. No iText dependency (avoids AGPL licensing headaches)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR&lt;/strong&gt; — ML Kit Text Recognition v2 with bounding box positioning, background processing via WorkManager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eSign&lt;/strong&gt; — Draw, type, or upload signatures. Composite onto document pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monetization&lt;/strong&gt; — RevenueCat integration with metered paywall (soft prompt at scan 2, hard gate at limit), 4 A/B test paywall variants, weekly + annual plans&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full UI&lt;/strong&gt; — Material 3 + Jetpack Compose. Dark onboarding flow, animated paywall, bottom navigation, document grid/list views&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/          → Room DB (5 entities, 5 DAOs), Repository pattern, DataStore prefs
domain/        → Use cases, engine layer (Scan, PDF, OCR, eSign, Barcode)
ui/            → 15+ screens, Compose navigation, Hilt DI
service/       → WorkManager workers for background OCR + PDF export
util/          → Analytics, RevenueCat config, A/B testing via Firebase Remote Config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean Architecture with proper separation. Not a tutorial project — this is what you'd actually ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Kotlin 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Jetpack Compose + Material 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Camera&lt;/td&gt;
&lt;td&gt;CameraX 1.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML&lt;/td&gt;
&lt;td&gt;ML Kit (Document Scanner, Text Recognition, Barcode)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Room 2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DI&lt;/td&gt;
&lt;td&gt;Hilt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;RevenueCat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analytics&lt;/td&gt;
&lt;td&gt;Firebase Analytics + Crashlytics + Remote Config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background&lt;/td&gt;
&lt;td&gt;WorkManager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;AES-256-GCM encryption, Biometric lock&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I kept seeing the same pattern: developers spend 2-3 months building scanner app infrastructure before they can even start on their unique features. Edge detection, perspective correction, OCR positioning, PDF generation with text layers, paywall flows — it's all solved problems, but the implementation details eat your time alive.&lt;/p&gt;

&lt;p&gt;This template handles the 80% that's identical across every scanner app, so you can focus on what makes yours different.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Can Build With It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Receipt scanner for expense tracking&lt;/li&gt;
&lt;li&gt;Business card scanner with CRM integration
&lt;/li&gt;
&lt;li&gt;Medical document organizer&lt;/li&gt;
&lt;li&gt;Construction site documentation tool&lt;/li&gt;
&lt;li&gt;Legal document management app&lt;/li&gt;
&lt;li&gt;Student note digitizer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rebrand it, customize the UI, add your domain-specific features, and ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get It
&lt;/h2&gt;

&lt;p&gt;The full template is available here: &lt;a href="https://kahlon71.gumroad.com/l/scanvault-pro" rel="noopener noreferrer"&gt;ScanVault Pro Template on Gumroad&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Includes complete source code, architecture docs, customization guide, and a quick-start walkthrough.&lt;/p&gt;

&lt;p&gt;Happy to answer any questions about the architecture decisions or implementation details in the comments.&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>mobile</category>
      <category>jetpackcompose</category>
    </item>
  </channel>
</rss>
