Build Workflow
Build Android applications and Go libraries for Android, manage dependencies with Gradle, and publish to Google Play Store.
Quick Start
# Navigate to project
cd /path/to/android-project
# Build debug APK
./gradlew assembleDebug
# Build release APK
./gradlew assembleRelease
# Build release AAB (for Play Store)
./gradlew bundleRelease
# Install debug on device
./gradlew installDebug
Build Types
APK (Android Package)
- Direct install on devices
- Can be shared and sideloaded
- Larger file size (contains all ABIs)
- Use for: Development, testing, direct distribution
AAB (Android App Bundle)
- Play Store distribution format
- Smaller download size (dynamic delivery)
- Cannot be directly installed
- Use for: Play Store releases
Gradle Fundamentals
Project Structure
myproject/
├── gradle/
│ ├── libs.versions.toml # Version catalog (recommended)
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── app/
│ └── build.gradle.kts # App module build file
├── build.gradle.kts # Root build file
├── settings.gradle.kts # Project settings
├── gradle.properties # Global Gradle properties
└── local.properties # Local config (not in git)
Version Catalogs (Modern Approach)
gradle/libs.versions.toml:
[versions]
agp = "8.3.0"
kotlin = "1.9.22"
compileSdk = "34"
minSdk = "24"
targetSdk = "34"
androidx-core = "1.12.0"
material = "1.11.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
Root build.gradle.kts:
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
}
App build.gradle.kts:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.myapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.myapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.material)
// Gomobile AAR
implementation(files("libs/mylib.aar"))
}
Managing Dependencies
Add dependency to version catalog:
[versions]
retrofit = "2.9.0"
[libraries]
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
Use in build.gradle.kts:
dependencies {
implementation(libs.retrofit)
}
Dependency scopes:
dependencies {
implementation("...") // Recommended: Not exposed to consumers
api("...") // Exposed to consumers
compileOnly("...") // Compile only (not in APK)
runtimeOnly("...") // Runtime only
testImplementation("...") // Unit tests
androidTestImplementation("...")// Instrumentation tests
debugImplementation("...") // Debug build only
}
Local AAR/JAR files:
dependencies {
// Single AAR
implementation(files("libs/mylib.aar"))
// All AARs in libs directory
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
}
gradle.properties
# Performance
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
# AndroidX
android.useAndroidX=true
android.enableJetifier=true
# Build
android.nonTransitiveRClass=true
Build Variants
Build types:
android {
buildTypes {
debug {
applicationIdSuffix = ".debug"
isDebuggable = true
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(...)
}
create("staging") {
initWith(getByName("debug"))
applicationIdSuffix = ".staging"
}
}
}
Product flavors:
android {
flavorDimensions += "version"
productFlavors {
create("free") {
dimension = "version"
applicationIdSuffix = ".free"
}
create("paid") {
dimension = "version"
applicationIdSuffix = ".paid"
}
}
}
Build specific variant:
./gradlew assembleFreeDebug
./gradlew assemblePaidRelease
Common Gradle Tasks
# List all tasks
./gradlew tasks
# Clean build outputs
./gradlew clean
# Build all variants
./gradlew build
# Check dependencies
./gradlew dependencies
# Show dependency tree
./gradlew app:dependencies --configuration debugRuntimeClasspath
# Refresh dependencies
./gradlew --refresh-dependencies
Building Go Libraries (gomobile bind)
Use gomobile bind to create Android AAR packages from Go code. This allows you to use Go libraries in Android apps written in Java/Kotlin.
When to Use
- You have existing Go code (networking, crypto, business logic)
- You want to integrate Go into an existing Android app
- You need Go’s concurrency or standard library in Android
- You want to share code between backend and mobile
Prerequisites
# Install gomobile
go install golang.org/x/mobile/cmd/gomobile@latest
# Initialize (downloads Android toolchain)
gomobile init
# Verify
gomobile version
Go Code Requirements
What works in gomobile:
- ✅ Exported functions with basic types
- ✅ Exported structs with exported fields
- ✅ Methods on exported structs
- ✅ Interfaces (for callbacks)
- ✅ Error return values (become exceptions)
- ✅ Slices of basic types (
[]byte,[]int,[]string)
What doesn’t work:
- ❌ Maps (use structs instead)
- ❌ Channels (use callbacks via interfaces)
- ❌ Generics
- ❌ Unexported types in function signatures
- ❌ Variadic functions
Example Go library:
// File: golib/network.go
package network
import (
"fmt"
"net/http"
)
// FetchURL fetches content from a URL
func FetchURL(url string) (string, error) {
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Implementation...
return "content", nil
}
// Config represents network configuration
type Config struct {
Timeout int
BaseURL string
}
// NewConfig creates a new Config
func NewConfig(timeout int, baseURL string) *Config {
return &Config{
Timeout: timeout,
BaseURL: baseURL,
}
}
// Callback interface for async operations
type Callback interface {
OnSuccess(data string)
OnError(err error)
}
// FetchAsync fetches URL asynchronously
func FetchAsync(url string, callback Callback) {
go func() {
result, err := FetchURL(url)
if err != nil {
callback.OnError(err)
return
}
callback.OnSuccess(result)
}()
}
Build AAR
Basic build (all architectures):
gomobile bind -target=android -o mylib.aar ./golib
Build specific architectures:
# ARM64 only (most modern devices)
gomobile bind -target=android/arm64 -o mylib.aar ./golib
# ARM64 and ARM32
gomobile bind -target=android/arm64,android/arm -o mylib.aar ./golib
# All common architectures
gomobile bind -target=android/arm64,android/arm,android/amd64,android/386 -o mylib.aar ./golib
Architecture options:
android/arm64: 64-bit ARM (arm64-v8a) - modern devicesandroid/arm: 32-bit ARM (armeabi-v7a) - older devicesandroid/amd64: 64-bit x86 (x86_64) - emulatorsandroid/386: 32-bit x86 (x86) - old emulators
Integrate AAR into Android Project
Copy AAR:
cp mylib.aar /path/to/android-project/app/libs/
Add dependency in app/build.gradle.kts:
dependencies {
implementation(files("libs/mylib.aar"))
}
Sync Gradle:
./gradlew sync
Use in Android Code
Kotlin example:
import golib.Network
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Call Go function
try {
val result = Network.fetchURL("https://example.com")
println("Result: $result")
} catch (e: Exception) {
println("Error: ${e.message}")
}
// Use Go struct
val config = Network.newConfig(30, "https://api.example.com")
println("Timeout: ${config.timeout}")
// Async with callback
Network.fetchAsync("https://example.com", object : Network.Callback {
override fun onSuccess(data: String) {
println("Success: $data")
}
override fun onError(err: Exception) {
println("Error: ${err.message}")
}
})
}
}
Type Conversion Reference
| Go Type | Java/Kotlin Type | Notes |
|---|---|---|
bool |
boolean / Boolean |
|
int, int32 |
int / Int |
32-bit signed |
int64 |
long / Long |
64-bit signed |
float64 |
double / Double |
|
string |
String |
UTF-8 encoded |
[]byte |
byte[] / ByteArray |
|
error |
Exception |
Go errors become Java exceptions |
struct |
class |
Exported fields become getters/setters |
interface |
interface |
Must have exported methods only |
Optimization Tips
Minimize boundary crossings:
Bad (many crossings):
func ProcessItem(item string) string { ... }
// Called 1000 times from Android = 1000 Go calls
Good (single crossing):
func ProcessItems(items []string) []string { ... }
// Called once from Android = 1 Go call
Use appropriate architectures:
For development (faster builds):
gomobile bind -target=android/arm64 -o mylib.aar ./golib
For release (wider compatibility):
gomobile bind -target=android/arm64,android/arm -o mylib.aar ./golib
Build Automation Script
#!/usr/bin/env bash
set -euo pipefail
echo "Building AAR..."
TARGETS=${ANDROID_TARGETS:-"android/arm64,android/arm"}
OUTPUT=${AAR_OUTPUT:-"mylib.aar"}
PACKAGE=${GO_PACKAGE:-"./golib"}
gomobile bind \
-target="$TARGETS" \
-o "$OUTPUT" \
"$PACKAGE"
echo "AAR built: $OUTPUT"
# Copy to Android project if path is set
if [ -n "${ANDROID_PROJECT:-}" ]; then
cp "$OUTPUT" "$ANDROID_PROJECT/app/libs/"
echo "Copied to Android project"
fi
Usage:
# Build for ARM64 only
ANDROID_TARGETS=android/arm64 ./build-aar.sh
# Build and copy to Android project
ANDROID_PROJECT=/path/to/android-app ./build-aar.sh
Building Standalone Go Apps (gomobile build)
Build standalone Android applications entirely in Go using gomobile build. Creates a complete APK with Go code as the main application logic.
When to Use vs gomobile bind
| Feature | gomobile build | gomobile bind |
|---|---|---|
| UI | OpenGL only | Full Android UI |
| Android APIs | Limited | Full access |
| Development speed | Fast | Medium |
| Production apps | Demos/utilities | Production ready |
| Learning curve | Low | Medium |
Use gomobile build when:
- You want to write the entire app in Go
- You’re building a simple utility or demo
- You want to prototype quickly
- You’re comfortable with limited UI
Use gomobile bind when:
- You need complex native Android UI
- You need full Android framework APIs
- You’re building a production app with rich UI
Basic App Example
package main
import (
"log"
"golang.org/x/mobile/app"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/gl"
)
func main() {
app.Main(func(a app.App) {
var glctx gl.Context
for e := range a.Events() {
switch e := a.Filter(e).(type) {
case lifecycle.Event:
switch e.Crosses(lifecycle.StageVisible) {
case lifecycle.CrossOn:
glctx, _ = e.DrawContext.(gl.Context)
log.Println("App started")
case lifecycle.CrossOff:
log.Println("App stopped")
glctx = nil
}
case paint.Event:
if glctx == nil {
continue
}
// Clear screen
glctx.ClearColor(0.2, 0.2, 0.3, 1.0)
glctx.Clear(gl.COLOR_BUFFER_BIT)
a.Publish()
}
}
})
}
Build APK
Basic build:
gomobile build -target=android .
Build with custom app ID:
gomobile build -target=android -appid=com.example.myapp .
Build for specific architectures:
# ARM64 only
gomobile build -target=android/arm64 -appid=com.example.myapp .
# ARM64 and ARM32
gomobile build -target=android/arm64,android/arm -appid=com.example.myapp .
Build with custom icon:
# Place icon at assets/icon.png
gomobile build -target=android -icon=assets/icon.png .
Install and Run
# Build and install
gomobile install -target=android -appid=com.example.myapp .
# Or build then install separately
gomobile build -target=android -appid=com.example.myapp .
adb install -r myapp.apk
View logs:
adb logcat | grep GoLog
Limitations
UI Limitations:
- ❌ No native Android UI widgets
- ❌ No Material Design components
- ❌ Limited text rendering
- ✅ OpenGL ES 2.0 for custom graphics
- ✅ Touch event handling
Platform Limitations:
- ❌ No direct access to Android framework APIs
- ❌ Can’t use Java/Kotlin libraries
- ✅ Network access (HTTP, TCP, UDP)
- ✅ File I/O
- ✅ Concurrency with goroutines
Building Android Apps
Build Debug
# Build debug APK
./gradlew assembleDebug
# Output location
ls app/build/outputs/apk/debug/app-debug.apk
# Install on device
adb install app/build/outputs/apk/debug/app-debug.apk
# Or use Gradle install task
./gradlew installDebug
Build Release
# Build signed release APK
./gradlew assembleRelease
# Output
ls app/build/outputs/apk/release/app-release.apk
# Build AAB for Play Store
./gradlew bundleRelease
# Output
ls app/build/outputs/bundle/release/app-release.aab
Build with Gomobile AAR
If using gomobile bind:
# 1. Build AAR from Go code
cd golib
gomobile bind -target=android -o mylib.aar .
# 2. Copy to Android project
cp mylib.aar /path/to/android-project/app/libs/
# 3. Build Android app
cd /path/to/android-project
./gradlew assembleDebug
Automate in script:
#!/usr/bin/env bash
set -euo pipefail
echo "Building Go library..."
cd golib
gomobile bind -target=android -o mylib.aar .
echo "Copying AAR to Android project..."
cp mylib.aar ../app/libs/
echo "Building Android app..."
cd ..
./gradlew assembleDebug
echo "Done! APK: app/build/outputs/apk/debug/app-debug.apk"
Build Optimization
Enable build cache (gradle.properties):
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx4096m
Optimize build time:
android {
// Use only needed ABIs during development
splits {
abi {
isEnable = false
}
}
// Disable PNG crunching in debug
buildTypes {
debug {
isCrunchPngs = false
}
}
}
Incremental builds:
# Good: Incremental build
./gradlew assembleDebug
# Bad: Full rebuild (slower)
./gradlew clean assembleDebug
Signing and Publishing
Create Signing Key
Generate keystore:
keytool -genkey -v \
-keystore release.keystore \
-alias myapp-key \
-keyalg RSA \
-keysize 2048 \
-validity 10000
Important:
- NEVER commit keystore to git
- Backup keystore securely
- Remember passwords
View keystore details:
keytool -list -v -keystore release.keystore
Configure Signing in Gradle
Using environment variables (recommended):
app/build.gradle.kts:
android {
signingConfigs {
create("release") {
storeFile = file("../release.keystore")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = "myapp-key"
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
Build with environment variables:
export KEYSTORE_PASSWORD="your-keystore-password"
export KEY_PASSWORD="your-key-password"
./gradlew bundleRelease
ProGuard / R8
Enable code shrinking:
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
proguard-rules.pro:
# Keep gomobile generated classes
-keep class go.** { *; }
-keep class mylib.** { *; }
# Keep model classes
-keep class com.example.myapp.models.** { *; }
# Kotlin
-keep class kotlin.** { *; }
-keep class kotlin.Metadata { *; }
Build Release AAB
./gradlew bundleRelease
# Output location
ls app/build/outputs/bundle/release/app-release.aab
Verify Signature
# Check AAB signature
jarsigner -verify -verbose app-release.aab
# Check APK signature
apksigner verify --verbose app-release.apk
Test Release Build
Generate APKs from AAB (using bundletool):
# Download bundletool from:
# https://github.com/google/bundletool/releases
# Generate APKs
java -jar bundletool-all.jar build-apks \
--bundle=app-release.aab \
--output=app.apks \
--mode=universal
# Extract universal APK
unzip app.apks universal.apk
# Install
adb install universal.apk
Test checklist:
- App installs successfully
- App launches without crashes
- All features work correctly
- ProGuard hasn’t broken anything
- Gomobile integration works
- Network requests succeed
Versioning
Version code (monotonically increasing):
android {
defaultConfig {
versionCode = 1 // Increment for each release
versionName = "1.0"
}
}
Automated versioning from git:
fun getVersionCode(): Int {
val process = Runtime.getRuntime().exec("git rev-list --count HEAD")
return process.inputStream.bufferedReader().readText().trim().toInt()
}
fun getVersionName(): String {
val process = Runtime.getRuntime().exec("git describe --tags --always")
return process.inputStream.bufferedReader().readText().trim()
}
android {
defaultConfig {
versionCode = getVersionCode()
versionName = getVersionName()
}
}
Upload to Play Console
Prerequisites:
- Google Play Developer account ($25 one-time)
- App created in Play Console
- Store listing completed
Upload AAB:
- Go to Play Console
- Select your app
- Production → Releases
- Create new release
- Upload AAB file
- Fill release notes
- Review and rollout
Release tracks:
| Track | Purpose | Audience |
|---|---|---|
| Internal testing | Quick testing | Up to 100 testers |
| Closed testing | Alpha/beta | Invited testers |
| Open testing | Public beta | Anyone can join |
| Production | Public release | All users |
Staged rollout (recommended):
- Start with 5-10%
- Monitor crashes/issues
- Increase to 25%, 50%, 100%
Store Listing Requirements
Required assets:
- Screenshots: Minimum 2, recommended 4-8
- Icon: 512x512 PNG
- Feature graphic: 1024x500 PNG
Required information:
- App name
- Short description (80 characters)
- Full description (4000 characters)
- Category
- Content rating
- Privacy policy URL (if app collects data)
- Contact email
CI/CD Publishing Example
.github/workflows/release.yml:
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Build gomobile AAR
run: |
go install golang.org/x/mobile/cmd/gomobile@latest
gomobile init
cd golib
gomobile bind -target=android -o mylib.aar .
cp mylib.aar ../app/libs/
- name: Build Release AAB
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > release.keystore
./gradlew bundleRelease
- name: Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: production
Troubleshooting
Build Issues
SDK not found:
# Create local.properties
echo "sdk.dir=$ANDROID_HOME" > local.properties
OutOfMemoryError during build:
# Increase Gradle memory in gradle.properties
org.gradle.jvmargs=-Xmx8192m -XX:MaxMetaspaceSize=2048m
Duplicate class errors:
android {
packagingOptions {
resources {
excludes += "META-INF/*.kotlin_module"
}
}
}
Gomobile Issues
“gomobile: command not found”:
# Ensure Go bin is in PATH
export PATH=$PATH:$(go env GOPATH)/bin
# Reinstall gomobile
go install golang.org/x/mobile/cmd/gomobile@latest
“NDK not found”:
# Set ANDROID_HOME
export ANDROID_HOME=$HOME/Android/Sdk
# Install NDK
sdkmanager "ndk;26.1.10909125"
# Reinitialize gomobile
gomobile init
“Cannot use map in function signature”:
// Bad - maps don't bind
func BadFunc(data map[string]string) error { ... }
// Good - use struct instead
type KeyValue struct {
Key string
Value string
}
func GoodFunc(data []KeyValue) error { ... }
AAR is very large:
# Build only needed architectures
gomobile bind -target=android/arm64 -o mylib.aar ./golib
Dependency Issues
Dependency conflicts:
# View conflict resolution
./gradlew app:dependencies
# Force specific version
dependencies {
implementation("com.example:library:1.0") {
force = true
}
}
# Exclude transitive dependency
dependencies {
implementation("com.example:library:1.0") {
exclude(group = "com.unwanted", module = "module")
}
}
Sync issues:
# Refresh dependencies
./gradlew --refresh-dependencies
# Clear cache
./gradlew clean cleanBuildCache
Signing Issues
“App not signed” error:
# Check signing configuration
./gradlew :app:signingReport
# Verify keystore exists and credentials are correct
ProGuard breaks app:
# Add keep rules in proguard-rules.pro
-keep class com.your.package.** { *; }
Upload rejected by Play Console:
- Version code not incremented
- Signature mismatch
- Missing required permissions
- Policy violations
Next Steps
- Debug and test - Use the Debug workflow
- Monitor app performance - Play Console metrics
- Respond to user feedback - Reviews and ratings
- Plan updates - New features and bug fixes