●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
An Implementation Notebook for Shipping Android/Kotlin Apps to Google Play with Claude Code
A production notebook from years of running an indie Android app business. Compose × Hilt × Room design calls, ProGuard crash triage, and a 14-item pre-release checklist that goes in front of every Google Play AAB upload.
The "Too Much to Keep in Your Head" Problem in Android
Kotlin, Jetpack Compose, Hilt, Room, Retrofit, Coroutines. The "current best practice" toolset for Android shifts substantially every two or three years. The recommended stack when I started shipping Android apps and the current one are essentially different worlds, and re-checking docs has eaten a lot of my time over the years. Rapid evolution of the Kotlin language, the ever-growing Jetpack library suite, multi-device compatibility requirements, and frequent Google Play policy updates — keeping up with all of this as a solo developer or small team is genuinely challenging.
Claude Code addresses this complexity by letting you "code through conversation." It's not just a completion tool — it's an architecture consultant, a debugging partner, and a release automation orchestrator.
Who This Guide Is For
Kotlin developers aiming to build production-quality apps
Developers looking to integrate Claude Code into their Android workflow
Flutter or React Native developers evaluating native Kotlin
Anyone wanting to shorten their release cycle through automation
Step 1: Project Setup and CLAUDE.md Configuration
Giving Claude Code Your Project's DNA
The most impactful first step is adding a CLAUDE.md file to your project root. This lets Claude Code understand your architecture and constraints — without you having to repeat them in every session.
# MyApp — CLAUDE.md## Architecture- Pattern: MVVM + Clean Architecture- DI: Hilt- Database: Room (SQLite)- Networking: Retrofit + OkHttp- Async: Kotlin Coroutines + Flow- UI: Jetpack Compose (Material 3)## Package Structure- app/ — Application class, DI modules- feature/{name}/ — Feature UI and ViewModel- domain/ — UseCases, Repository interfaces- data/ — Repository implementations, Room DAOs, API clients- core/ — Shared utilities, extension functions## Coding Rules- Use sealed classes for error handling: Result<T, AppError>- ViewModels expose UI State as StateFlow- Testing: JUnit5 + MockK, 80%+ coverage target- Compose previews: use @PreviewParameter with realistic sample data## Restrictions- No business logic in Activity/Fragment- GlobalScope is banned- Hardcoded strings must be moved to strings.xml
With this file in place, Claude Code knows the tech stack and rules for every session. No more repeating "this project uses Hilt" at the start of every prompt.
Migrating to Version Catalogs
Managing dependencies across multiple modules can get messy quickly. Ask Claude Code to handle the migration:
Review the build.gradle.kts files across the project and migrate
them to a version catalog (libs.versions.toml) format.
Base everything on Kotlin 2.0 / AGP 8.9 with up-to-date dependencies.
Claude Code reads the files, extracts the current version list, and produces both the libs.versions.toml template and the diff for each module's build.gradle.kts.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦What years of indie Android work taught me about where to delegate to Claude Code and where humans must stay accountable.
✦A 14-item Google Play pre-release checklist that I run before every AAB upload (ANR thresholds, Data Safety form, AD_ID permission, edge-to-edge on Android 15+, and more).
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Well-structured UI State is the foundation of a reliable Compose app. Establish this pattern early:
// domain/state/ProductListState.ktsealed class ProductListState { data object Loading : ProductListState() data class Success( val products: List<Product>, val isRefreshing: Boolean = false ) : ProductListState() data class Error(val message: String, val retryable: Boolean) : ProductListState()}
Once you show Claude Code this pattern, asking "build CartViewModel using the same approach" reliably produces cart-specific logic — quantity adjustments, totals, validation — without you having to spell out the architecture.
A useful prompt tip: add "use modern APIs like animateItem()" and "include a @PreviewParameter sample" to get production-ready code that's also easy to iterate on.
When you need to add a Room migration, just describe it:
Write MIGRATION_2_3 for Room. It adds a `category_id` column
(INTEGER NOT NULL DEFAULT 0) to the `products` table.
Include the migration test as well.
Claude Code produces both the migration object and a MigrationTest class.
Offline-First Architecture
// data/repository/ProductRepositoryImpl.ktclass ProductRepositoryImpl @Inject constructor( private val productApi: ProductApi, private val productDao: ProductDao, private val networkMonitor: NetworkMonitor) : ProductRepository { override fun getProducts(): Flow<Result<List<Product>>> = flow { // Emit cache first for instant UI response val cachedProducts = productDao.getAllProducts().first() if (cachedProducts.isNotEmpty()) { emit(Result.success(cachedProducts.map { it.toDomain() })) } // Only hit the network when connected if (networkMonitor.isConnected) { runCatching { productApi.getProducts() } .onSuccess { response -> productDao.upsertProducts(response.map { it.toEntity() }) emit(Result.success(response.map { it.toDomain() })) } .onFailure { error -> // If cache exists, swallow the error silently if (cachedProducts.isEmpty()) { emit(Result.failure(error)) } } } }.catch { emit(Result.failure(it)) }}
Step 4: Test Automation and Quality Assurance
ViewModel Unit Tests
// feature/products/ProductListViewModelTest.kt@ExtendWith(CoroutinesTestExtension::class)class ProductListViewModelTest { @MockK private lateinit var getProductsUseCase: GetProductsUseCase private lateinit var viewModel: ProductListViewModel @BeforeEach fun setUp() { MockKAnnotations.init(this) viewModel = ProductListViewModel(getProductsUseCase) } @Test fun `initial state is loading`() = runTest { coEvery { getProductsUseCase() } coAnswers { delay(1000) Result.success(emptyList()) } val states = mutableListOf<ProductListState>() val job = launch { viewModel.uiState.collect { states.add(it) } } assertTrue(states.first() is ProductListState.Loading) job.cancel() } @Test fun `success state shows product list`() = runTest { val products = listOf( Product(id = "1", name = "Product A", price = 1000), Product(id = "2", name = "Product B", price = 2000) ) coEvery { getProductsUseCase() } returns Result.success(products) val finalState = viewModel.uiState .filter { it is ProductListState.Success } .first() assertThat(finalState).isInstanceOf(ProductListState.Success::class.java) assertThat((finalState as ProductListState.Success).products).isEqualTo(products) }}
Prompt tip: "Write tests for ProductListViewModel using MockK. Cover the success case, network error, and empty list scenarios." This produces a comprehensive test suite in a single request.
When a test fails, Claude Code receives the output and incorporates the fix into its next suggestion — a tight feedback loop that keeps quality high without manual context-switching.
Claude Code handles store listing copy as well as code. Here's a prompt that works well:
Generate optimized Google Play store metadata for this app.
App name: TaskFlow
Category: Task management / Productivity
Key features:
- Kanban board
- Natural language task input (AI-powered)
- Team sharing and collaboration
- Push notification reminders
Competitors: Todoist, TickTick, Notion
Target audience: Business professionals, 25–45
Provide:
- Title (under 30 characters)
- Short description (under 80 characters)
- Full description (under 4,000 characters, with SEO keywords)
- Release notes (under 500 characters)
Output both English and Japanese versions.
The generated copy is often usable as-is, but A/B testing through the Play Console is the path to real optimization.
Pre-Release Checklist
Create a production release checklist for Google Play.
Separate technical and business/compliance items.
Include ProGuard / R8 verification, debug log removal, and analytics setup.
Key items from a typical generated checklist:
ProGuard / R8 enabled with critical classes protected
Debug logs (Log.d, Timber.d) suppressed in release builds
API keys stored in BuildConfig or Secrets Manager — never hardcoded
All network calls use HTTPS (Network Security Config enforced)
Target API level meets current requirement (API 35+)
64-bit architecture (arm64-v8a) supported
App size minimized with AAB format and Dynamic Delivery
Step 7: Performance Optimization and Production Monitoring
Diagnosing Unnecessary Recomposition
The most common Compose performance issue is excessive recomposition. This debug helper makes it visible:
@Composablefun DebugRecompositionHighlight( enabled: Boolean = BuildConfig.DEBUG, content: @Composable () -> Unit) { if (enabled) { val recompositionCount = remember { mutableIntStateOf(0) } val color by animateColorAsState( targetValue = if (recompositionCount.intValue % 2 == 0) { Color.Transparent } else { Color.Red.copy(alpha = 0.1f) }, label = "recompose_highlight" ) recompositionCount.intValue++ Box(modifier = Modifier.background(color)) { content() } } else { content() }}
Claude Code prompt: "This screen is recomposing excessively. Review how StateFlow is being collected and suggest optimizations using remember, derivedStateOf, and proper key() usage."
Step 8: Claude Code Prompt Patterns That Actually Work
After using Claude Code across multiple Android projects, these five patterns consistently produce high-quality results:
Pattern 1: Lead with context
/read app/src/main/kotlin/com/example/myapp/feature/products/
Review this directory structure, then implement CartFeature using the same pattern.
Pattern 2: Paste errors verbatim
Fix this build error:
[paste error exactly as it appears]
Pattern 3: Ask for a review, then implement
/read app/.../ProductRepositoryImpl.kt
Review this file for Kotlin best practices, focusing on Coroutines usage
and exception handling. Then show me the improved version.
Pattern 4: Test-first development
Write unit tests for ProductRepositoryImpl.getProducts() using MockK.
Cover: success, network error, and cache-only scenarios.
Pattern 5: Structured refactoring
This class is over 500 lines. Propose a separation of responsibilities
following SRP, show me the planned class structure, then implement it.
Two Pitfalls Worth Knowing in Advance
expect / actual implementations in Kotlin Multiplatform projects
KMP's expect / actual pattern gets complex fast. Claude Code can scaffold both Android and iOS actual implementations in one go, but KMP-specific Gradle configuration and native crash debugging still benefit from human review. A detailed CLAUDE.md describing your KMP structure (the responsibilities of the common module versus platform modules) makes a substantial difference to output quality.
ProGuard / R8 crashes after minification
A prompt that works well: "I'm using [library name] and it crashes after minification. Read this crash log and suggest the right keep rules for proguard-rules.pro." Claude Code reads the log, identifies what needs protection, and generates the rules. The minified release build test is non-negotiable as a final verification step.
Here's what's only visible after running an indie Android app business for years with Claude Code wired into the daily loop. None of the items below are explicitly written in Android Developers docs, Compose samples, or the Kotlin reference — which is precisely why they bite hardest in production.
1. Compose recomposition runaway and where @Stable / @Immutable actually matters
The biggest Compose trap is "recomposition firing 200 times per second on a screen that isn't even transitioning." Profiling my wallpaper app revealed that each item in a LazyVerticalGrid containing Image was recomposing on every 1px of scroll, dropping average frame rate from 60fps to 38fps on mid-range Android devices.
The cause was that ProductUiState was being passed as a plain data class. The Compose compiler couldn't infer it as Stable, so it skipped equality checks every frame.
// ❌ Bad: too many parameters; Compose compiler gives up on Stable inferencedata class ProductUiState( val id: String, val title: String, val tags: List<String>, val downloads: Int, val previewUrls: List<String>, val isFavorite: Boolean,)// ✅ Good: explicit @Immutable, with List replaced by ImmutableList@Immutabledata class ProductUiState( val id: String, val title: String, val tags: ImmutableList<String>, // kotlinx.collections.immutable val downloads: Int, val previewUrls: ImmutableList<String>, val isFavorite: Boolean,)
A prompt like "List every place a @Immutable annotation should be added and every List that should be swapped to ImmutableList, starting from the composable function signatures" gets Claude Code to sweep the whole module. Frame rates stabilized once each screen had 4–7 @Immutable annotations applied.
2. Coroutine exceptions that bring the whole app down — where CoroutineExceptionHandler actually fires
Throwing inside viewModelScope.launch { ... } without explicit handling can crash the whole process, even though SupervisorJob is supposed to isolate failures. The usual culprits are missing await() calls on child async coroutines and missing runCatching after switching to Dispatchers.IO.
A class of UndeliverableException crashes that was hitting 1,200/month in Crashlytics dropped to 40/month (a 97% reduction) after layering three lines of defense:
// 1. Global handler — last line of defenseclass App : Application() { override fun onCreate() { super.onCreate() Thread.setDefaultUncaughtExceptionHandler { _, throwable -> FirebaseCrashlytics.getInstance().recordException(throwable) } }}// 2. ViewModel-level handlerprivate val exceptionHandler = CoroutineExceptionHandler { _, throwable -> FirebaseCrashlytics.getInstance().recordException(throwable) _uiState.update { it.copy(error = throwable.toUiError()) }}fun loadProducts() { viewModelScope.launch(exceptionHandler) { runCatching { repository.getProducts() } .onSuccess { products -> _uiState.update { it.copy(products = products) } } .onFailure { e -> FirebaseCrashlytics.getInstance().recordException(e) } }}// 3. RxJava interop plugin (required when legacy modules coexist)RxJavaPlugins.setErrorHandler { e -> if (e is UndeliverableException) { FirebaseCrashlytics.getInstance().recordException(e.cause ?: e) return@setErrorHandler } Thread.currentThread().uncaughtExceptionHandler?.uncaughtException(Thread.currentThread(), e)}
Asking Claude Code "Enumerate every launch call in this ViewModel that doesn't pass a CoroutineExceptionHandler" closes the remaining gaps via grep-style triage.
3. The trap when asking Claude Code to write Room migrations
Ask "add a column" and Claude Code will hand you the ALTER TABLE SQL — but if fallbackToDestructiveMigration() is still in place, your production users' data disappears on the next update. I learned this only after favorites-wallpaper-vanished reports started coming in.
Four operational rules to lock down:
fallbackToDestructiveMigration() is dev-only. Never let it survive into release builds (a lint rule is the simplest enforcement).
Split each MIGRATION_X_Y into its own file so PR diffs stay readable.
Set exportSchema = true and commit the JSON schemas to VCS. Claude Code can read the old schema to generate diff SQL accurately.
Run every migration through MigrationTestHelper in CI. Without replaying production data, foreign-key constraints will eventually break.
A single prompt — "Read schemas/com.example.MyDatabase/3.json and 4.json, write MIGRATION_3_4, and also generate a MigrationTestHelper test" — gets all three artifacts at once.
4. ProGuard / R8 — go for "minimal keep" rules
"Keep everything" disables code shrinking and inflates APK size. Switching to minimal keep rules brought my APK from 18.4 MB to 12.1 MB (a 34% reduction) and cleared the Google Play "app size warning" in one move.
Paste the "Top crashes (last 7 days)" from Crashlytics into Claude Code and ask: "Generate the minimal -keep rules required for proguard-rules.pro from these crashes. Wildcard -keep class ** is not allowed." For reflection-based libraries like GSON, the trick is to keep data class fields only, not methods.
# ✅ Good: protect only the data class fields-keepclassmembers class com.example.myapp.data.dto.** { <fields>;}# ❌ Bad: keep all members (APK size balloons)-keep class com.example.myapp.data.dto.** { *; }
5. WorkManager behavior diverges by OEM (OEM doze)
A WorkManager periodic job that the docs say runs every 15 minutes will, on Xiaomi/OPPO/Vivo devices, often not fire for 6+ hours. Each OEM ships its own aggressive battery optimization (doze-adjacent) layer.
I asked Claude Code to generate the OEM-specific fallback this way:
Periodic Work is supposed to fire every 15 minutes but isn't running for 22% of users
on Xiaomi/OPPO/Vivo devices. Branch on Build.MANUFACTURER and produce a helper that
launches the appropriate Intent to the "auto-start permission" / "battery optimization
exemption" settings screen for each OEM.
There's no perfect solution, but consolidating the user guidance into a Compose BottomSheet improved periodic sync success rate from 62% to 87% after rollout.
Design Calls That Years of Indie Android Shipping Surface
Beyond the official usage docs, here are the judgment calls I make when wiring Claude Code into Android apps that ship continuously and need to monetize through AdMob.
Code-sharing ratio and AdMob eCPM
Standardizing on Compose × Hilt × Room across multiple apps brings the practical code-sharing ratio to 70–80%. In my case, six of eight wallpaper apps share nearly identical feature modules, and the effort to add a new title dropped to 3–4 days (half of what it used to be).
The downside of pushing standardization is longer build times and tighter dependency graphs. A monthly pass with Claude Code — "Visualize the dependency graph of this feature module and list circular dependencies and unused dependencies" — uses the :app:dependencies output to keep build-time regression visible.
On the AdMob side, Compose-rendered UI has noticeably shorter PaintInteract delay for ad rendering, and eCPM trends around 12% higher (though I suspect session retention is the actual driver more than UI speed alone).
The "have Claude Code write the implementation note first" strategy
Before writing a new feature module I always ask Claude Code: "Write the implementation note for this feature in Markdown — four sections: requirements, data flow, state transitions, exception cases." That note lives at docs/features/<name>.md and gets written before any code. Three months later, when I'm back in the same file, the design rationale is there to argue with.
The other payoff: refactor requests become much sharper. Prefixing prompts with "Take docs/features/cart.md into account when refactoring" constrains Claude Code's suggestions to what won't violate the original design intent.
Compose × XML legacy mix — pick your battles
Apps that have been around for 10+ years will almost always have legacy XML layouts coexisting with Compose. Rewriting everything to Compose is rarely a good investment. The priority order I use:
Migrate screens with high ad-impression frequency first — recomposition control translates to visible eCPM gains.
Migrate screens with complex state next — escaping the findViewById swamp alone halves maintenance overhead.
Defer static screens like help and terms-of-service — they're touched once every few years.
Asking Claude Code "Rewrite this XML layout in Compose, and also propose an AndroidViewBinding-wrapped staged migration that preserves the existing ViewModel wiring" produces phased options. Choosing not to rewrite everything in one go is what makes long-lived apps survivable.
Designing the first 60 seconds after install — with Claude Code
What a user does in the first 60 seconds after installing from the Play Store dominates retention math. In my apps, Day-1 retention moved from 18% to 31% almost entirely through changes to the first-minute experience.
Ask Claude Code: "List the chronological sequence from MainActivity.onCreate to the first Composable paint, with estimated milliseconds per step, and propose what to cut." The typical bottlenecks that surface are Hilt eager initialization, I/O in Room init blocks, and synchronous Firebase Remote Config fetches.
Pre-Release Checklist — 14 Items I Run Before Every Google Play AAB Upload
I walk through these items before uploading any AAB to Google Play Console. Skipping even one tends to cause "policy violation notice," "review rejection," or "ANR spike." Wiring Claude Code into the verification flow makes this routine instead of stressful.
minSdk and targetSdk policy deadlines — Google Play raises the targetSdk floor each August or so. Plan to upgrade 2 months before the deadline.
Include 64-bit ABIs — arm64-v8a and x86_64 must be in your build config. Double-check CMake abiFilters if you ship native libraries.
Ship App Bundle (.aab), not APK — direct APK upload is no longer allowed for new submissions.
ProGuard / R8 enabled — confirm minifyEnabled true is set on the release build.
Crashlytics and Performance Monitoring active in release — verify the firebase-perf Gradle plugin is applied to the release variant.
Data Safety form matches actual SDK behavior — every data point collected by AdMob, Firebase Analytics, and billing libraries must be declared. False declarations get apps removed.
Ad ID permission — Android 13+ requires com.google.android.gms.permission.AD_ID explicitly in AndroidManifest (mandatory for targetSdk 33+).
Store screenshots match current UI — outdated screenshots don't fail review, but they crush conversion and rating.
In-app purchase pricing tiers are correctly localized — 1:1 conversion across JP / US / emerging markets is a mistake. A prompt — "Convert JPY ¥250 to 30 major currencies using Purchasing Power Parity" — produces a usable table.
<queries> element declared — required in AndroidManifest from Android 11+ if your app queries other apps.
Edge-to-edge enabled — Android 15+ enforces edge-to-edge by default, so WindowCompat.setDecorFitsSystemWindows(window, false) and Modifier.windowInsetsPadding need to be consistently applied.
ANR monitoring — confirm "User-perceived ANR rate" in Play Console Vitals stays below 0.47% (above that, search ranking drops).
App can start offline — toggle airplane mode and verify a minimal cached screen renders at launch.
Uninstall and data-deletion paths — beyond the OS-level uninstall, your app needs an in-app "delete my data" path (GDPR / similar laws).
Wrap up with a single prompt: "Read AndroidManifest.xml, build.gradle.kts, and MainActivity.kt, then verify the 14 items above and report what's missing by item number." On my own apps, 2–3 items reliably fail this check every release cycle.
One Thing to Do Today
If you can only do one thing today: paste the Top 5 crashes from Crashlytics into Claude Code and resolve the missing minimal -keep rules and CoroutineExceptionHandler gaps. On my own apps, that single move cut monthly crashes from 1,200 to 350 (a 71% reduction).
Years of indie shipping, in retrospect, weren't built from anything heroic. They were built from this kind of weekly loop: observe, fix one thing, ship, observe again. What Claude Code changed is the cadence — that loop now runs in five-minute increments instead of hour-long ones. The volume of observations went up, which means the judgment side (the human side) carries more weight. Writing the non-negotiable principles into CLAUDE.md, and forcing Claude Code to reference them throughout the dialogue — those two are the parts I'd rather not delegate.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.