Skip to content
All posts
May 21, 20265 min read

My Android Release Checklist: Zero Bad Releases in 2 Years

A detailed, battle-tested checklist for solo Android developers to ensure stable, secure, and high-performance releases. Covers automated testing, CI/CD, performance monitoring, and final sanity checks.

AndroidKotlinTestingCI/CD
Share:

In the past two years, I've shipped over 50 Android app updates without a single critical crash or user-facing regression. As a solo developer with a QA background, I learned early that a rigorous release process isn't optional—it's the price of sanity. This checklist is my non-negotiable system for shipping confidently.

The Problem: Why Solo Devs Can't Afford Bad Releases

When you're the only engineer, designer, and product manager, a single bad release can destroy user trust, flood you with support tickets, and set your project back weeks. My 13+ years in QA taught me that release quality is a process, not luck. Without a systematic approach, even simple updates can introduce subtle bugs that slip through manual testing. This checklist evolved from real failures—each item here solves a problem I've personally faced.

1. Automated Testing: The Non-Negotiable Gate

Every release starts with a locked testing gate. I require 100% unit test coverage on all new code and critical paths, plus a robust UI test suite that runs on every commit. My rule: if a test can't be automated, it's not a reliable check.

I use a tiered testing strategy:

  • Unit tests (JUnit + Mockito) for business logic, data repositories, and use cases.
  • Instrumented tests (Compose UI tests) for critical user journeys.
  • End-to-end tests (ActivityScenario + Espresso) for multi-screen flows.
kotlin
// Example: A unit test for a login use case
@Test
fun `login with valid credentials succeeds`() = runTest {
    val mockRepo = mock<AuthRepository> {
        on { login("user@example.com", "password123") } doReturn Result.success(User("John"))
    }
    val useCase = LoginUseCase(mockRepo)
    
    val result = useCase("user@example.com", "password123")
    
    assertTrue(result.isSuccess)
    assertEquals("John", result.getOrThrow().name)
}

[!IMPORTANT]
Tests must run in isolation and fail fast. I configure Gradle to stop the build on the first test failure.

Test Coverage Thresholds

Test TypeMinimum CoverageExecution Time
Unit Tests100% of new code< 2 minutes
UI TestsCritical paths only< 5 minutes
E2E TestsCore user journeys< 10 minutes

I enforce these thresholds with a custom Gradle task that blocks the release build if any metric falls short.

2. CI/CD Pipeline: From Commit to Release-Ready

My CI pipeline is the engine that enforces the checklist. I use GitHub Actions because it's cost-effective for solo projects and integrates seamlessly with Play Store.

The pipeline has three stages:

  1. Build & Test: Compiles the app, runs all tests, and checks code quality.
  2. Build Artifacts: Generates signed APKs and app bundles, then runs lint and dependency checks.
  3. Pre-Release Validation: Deploys to an internal track for final QA.
yaml
# .github/workflows/release.yml (simplified)
name: Release Pipeline
on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: '17'
      - name: Run unit tests
        run: ./gradlew testDebugUnitTest
      - name: Run UI tests
        run: ./gradlew connectedDebugAndroidTest

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Build signed bundle
        run: ./gradlew bundleRelease
        env:
          SIGNING_KEY: ${{ secrets.ANDROID_SIGNING_KEY }}

  validate:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to internal track
        uses: rorochamp/android-play-publisher@v1
        with:
          service_account_json: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
          package_name: com.sudarshantechlabs.myapp
          track: internal
          apk_path: app/build/outputs/bundle/release/app-release.aab

[!TIP]
Always use a separate internal test track in the Play Console. It's your safety net for real-device validation without affecting production users.

3. Performance and Crash Monitoring: The Safety Net

Even with perfect tests, production is unpredictable. I monitor four key metrics: crash rate, ANR rate, API latency, and battery impact. My threshold for release is simple: zero critical crashes and ANR rate below 0.1%.

I use Firebase Crashlytics for crash reporting and Performance Monitoring for real-time metrics. The setup is straightforward but critical:

kotlin
// Initialize Crashlytics and Performance Monitoring
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
        FirebasePerformance.getInstance().startTrace("app_startup")
    }
}

Performance Thresholds for Release

MetricGreen ZoneYellow ZoneRed Zone (Block Release)
Crash Rate< 0.01%0.01% - 0.1%> 0.1%
ANR Rate0%< 0.05%> 0.05%
API Latency (95th)< 500ms500ms - 2s> 2s
Battery ImpactNegligibleModerateHigh

Before every release, I review the last 7 days of data from the internal test track. If any metric is in the red zone, I halt the release until it's fixed.

4. The Pre-Release Manual Checklist

Automation catches 95% of issues, but the final 5% require human judgment. I run through this checklist on a physical device (not an emulator) the day before release.

Sudarshan's Final Sanity Checklist

CheckPass/FailNotes
App installs from Play StoreNot just from ADB
First-launch tutorial worksFor new users
Push notifications triggerUse Firebase Console
Offline mode handles gracefullyAirplane mode test
Permissions request correctlyCamera, location, etc.
Deep links open proper screensFrom browser/email
Accessibility services workTalkBack enabled
Battery optimization exemptSettings > Battery
Data usage in background< 10MB/day
Play Store listing screenshots matchNo surprises

[!WARNING]
Never skip the physical device test. Emulators don't replicate real-world conditions like network switching, battery drain, or manufacturer-specific quirks.

Key Takeaways

  • Automate everything that can fail: Unit tests, UI tests, and CI checks are your first line of defense.
  • Monitor in production before you ship: Use internal test tracks and performance thresholds to catch issues early.
  • Always do a final manual run on a physical device: No amount of automation replaces real-world validation.
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