Skip to main content

Build & Release

This page covers how to build release artifacts and submit them to the App Store (iOS) and Google Play (Android).


Versioning

Version numbers are managed in two places and must be kept in sync:

AndroidcomposeApp/build.gradle.kts:

android {
defaultConfig {
versionCode = 135 // Integer, increment on every store submission
versionName = "2.0.0" // Semantic version shown to users
}
}

iOSiosApp/Configuration/config.xcconfig:

MARKETING_VERSION = 2.0.0     // Displayed version (CFBundleShortVersionString)
CURRENT_PROJECT_VERSION = 135 // Build number (CFBundleVersion)
info

versionCode (Android) and CURRENT_PROJECT_VERSION (iOS) must be incremented for every upload to the stores, even if the user-facing version number (versionName / MARKETING_VERSION) does not change.


Android — Google Play

1. Configure release signing

Create a production keystore if you don't have one:

keytool -genkey -v \
-keystore release.keystore \
-alias association-app \
-keyalg RSA \
-keysize 2048 \
-validity 10000

Add signing config to composeApp/build.gradle.kts:

android {
signingConfigs {
create("release") {
storeFile = file("../release.keystore")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = "association-app"
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true // Enable for production
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
caution

The keystore file and passwords must be kept secret. Never commit the keystore or plaintext passwords to version control. Use environment variables or a secrets manager.

2. Pre-release checklist

Before building a release APK or AAB:

  • Remove android:usesCleartextTraffic="true" from AndroidManifest.xml (development only)
  • Set AppConfig.API_BASE_URL to the production API URL
  • Set AppConfig.USE_MOCK_DATA = false
  • Increment versionCode in build.gradle.kts
  • Update versionName if this is a new user-facing version
  • Verify google-services.json references the production Firebase project
  • Confirm Firebase Crashlytics and Analytics are enabled

Google Play prefers the Android App Bundle (AAB) format:

./gradlew :composeApp:bundleRelease

Output: composeApp/build/outputs/bundle/release/composeApp-release.aab

To build a signed APK instead:

./gradlew :composeApp:assembleRelease

Output: composeApp/build/outputs/apk/release/composeApp-release.apk

4. Upload to Google Play

  1. Open the Google Play Console
  2. Select the Association App application
  3. Go to Release → Production (or the appropriate track: Internal, Alpha, Beta)
  4. Create a new release and upload the .aab file
  5. Complete the release notes and submit for review

iOS — App Store

1. Pre-release checklist

  • Set AppConfig.API_BASE_URL to the production API URL
  • Set AppConfig.USE_MOCK_DATA = false
  • Increment CURRENT_PROJECT_VERSION in config.xcconfig
  • Update MARKETING_VERSION if this is a new user-facing version
  • Verify GoogleService-Info.plist references the production Firebase project
  • Confirm the correct Team and Bundle ID are set in Signing & Capabilities
  • Test on a physical device (push notifications, deep links, payment flows)

2. Archive in Xcode

  1. Open iosApp/iosApp.xcodeproj in Xcode
  2. Select Any iOS Device (arm64) as the build target (not a simulator)
  3. Go to Product → Archive
  4. Wait for the archive to complete — the Organizer window opens automatically

3. Distribute via App Store Connect

In the Organizer:

  1. Select the latest archive
  2. Click Distribute App
  3. Choose App Store Connect
  4. Select Upload (to submit directly) or Export (to get a local IPA for manual upload)
  5. Follow the wizard — Xcode handles code signing and dSYM upload automatically

After upload, open App Store Connect:

  1. Go to the Association App page → TestFlight or App Store tab
  2. Select the new build
  3. Complete review information and submit for Apple review

4. dSYM upload (Kotzilla)

dSYM files enable human-readable crash reports in Kotzilla. An Xcode Run Script build phase automatically uploads dSYMs after every archive:

# Already configured in Xcode build phases
# Uploads dSYM to Kotzilla for symbolication

No manual action is needed. Verify the script is present in Build Phases if crash reports show unsymbolicated addresses.


Firebase Configuration per Environment

The project uses separate Firebase projects per environment. Swap the config files when building for different targets:

EnvironmentAndroidiOS
Developmentgoogle-services-dev.jsonGoogleService-Info-dev.plist
Staginggoogle-services-staging.jsonGoogleService-Info-staging.plist
Productiongoogle-services.jsonGoogleService-Info.plist

Place the correct file at the expected location before building:

# Android
cp config/google-services-prod.json composeApp/google-services.json

# iOS
cp config/GoogleService-Info-prod.plist iosApp/iosApp/GoogleService-Info.plist

App Icon Variants

Dynamic branding (alternate app icons) requires each icon variant to be registered at build time in both platforms — adding a new organisation icon means:

Android:

  1. Add icon resources to composeApp/src/androidMain/res/mipmap-*/
  2. Add a new <activity-alias> in AndroidManifest.xml
  3. Register the variant in AppIconVariant.kt

iOS:

  1. Add an icon asset set to iosApp/iosApp/Assets.xcassets
  2. Add the asset name to the ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES build setting in Xcode
  3. Register the variant in AppIconVariant.kt

A new build and store submission is required whenever icon variants change.


There is no CI/CD pipeline in place yet. The recommended manual process is:

1. Merge feature branches → main
2. Bump versionCode + versionName / MARKETING_VERSION + CURRENT_PROJECT_VERSION
3. Update API_BASE_URL to production
4. Swap production Firebase config files
5. Android: ./gradlew :composeApp:bundleRelease → upload AAB to Play Console
6. iOS: Xcode Archive → Distribute to App Store Connect
7. Submit both for review
8. Tag the release in git: git tag v2.1.0

Build Variants

Android build types

TypeAPI URLMinificationCleartext trafficSigning
debugapi-dev.*OffOnDebug keystore
releaseapi.* (prod)Recommended onOffRelease keystore

Gradle properties (performance)

gradle.properties is already configured for optimal build performance:

org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
kotlin.incremental=true

Increase -Xmx if builds fail with out-of-memory errors on machines with more RAM available.