RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/App Dev
App Dev/2026-05-29Intermediate

Two Weeks of Maintenance After v2.1.0 — Running Crashlytics Triage Through Claude in Chrome

Notes from operating Beautiful 4K/HDR Wallpapers v2.1.0 and Ukiyo-e Wallpapers v1.8.0 through phased rollout. Defensive RecyclerView copies, Glide desugaring, drawable-nodpi placement, and what I now hand off to Claude in Chrome every morning.

Android43Maintenance2Crashlytics12Phased RolloutRecyclerView3Glide2Indie Developer11Claude in Chrome4

I have been shipping iOS and Android apps as an indie developer since 2014, and I keep a 50M-cumulative-download wallpaper lineup alive by myself. Beautiful 4K/HDR Wallpapers v2.1.0 (versionCode 49) and Ukiyo-e Wallpapers v1.8.0 (versionCode 41) just cleared Google Play review and are now in phased rollout. Two weeks in, I want to write down what post-release maintenance looks like in practice, and what I now hand off to Claude in Chrome.

The first 48 hours are the dangerous ones

My standard Android rollout schedule is 5% → 25% → 50% → 100%, with each step running for 24 to 48 hours. The most nerve-wracking window is the first 48 hours at 5%. If Crashlytics's Crash-free users metric dips below 99.7%, or ANR pushes past 0.20%, I halt the rollout immediately by setting the share to 0%.

I halted twice during the v2.0.0 rollout. WallpaperPagerAdapter and ThumbnailViewPagerAdapter were throwing IndexOutOfBoundsException during scroll, and in 28 days the crash hit 50+ users across 56+ events.

The root cause was holding a direct reference to the list. The RecyclerView was reading the data source while it was being updated, hitting a race. One line fixed it: copy defensively in setItems.

// Before: shared reference
public void setItems(List<Wallpaper> list) {
    this.mList = list;
    notifyDataSetChanged();
}
 
// After: defensive copy
public void setItems(List<Wallpaper> list) {
    this.mList = new ArrayList<>(list);
    notifyDataSetChanged();
}

Eight years of running this app and the conclusion is unromantic: RecyclerView crashes are almost always shared list references.

minSdk 23 and Glide 5.0.5's hard requirement on desugaring

Right after v2.0.0 shipped, Crashlytics flagged that every single Android 6.0.1 (API 23) user was crashing within three seconds of launch. 12 events, 4 users over 7 days, all tagged "early crash."

Glide 5.0.5 uses java.util.function.Supplier internally, and at minSdk 23 desugaring is mandatory. My build.gradle had the dependency coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5' but I had not flipped the coreLibraryDesugaringEnabled true flag in compileOptions.

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
        coreLibraryDesugaringEnabled true  // <-- I had missed this
    }
}
 
dependencies {
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}

When a library's dependency tree requires Java 8 APIs, adding the desugar dependency is not enough; you have to enable the compileOptions flag too. A small, dull lesson worth writing down.

A resource that vanished under Play Store density split

Some Google Pixel users on Android 12 hit Resources$NotFoundException at startup. The crash report pointed at MainActivity.onCreate:120, where I referenced the background_silver drawable.

The cause was placing background_silver.jpg only in drawable-xxhdpi/ and drawable-xxxhdpi/. When the Android App Bundle gets density-split by Play Store, the APK delivered to lower-density buckets did not contain the resource.

The fix is to also drop the file into drawable-nodpi/. drawable-nodpi/ is included in every density split APK, so it is the safe location for resources that must exist.

app/src/main/res/
├── drawable-xxhdpi/background_silver.jpg
├── drawable-xxxhdpi/background_silver.jpg
└── drawable-nodpi/background_silver.jpg  <-- reaches every density

In the App Bundle era, deciding between density-specific directories and nodpi tends to fall between design and ops. I now keep a simple rule: critical resources go in nodpi, quality-sensitive ones go in density-specific buckets. The class of bug went away.

What I now hand off to Claude in Chrome for Crashlytics

The first 48 hours of a rollout need human eyes. Past that window, I hand off first-pass crash triage to Claude in Chrome every morning.

The routine is straightforward. Open the Firebase Console Crashlytics tab and sort yesterday's new crashes by these criteria:

  • Crashes that pushed Crash-free users below 99.7%
  • Crashes affecting more than 10 users
  • Status "Active" and "Untouched"

After Claude in Chrome lays the list out, I decide which crashes actually need a response. The stack trace reading I delegate; the decision to write a patch is mine, based on blast radius and fix cost.

The gotcha is that Firebase Console redesigns its UI periodically, so my prompt now always asks Claude to confirm "which filters are currently active." Without that confirmation, the filter sometimes silently drops and yesterday's crashes look like new ones.

Synthesizing ad-free state from multiple stores

The most delicate piece in post-v2.0.0 maintenance has been the ad-free state. Globals.SHOW_AD is the synthesis of two independent persistent stores:

  • BillingManager — lifetime IAP and subscriptions
  • AdFreeManager — time-limited ad-free earned by watching rewarded ads

If anywhere in the code calls setAdFree(false) directly, users who are ad-free via BillingManager get accidentally flipped back to ad-supported. The rule is to always route through the synthesis check (isAdFree || isRewardAdFree).

Earlier there was a logic-inversion bug in BillingManager.restorePurchases() where, if the user was ad-free but the subscription had expired, no path existed to flip back to false. I rebuilt that path with a finalEvaluation pattern: run both inapp and subs queries in parallel, wait for both, then synthesize. I also moved restorePurchases() to Activity.onResume() so an expiration during a long session is detected on the next resume.

When you touch billing as an indie developer, the shortest path is to admit upfront that "single source of truth" has multiple sources, and design the synthesis layer explicitly.

User settings vs. API defaults

Server-side defaults (shuffle, personalized-ads on/off) returned at app launch will conflict with what the user changed in the Settings screen. I settled on a *_USER_OVERRIDE flag per setting, stored under a separate key.

  • Saves originating from user action go through a dedicated *ByUser() method, which sets the override at the same time
  • Saves originating from the system skip the write if the override is set

This way the server can change its defaults without overwriting user choices, and clearing the override is enough to fall back to the API default. It is not glamorous, but it makes the precedence rules debuggable.

Rate-limited multilingual review replies

The other surprisingly heavy maintenance task is user review replies. I once handled 72 review replies in a single session across four apps and six major countries on the App Store alone. There are traps.

  • Sending more than 30–40 in a single session risks Google's spam detection
  • The App Store has rate limits; about 8 seconds between sends keeps things safe
  • Pasting machine translation can be flagged as policy violation, so I write in a human-toned voice
  • Affiliate links, cross-app promotion, and social links are not allowed

I currently reply in 11 languages: Japanese, English, Traditional Chinese, Italian, Russian, Korean, Persian, Ukrainian, Thai, Polish, and Brazilian Portuguese. Claude in Chrome handles the translation in a human-toned voice, and the prompt explicitly enforces an eight-second delay between sends.

Maintenance is three layers: monitor, triage, decide

Stepping back across v2.0.0 to v2.1.0, the loop reduces to three layers:

  1. Monitor — Check Crashlytics and Play Console quality metrics every morning. Most of it goes to Claude in Chrome.
  2. First-pass triage — Read stack traces of new crashes, name the blast radius and the pattern. A mix of AI and human.
  3. Fix decision — Look at impact and fix cost, then choose: patch, halt the rollout, or ignore. Human.

Running six apps as an indie developer, doing the monitoring layer by hand does not fit into the day. Hand the mechanical parts to AI and protect the cognitive bandwidth for layer 3. That has been the working trade-off.

Starting tomorrow, the AdMob mediation expansion (Liftoff, Unity Ads, InMobi) and the iOS port of the slideshow feature start running in parallel. I plan to hand v2.1.0 maintenance off to AI and pour the focus into the new work for the week.

Share

Thank You for Reading

Rork 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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-05-28
Adding Slideshow and Page Jump to My Android Wallpaper Apps
Implementation notes from adding slideshow, a page-jump slider, and an ad-free option to the Android editions of Beautiful 4K/HDR Wallpapers and Ukiyo-e Wallpapers.
App Dev2026-06-25
Crashes Only in the Release Build — Rescuing Classes R8 Stripped in Expo (Android)
I turned on R8 code shrinking to slim down an AAB, and one screen started crashing only in production. Here is how I traced the stripped class through mapping.txt and added keep rules via expo-build-properties.
App Dev2026-05-31
Fixing the 'Signed With the Wrong Key' Error When Uploading a Rork App to Google Play
Your Rork app builds fine but Google Play rejects the upload with 'signed with the wrong key'? Here's how to tell which signing key is involved and the exact steps to fix it for each build setup.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →