I Shipped an Android App Using Claude Code. Here’s What Nobody Tells You.
The features were the easy part. Everything between the systems is where the real work lives.
There’s a particular flavor of optimism you feel when you decide to build a mobile app. “How hard can it be?” you think. You have an idea, you have a laptop, and now you have an AI coding assistant that writes code faster than you can read it.
A few weeks later, you find yourself at 11 PM parsing the binary headers of a shared library file to figure out why Google won’t let you publish.
This is the story of building QwikScanner — a privacy-first document scanner with on-device OCR and Google Drive sync — from prototype to a submitted Google Play release, while pair-programming the entire thing with Claude Code in my terminal. It’s not a “look how easy AI makes everything” puff piece. It’s the honest version: the parts that genuinely felt like magic, and the parts that reminded me that shipping software is still hard — just hard in different places now.
If you’re a developer curious about what AI-assisted development actually feels like in practice, or a builder wondering whether the hype is real, this is for you.
The App
QwikScanner does what it says: you point your phone at a document, it detects the edges, crops it, and saves a clean scan. From there:
- On-device OCR extracts the text — using Google’s ML Kit, running entirely on the phone. No internet, no API costs, no data leaving the device.
- Google Drive sync backs up scans to your Drive, in a folder the app creates. It uses the narrowest possible permission scope, so it can’t see anything else in your Drive.
- Full-text search lets you find a document by words inside the scan, not just the title.
Honestly there are many scanner apps in the store. I have used plenty myself. I wanted to create a cleaner and leaner app that doesn’t overload with features and charge monthly recurring subscription fee. Just Scan. Extract and Sync to Google Drive — Turn your Android phone into a pocket document scanner with real-time edge detection, on-device OCR, and automatic Google Drive backup — no subscription needed.
The stack: React Native + Expo, SQLite for local storage, Zustand for state management. Everything free, everything privacy-respecting.
That’s the product. Here’s the journey.
Act I: When It Feels Like Magic
The first phase was intoxicating.
I described features in plain English and they materialized as working code. “Add on-device OCR.” “Build a three-slide onboarding flow.” “Add sort and filter to the home screen, and make search look inside the OCR text.” Each request turned into real, functioning features — often in a single pass.
This is the part of AI-assisted development that lives up to the marketing. The traditional bottleneck — how do I actually implement this? — largely dissolved. I wasn’t reading documentation for a charting library or fighting with a state management boilerplate. I was thinking about the product.
But here’s the thing I learned, and it’s the thesis of this entire article:
Writing features is now the easy part. The hard part is the connective tissue — the integration points where your app meets other people’s systems.
And those systems do not care that you have an AI assistant.
Act II: OAuth, or How Google Tested My Resolve
Every developer who has integrated Google Sign-In is already smiling grimly.
QwikScanner needed users to sign in so they could sync to their own Drive. Simple in concept. In practice, I collected a small museum of authentication errors:
Error 400: redirect_uri_mismatchError 400: invalid_requestAccess blocked: Authorization ErrorDEVELOPER_ERROR
The root cause was architectural. We’d started with a browser-based OAuth flow (expo-auth-session), which sends users out to a web page to authenticate. The problem: that flow uses a custom URL scheme to come back to the app, and Google's Android OAuth client validation fundamentally clashes with that approach. No amount of tweaking redirect URIs fixed it, because the mismatch was conceptual, not configurational.
The fix was a strategic retreat: rip it out entirely and switch to native Google Sign-In (@react-native-google-signin/google-signin), which uses Android's system-level authentication — no browser, no redirect URIs, no mismatch.
That solved three of the four errors. The last one, DEVELOPER_ERROR, was its own little riddle: it turned out to be a SHA-1 fingerprint mismatch. Google ties your OAuth client to your app's signing certificate, and I had to extract the SHA-1 from my debug keystore and register it in Google Cloud Console before sign-in would work.
The takeaway: AI does not make platform bureaucracy disappear. Google’s OAuth setup is irreducibly fiddly, and it always will be. What changed was the diagnostic loop. Instead of me reading fifteen Stack Overflow threads of varying staleness, the error came in, a hypothesis came out, and we tested it. We still had to grind — but the grinding had direction.
Act III: The Call Was Coming From Inside the House
This one is for everyone who has ever tried to build software on a corporate or security-locked machine.
At a certain point, everything started failing with SSL certificate errors. Not in the app — in the tooling. npm install failed. The Expo CLI failed. Gradle failed. Each threw something cryptic:
unable to verify the first certificate
PKIX path building failed: unable to find valid
certification path to requested targetThe culprit, after some detective work, was Norton Antivirus. Its “Web Shield” feature was silently intercepting every HTTPS connection on my machine — a man-in-the-middle by design — and re-signing the traffic with its own root certificate. My tools didn’t trust that certificate, so every download over HTTPS blew up.
The fix had nothing to do with my app. We extracted Norton’s root certificate straight off the live connection, imported it into the Java truststore that Gradle’s JVM uses, and pointed Gradle at it permanently. For npm and Node, we disabled strict SSL for the session.
The takeaway: Some of the hardest problems in modern development aren’t in your code at all. They live in the gap between your machine and the internet — proxies, certificates, antivirus, corporate policies. These are invisible in every tutorial, and they’ll eat an afternoon if you don’t know to look for them.
Act IV: The Play Console Gauntlet
I built the release bundle. I uploaded it. I felt accomplished. Then Google Play Console showed me a wall of problems that had nothing to do with whether my app actually worked.
Problem 1: “Inactive” status. Uploading a bundle isn’t publishing. You have to attach it to a release. Fine — confusing, but fine.
Problem 2: Undeclared permissions. Play flagged that my app requested READ_MEDIA_VIDEO and RECORD_AUDIO. My app does neither — a dependency was auto-injecting them into the manifest. We stripped them out with manifest merge rules.
Get Srinivas Patibanda’s stories in your inbox
Join Medium for free to get updates from this writer.
Problem 3 & 4 — the big ones:
- “Your app must target at least API level 35.” Mine targeted 34.
- “Your app does not support 16 KB memory page sizes.” A brand-new Android requirement for newer hardware.
These two forced the scariest decision of the project: a full framework upgrade from Expo SDK 51 to SDK 53 — which meant jumping React Native from 0.74 to 0.79 and React from 18 to 19. If you’ve ever done a major version upgrade on a real project, you know the feeling. These break things.
It broke things.
- A peer-dependency conflict during install (resolved with legacy peer deps).
- Regenerating the native Android project wiped all my release signing configuration — thankfully we’d snapshotted it first, a save that turned a disaster into a five-minute restore.
- A native module (
expo-asset) was now required but missing, surfacing only at build time.
And then, the worst kind of bug — the one that builds fine and crashes at runtime.
Act V: The Race Condition
After the upgrade, the app installed cleanly. Then it crashed the instant I tried to scan a document:
Call to function 'NativeDatabase.prepareAsync' has been
rejected. → Caused by: java.lang.NullPointerExceptionThis was a beautiful bug, in the way that only bugs you’ve already fixed are beautiful.
The new version of the SQLite library was stricter, and it exposed a latent race condition in my database layer. The code that opened the database looked roughly like this:
let _db = null;export async function getDb() {
if (_db) return _db;
_db = await SQLite.openDatabaseAsync('qwikscanner.db');
return _db;
}Spot it? The if (_db) return _db check passes only after the await completes. If two parts of the app call getDb() at nearly the same moment — which happens constantly on a screen that loads data on mount — both see null, and both open the database. The second open corrupts the first's native handle, and you get a null pointer deep in the native layer.
The fix is to cache the promise, not the resolved value, so the second caller awaits the same in-flight operation:
let _dbPromise = null;export function getDb() {
if (!_dbPromise) {
_dbPromise = openDb().catch((err) => {
_dbPromise = null; // allow retry on failure
throw err;
});
}
return _dbPromise;
}A classic concurrency bug, the kind that’s invisible until a dependency’s timing changes underneath you.
Act VI: The 16 KB Detective Story (My Favorite Part)
Even after the entire SDK upgrade, Play Console still flagged the 16 KB error — though it now let me proceed under a temporary grace period.
I could have shipped. The grace period was right there. But a grace period is a debt that comes due, and it would eventually block every future update. So instead of taking the easy exit, we went hunting.
We extracted the app bundle (it’s just a ZIP), pulled out all twenty native .so libraries, and parsed the ELF binary headers of each one to read its memory alignment. Nineteen were correctly aligned to 16 KB. Exactly one was not:
libmlkit_google_ocr_pipeline.so align: 4096 ❌It came from an outdated version of Google’s ML Kit (16.0.0) that one of my dependencies had pinned. The fix was a single line — force the build to use ML Kit 16.0.1 — followed by a rebuild and a re-verification that confirmed all twenty libraries were now aligned.
The takeaway, and the reason this is my favorite part: Manually inspecting binary headers to locate one misaligned library inside an app bundle is expert-tier debugging. It’s the kind of thing most indie developers would never attempt — they’d take the grace period and pray. Being able to say “find out which library is the problem” and get a precise, verified answer is a genuine capability multiplier. It let me fix the root cause properly instead of kicking the can down the road.
What I Actually Learned
Five honest takeaways, for anyone considering this path:
1. Features are cheap now. Integration is still expensive. The code itself was rarely the obstacle. The obstacles were OAuth, certificates, store policies, and SDK migrations — the seams where my app met systems I didn’t control.
2. The bottleneck moved from writing to deciding. My real job became steering: choosing native sign-in over browser OAuth, deciding to risk the SDK upgrade, choosing to fix the 16 KB issue properly instead of taking the grace period. The judgment calls were mine. The typing was shared.
3. AI makes you braver. I attempted a major framework version upgrade and binary-level debugging — things I would previously have avoided, deferred, or paid someone to do — because the cost of trying dropped close to zero. That shift in courage might be the most underrated effect of these tools.
4. It does not remove the need to understand your stack. Every fix made sense in hindsight, but I had to follow the reasoning to make good decisions. AI didn’t let me stop learning. It rewarded me for learning faster.
5. Write down your keystore password. Some lessons are eternal. If you lose the key you signed your app with, you can never update it on the Play Store again.
By the Numbers
- 1 framework major-version upgrade (Expo 51 → 53, React Native 0.74 → 0.79)
- ~5 distinct OAuth errors conquered
- 3 separate SSL/certificate battles (npm, Expo, Gradle)
- 20 native libraries audited at the binary level to find 1 misalignment
- 3 app bundle versions before a clean upload
- 0 backend servers — privacy-first by design
The Real Conclusion
The promise of AI-assisted development isn’t that it makes building software trivial. It doesn’t. Google’s OAuth is still labyrinthine. Antivirus software still breaks your toolchain. The Play Store still enforces requirements that have nothing to do with whether your app is any good.
What it changes is where you spend your effort. Less time stuck on syntax and boilerplate. More time on architecture, judgment, and the genuinely hard integration problems — the ones that were always the real work, now finally given the spotlight.
I went into this expecting an AI to write my app. What I got was something more useful: a tireless collaborator that let me punch above my weight, attempt harder things, and ship something I’m proud of — while still having to understand every step to steer it well.
The app is submitted (I’m waiting for Google Play store to approve and make it available to the public). The next one will be faster. And that, more than any single feature, is the point.
Building something similar? I’d love to hear the gnarliest “it’s not the code, it’s the environment” bug you’ve run into. Drop it in the comments.






