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.
A step-by-step breakdown of how I release Android apps to Google Play as a solo developer — versioning, signing, AAB builds, store listing, and the checklist that prevents me from shipping broken releases.
On this page
Every Android developer has a release horror story. Wrong keystore. Forgotten version bump. Screenshot from 3 versions ago. App crashing on the first launch for half your users.
After 22+ apps and hundreds of releases, I've reduced that risk to near zero. Here's the exact workflow.
Before I touch a build command, I run through this list:
PRE-RELEASE CHECKLIST
─────────────────────────────────────
[ ] Version bumped (versionName + versionCode)
[ ] CHANGELOG.md updated with new features/fixes
[ ] All debug logging removed or guarded by BuildConfig.DEBUG
[ ] ProGuard rules verified (no R8 surprises)
[ ] Signing config using local.properties (not hardcoded)
[ ] Target SDK is current (36)
[ ] Permissions in manifest match actual usage
[ ] Privacy policy URL is live
[ ] Store screenshots are current (not from 3 versions ago)
[ ] Release notes written in plain language
[ ] Tested on physical device + emulator (API 24 + API 34)
[ ] Crash-free session rate > 99% on existing versionThis isn't a "nice to have." I run it every time. Missing one item has cost me a rollback.
All my apps use the same version file:
# config/version.properties
VERSION_MAJOR=1
VERSION_MINOR=4
VERSION_PATCH=0
VERSION_CODE=52The
build.gradle.ktsimport java.util.Properties
val versionProps = Properties().apply {
load(rootProject.file("config/version.properties").inputStream())
}
android {
defaultConfig {
versionName = "${versionProps["VERSION_MAJOR"]}.${versionProps["VERSION_MINOR"]}.${versionProps["VERSION_PATCH"]}"
versionCode = (versionProps["VERSION_CODE"] as String).toInt()
}
}Why this matters: Never hardcode version numbers in
build.gradleI use a script that handles the math:
#!/bin/bash
# version-bump.sh patch|minor|major [repo-name]
BUMP_TYPE=${1:-patch}
PROPS_FILE="config/version.properties"
# Read current values
MAJOR=$(grep VERSION_MAJOR $PROPS_FILE | cut -d= -f2)
MINOR=$(grep VERSION_MINOR $PROPS_FILE | cut -d= -f2)
PATCH=$(grep VERSION_PATCH $PROPS_FILE | cut -d= -f2)
CODE=$(grep VERSION_CODE $PROPS_FILE | cut -d= -f2)
# Increment
CODE=$((CODE + 1))
case $BUMP_TYPE in
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
patch) PATCH=$((PATCH + 1)) ;;
esac
# Write back
sed -i '' "s/VERSION_MAJOR=.*/VERSION_MAJOR=$MAJOR/" $PROPS_FILE
sed -i '' "s/VERSION_MINOR=.*/VERSION_MINOR=$MINOR/" $PROPS_FILE
sed -i '' "s/VERSION_PATCH=.*/VERSION_PATCH=$PATCH/" $PROPS_FILE
sed -i '' "s/VERSION_CODE=.*/VERSION_CODE=$CODE/" $PROPS_FILE
echo "Bumped to $MAJOR.$MINOR.$PATCH (code: $CODE)"Run
./version-bump.sh patchThis is where most developers make dangerous mistakes. Here's the safe setup:
# local.properties (NEVER commit this file)
KEYSTORE_PATH=/Users/sudarshan/keystores/myfamilytracker.jks
KEYSTORE_PASSWORD=your-password-here
KEY_ALIAS=myfamilytracker
KEY_PASSWORD=your-password-here// build.gradle.kts
import java.util.Properties
val localProps = Properties().apply {
val f = rootProject.file("local.properties")
if (f.exists()) load(f.inputStream())
}
android {
signingConfigs {
create("play store") {
storeFile = localProps["KEYSTORE_PATH"]?.let { file(it) }
storePassword = localProps["KEYSTORE_PASSWORD"] as? String
keyAlias = localProps["KEY_ALIAS"] as? String
keyPassword = localProps["KEY_PASSWORD"] as? String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("play store")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}Critical rule:
must be incodelocal.properties. If you accidentally commit credentials, rotate them immediately — even if the repo is private.code.gitignore
Always ship an AAB (Android App Bundle), never an APK, to Google Play. AABs let Play Store generate optimized APKs per device. Smaller download size, faster installs, better user retention.
# Clean build to avoid cache issues
./gradlew clean
# Build release AAB
./gradlew bundleRelease
# Output location
ls -la app/build/outputs/bundle/release/app-release.aabI always do a clean build for releases. Build cache has caused me subtle bugs exactly once — that was enough.
Before uploading, I verify the signing:
# Check the AAB is signed with the right key
bundletool validate --bundle=app/build/outputs/bundle/release/app-release.aab
# Extract APKs locally to test on device
bundletool build-apks \
--bundle=app/build/outputs/bundle/release/app-release.aab \
--output=app.apks \
--ks=myfamilytracker.jks \
--ks-key-alias=myfamilytracker
bundletool install-apks --apks=app.apksWrite release notes in plain language. Not developer speak:
❌ "Fixed NPE in LocationService initialization"
✅ "Fixed a crash that some users experienced when first opening the app"
❌ "Migrated from deprecated API to LocationManager v2"
✅ "Improved location accuracy and battery efficiency"Your users don't know what an NPE is. They know "the app crashed."
I never release to 100% immediately. My default rollout:
Day 1: 10% rollout
Day 2: Check crash-free rate in Play Console
Day 3: 25% rollout (if stable)
Day 5: 50% rollout
Day 7: 100% rolloutIf crash-free rate drops below 99%, I halt and investigate before expanding. Play Console shows you crash data within hours.
After every release, I monitor for 48 hours:
// Crashlytics setup in Application class
@HiltAndroidApp
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
if (!BuildConfig.DEBUG) {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
}
}
}Never enable Crashlytics in debug builds. You'll drown in noise from your own testing.
Play Store lets you halt a rollout instantly. If you see a spike in crashes after expanding to 25%:
I've done emergency rollbacks. The key is catching problems at 10% before they reach everyone.
| Step | Time |
|---|---|
| Run pre-release checklist | 10 min |
| Bump version | 1 min |
| Clean + build AAB | 8-12 min |
| Verify signing | 2 min |
| Install + smoke test | 10 min |
| Write release notes | 5 min |
| Upload to Play Console | 5 min |
| Submit for review | 2 min |
| Total | ~45 min |
Google Play review takes 1-3 days for updates (longer for new apps). Plan accordingly — if you're fixing a critical bug, submit as soon as the fix is ready.
Keep your keystores backed up in at least two places. Losing your keystore means you can never update your app — you'd have to publish it as a completely new app and lose all your reviews, ratings, and download history.
I store keystores in an encrypted backup on two separate drives. Not on GitHub. Not on cloud storage with public access. Offline, encrypted, in two physical locations.
This is the one disaster you can't recover from. Treat your keystores accordingly.
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.
Related Posts
Related Apps
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 meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read