flake-update-20260201

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 devices
  • android/arm: 32-bit ARM (armeabi-v7a) - older devices
  • android/amd64: 64-bit x86 (x86_64) - emulators
  • android/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:

  1. Google Play Developer account ($25 one-time)
  2. App created in Play Console
  3. Store listing completed

Upload AAB:

  1. Go to Play Console
  2. Select your app
  3. Production → Releases
  4. Create new release
  5. Upload AAB file
  6. Fill release notes
  7. 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):

  1. Start with 5-10%
  2. Monitor crashes/issues
  3. 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

Resources