Skip to content
All posts
June 14, 20266 min read

Maintaining 22 Apps Taught Me That Boring Is the Whole Point

After 13 years running 22+ Android apps solo from Bangkok, I break down the unglamorous maintenance systems—Gradle conventions, CI pipelines, versioning discipline—that keep everything alive.

AndroidKotlinCI/CDSolo Dev
Share:

I don't write features anymore. I patch crashes at 2 AM. I bump SDK versions before Google sunsets them. I review dependency updates that nobody asked for. Maintaining 22 apps is the least sexy work in tech—and it's the only reason any of them still exist.

When I started SudarshanTechLabs from a coworking space in Bangkok, I thought shipping fast was the game. Build the app, push to Play Store, move on. That lasted about six months. Then Android 14 dropped. Then Google deprecated WebViewCustomization. Then one of my apps started force-closing on Samsung devices with one UI 6.3. Twenty-two apps means twenty-two surfaces for decay.

The Maintenance Tax Nobody Calculates

Every app you own carries a recurring cost. Not in money—mostly in decisions. Each release cycle demands a version bump, a changelog entry, a build verification, a Play Store submission. Multiply that by 22 and you're drowning in mechanical work that has nothing to do with code quality.

I tracked my maintenance hours for three months across all apps. Here's the split:

TaskHours per Month (avg)% of Dev Time
New feature development1220%
Bug fixes from user reports1830%
Dependency & SDK updates1423%
CI/CD pipeline fixes813%
Play Store asset updates610%
Misc (signing, analytics, ads)54%

[!IMPORTANT] If you're a solo developer, dependencies and SDK updates alone will consume a quarter of your time. Automate or die.

The takeaway is brutal: 77% of my hours go to keeping the lights on. Feature work is the reward, not the default.

My Gradle Convention That Saves Hours Every Week

I enforce a strict versioning and dependency convention across every module in every app. Every project shares the same

code
versions.toml
file. No guessing. No drift.

toml
[versions]
agp = "8.7.3"
kotlin = "2.0.21"
composeBom = "2024.12.01"
hilt = "2.52"
room = "2.6.1"
coroutines = "1.9.0"

[libraries]
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }

Every app pulls from this file. When I need to bump Hilt across 22 apps, I change one line and run a script.

bash
#!/bin/bash
# update-all.sh — run from project root
for dir in apps/*/; do
  echo "Updating $dir..."
  (cd "$dir" && ./gradlew dependencyUpdates --init-script update.gradle)
done

This alone cut my per-app update time from 20 minutes to 5. Multiply by 22 and you save hours per cycle.

[!TIP] Keep a single

code
versions.toml
in a shared repo. Reference it via
code
../versions.toml
in each app. Never hardcode versions inside
code
build.gradle.kts
.

CI/CD Pipeline That Catches Regressions Before Users Do

I run GitHub Actions on every push to

code
main
. Each pipeline builds, runs unit tests, runs instrumented tests on a Firebase Test Lab matrix, and uploads a signed APK to an internal artifact store.

yaml
name: Build and Test All Apps
on:
  push:
    branches: [main]

jobs:
  build-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        app: [app1, app2, app3, app4, app5, app6, app7, app8, app9, app10]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: "17"
          distribution: "temurin"
      - name: Build and Test
        run: |
          cd apps/${{ matrix.app }}
          ./gradlew assembleDebug testDebugUnitTest
      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.app }}-debug
          path: apps/${{ matrix.app }}/build/outputs/apk/debug/*.apk

I don't test every app on every push. I rotate a subset weekly. Full matrix runs on release branches only. This keeps CI costs under $30/month while catching 90% of regressions.

[!WARNING] Don't run full instrumented test matrices on every push. Your CI bill will eat your margins. Reserve full test runs for release candidates.

I also maintain a

code
health-check.sh
script that runs locally before I push:

bash
#!/bin/bash
# health-check.sh
echo "Running pre-push checks..."

for app in apps/*/; do
  echo "Checking $app..."
  (cd "$app" && ./gradlew lintVitalRelease --quiet)
  if [ $? -ne 0 ]; then
    echo "Lint failed for $app. Fix before pushing."
    exit 1
  fi
done

echo "All apps passed lint."

This catches dependency conflicts, unused resources, and API mismatches before they hit CI.

Monitoring Crashes Without a Team

With 22 apps, crash reporting is not optional. I use Firebase Crashlytics across every app with a unified alerting rule. All apps send crashes to a single Firebase project organized by app ID.

I set a threshold alert: if any app exceeds 0.5% crash rate over a 24-hour window, I get a Slack notification.

kotlin
// BaseApplication.kt — shared across all apps
class BaseApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(BuildConfig.DEBUG.not())
        FirebaseCrashlytics.getInstance().log("App started: ${BuildConfig.FLAVOR}-${BuildConfig.BUILD_TYPE}")
    }
}

I wrote a small Kotlin script that queries the Firebase REST API weekly and dumps a crash summary into a Google Sheet. This replaces manual dashboard checks.

[!NOTE] Keep crash rate alerts low (0.5% threshold). High thresholds mean you're ignoring problems until users leave reviews.

The Play Store Routine That Kills Momentum

Every app update requires Play Store metadata. Screenshots need refreshing when UI changes. Promotional graphics need updating for seasonal campaigns. For 22 apps, this is a full day of work per quarter.

I batch metadata updates. Every first Monday of the month, I spend 4 hours updating changelogs, screenshots, and descriptions for all apps that shipped changes that month. I maintain a simple CSV tracker:

App NameLast UpdatedNext Screenshot RefreshOpen Bugs
AppOne2026-05-202026-06-150
AppTwo2026-05-222026-06-152
AppThree2026-05-282026-07-010

This tracker lives in the repo's root. I update it during my monthly batch session. No app falls through the cracks.

Key Takeaways

  • Standardize your versioning. One
    code
    versions.toml
    across all apps eliminates drift and makes bulk updates a one-line change.
  • Automate your pre-push checks. A 30-second lint run per app before pushing saves hours of CI failures and broken releases.
  • Batch maintenance work. Monthly metadata, weekly crash reviews, and quarterly dependency audits prevent the death-by-a-thousand-cuts that kills solo devs.
  • Monitor with a low threshold. 0.5% crash rate alerts catch real problems before they become review-level complaints.
  • Track everything in a single sheet. A plain CSV beats a fancy dashboard when you're maintaining 22 apps alone.
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this