Android Development¶
Complete guide for building C++ libraries for Android with CCGO.
Overview¶
CCGO provides comprehensive Android support with:
- Multi-architecture builds: arm64-v8a, armeabi-v7a, x86, x86_64
- AAR packaging: Ready-to-use Android Archive format
- Gradle integration: Seamless integration with Android Studio projects
- JNI support: Automatic JNI wrapper generation
- Maven publishing: Publish to Maven Local, Central, or private repositories
- Docker builds: Build without local Android SDK/NDK installation
Prerequisites¶
Option 1: Local Development¶
Required: - Android SDK (API level 21+) - Android NDK (r21+, recommended: r25+) - CMake (3.18+) - Python (3.8+)
Installation:
# Install Android Studio (includes SDK)
# Download from https://developer.android.com/studio
# Set environment variables
export ANDROID_HOME=$HOME/Android/Sdk
export ANDROID_NDK=$ANDROID_HOME/ndk/25.2.9519653
# Verify installation
ccgo check android --verbose
Option 2: Docker-Based Development¶
Required: - Docker Desktop
First build downloads prebuilt image (~3.5GB, 5-10 minutes). Subsequent builds use cached image.
Quick Start¶
Create New Project¶
# Create new Android-compatible project
ccgo new my-android-lib
cd my-android-lib/my-android-lib
# Build for Android
ccgo build android
Build Single Architecture¶
Build Multiple Architectures¶
# Build for arm64-v8a and armeabi-v7a
ccgo build android --arch arm64-v8a,armeabi-v7a
# Build all architectures (default)
ccgo build android
Build Options¶
# Release build (optimized)
ccgo build android --release
# Debug build (with symbols)
ccgo build android --debug
# Clean build
ccgo build android --clean
# Docker build
ccgo build android --docker
# Link type control
ccgo build android --build-as static # Static library only
ccgo build android --build-as shared # Shared library only
ccgo build android --build-as both # Both (default)
Output Structure¶
After building, find artifacts in target/android/:
target/android/
├── MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip # Main package
├── MY-ANDROID-LIB_ANDROID_SDK-1.0.0-SYMBOLS.zip # Debug symbols
└── build_info.json # Build metadata
Main Package Structure¶
MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip
├── lib/
│ ├── static/
│ │ ├── armeabi-v7a/
│ │ │ └── libmy-android-lib.a
│ │ ├── arm64-v8a/
│ │ │ └── libmy-android-lib.a
│ │ ├── x86/
│ │ │ └── libmy-android-lib.a
│ │ └── x86_64/
│ │ └── libmy-android-lib.a
│ └── shared/
│ ├── armeabi-v7a/
│ │ └── libmy-android-lib.so
│ ├── arm64-v8a/
│ │ └── libmy-android-lib.so
│ ├── x86/
│ │ └── libmy-android-lib.so
│ └── x86_64/
│ └── libmy-android-lib.so
├── haars/
│ └── my-android-lib-release.aar
├── include/
│ └── my-android-lib/
│ ├── my-android-lib.h
│ └── version.h
└── build_info.json
Symbols Package Structure¶
MY-ANDROID-LIB_ANDROID_SDK-1.0.0-SYMBOLS.zip
└── obj/
├── armeabi-v7a/
│ └── libmy-android-lib.so # Unstripped with debug symbols
├── arm64-v8a/
│ └── libmy-android-lib.so
├── x86/
│ └── libmy-android-lib.so
└── x86_64/
└── libmy-android-lib.so
Configuration¶
CCGO.toml¶
Configure Android-specific settings:
[package]
name = "my-android-lib"
version = "1.0.0"
[library]
type = "both" # Build both static and shared libraries
[android]
min_sdk_version = 21 # Android 5.0 (Lollipop)
target_sdk_version = 33 # Android 13
ndk_version = "25.2.9519653" # Specific NDK version
stl = "c++_static" # STL type: c++_static or c++_shared
architectures = ["arm64-v8a", "armeabi-v7a", "x86_64"] # Optional: limit architectures
[build]
cpp_standard = "17"
compile_flags = ["-Wall", "-Wextra"]
Android Configuration Options¶
| Option | Type | Description | Default |
|---|---|---|---|
min_sdk_version |
integer | Minimum Android API level | 21 |
target_sdk_version |
integer | Target Android API level | 33 |
ndk_version |
string | Specific NDK version | Latest installed |
stl |
string | STL type: c++_static, c++_shared |
c++_static |
architectures |
array | Target ABIs to build | All supported |
Supported Architectures¶
| ABI | Architecture | Description |
|---|---|---|
arm64-v8a |
ARM 64-bit | Modern Android devices (recommended) |
armeabi-v7a |
ARM 32-bit | Legacy Android devices |
x86_64 |
Intel 64-bit | Emulators, tablets, Chrome OS |
x86 |
Intel 32-bit | Legacy emulators |
Recommendation: Build arm64-v8a and armeabi-v7a for production apps.
AAR Integration¶
Using AAR in Android Project¶
1. Copy AAR to project:
# Copy AAR from build output
cp target/android/MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip .
unzip MY-ANDROID-LIB_ANDROID_SDK-1.0.0.zip
cp haars/my-android-lib-release.aar android-app/libs/
2. Configure app/build.gradle.kts:
android {
// ...
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
// or
implementation(files("libs/my-android-lib-release.aar"))
}
3. Use in Java/Kotlin:
class MainActivity : AppCompatActivity() {
companion object {
init {
System.loadLibrary("my-android-lib")
}
}
// Declare native methods
external fun nativeMethod(): String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Call native method
val result = nativeMethod()
Log.d("Native", "Result: $result")
}
}
JNI Integration¶
Automatic JNI Wrapper¶
CCGO can generate JNI wrappers automatically:
C++ Header (include/my-android-lib/my-android-lib.h):
#pragma once
#include <string>
namespace my_android_lib {
class MyLib {
public:
static std::string get_version();
static int calculate(int a, int b);
};
} // namespace my_android_lib
Generated JNI Wrapper (auto-generated):
#include <jni.h>
#include "my-android-lib/my-android-lib.h"
extern "C" {
JNIEXPORT jstring JNICALL
Java_com_example_mylib_MyLib_getVersion(JNIEnv* env, jclass) {
std::string version = my_android_lib::MyLib::get_version();
return env->NewStringUTF(version.c_str());
}
JNIEXPORT jint JNICALL
Java_com_example_mylib_MyLib_calculate(JNIEnv*, jclass, jint a, jint b) {
return my_android_lib::MyLib::calculate(a, b);
}
} // extern "C"
Manual JNI Implementation¶
Create src/jni/my_jni.cpp:
#include <jni.h>
#include <string>
#include "my-android-lib/my-android-lib.h"
extern "C" {
JNIEXPORT jstring JNICALL
Java_com_example_MyNativeLib_stringFromJNI(JNIEnv* env, jobject /* this */) {
std::string hello = my_android_lib::MyLib::get_version();
return env->NewStringUTF(hello.c_str());
}
JNIEXPORT jint JNICALL
Java_com_example_MyNativeLib_add(JNIEnv*, jobject, jint a, jint b) {
return my_android_lib::MyLib::calculate(a, b);
}
} // extern "C"
Java/Kotlin side:
package com.example
class MyNativeLib {
external fun stringFromJNI(): String
external fun add(a: Int, b: Int): Int
companion object {
init {
System.loadLibrary("my-android-lib")
}
}
}
Gradle Integration¶
Using CCGO Gradle Plugins¶
CCGO provides Gradle convention plugins for standardized Android builds.
settings.gradle.kts:
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
app/build.gradle.kts:
plugins {
id("com.android.library")
id("com.mojeter.ccgo.gradle.android.library")
id("com.mojeter.ccgo.gradle.android.library.native")
}
android {
namespace = "com.example.mylib"
compileSdk = 33
defaultConfig {
minSdk = 21
}
ndkVersion = "25.2.9519653"
}
ccgoNative {
projectPath.set(file("../")) // Path to CCGO project root
buildType.set("release") // or "debug"
architectures.set(listOf("arm64-v8a", "armeabi-v7a"))
}
Publishing¶
Maven Local (Development)¶
# Publish to Maven Local for testing
ccgo publish android --registry local
# Location: ~/.m2/repository/com/example/my-android-lib/1.0.0/
Maven Central (Production)¶
1. Configure credentials:
Create ~/.gradle/gradle.properties:
mavenCentralUsername=your-username
mavenCentralPassword=your-password
signing.keyId=12345678
signing.password=your-key-password
signing.secretKeyRingFile=/Users/you/.gnupg/secring.gpg
2. Publish:
Private Maven Repository¶
Using Published Library¶
app/build.gradle.kts:
Advanced Topics¶
Multi-Module Projects¶
Project structure:
my-project/
├── CCGO.toml
├── lib1/
│ ├── CCGO.toml
│ └── src/
└── lib2/
├── CCGO.toml (depends on lib1)
└── src/
lib2/CCGO.toml:
Custom CMake Configuration¶
CMakeLists.txt:
cmake_minimum_required(VERSION 3.18)
# CCGO automatically provides:
# - ${CCGO_CMAKE_DIR}: Path to CCGO cmake utilities
# - ${ANDROID_ABI}: Current architecture being built
# - ${ANDROID_PLATFORM}: Android API level
include(${CCGO_CMAKE_DIR}/CMakeUtils.cmake)
# Custom Android-specific configuration
if(ANDROID)
# Add Android-specific compiler flags
add_compile_options(-fPIC)
# Link against Android libraries
find_library(LOG_LIB log)
find_library(ANDROID_LIB android)
target_link_libraries(${PROJECT_NAME}
${LOG_LIB}
${ANDROID_LIB}
)
endif()
Proguard Rules¶
Create proguard-rules.pro:
# Keep native methods
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep JNI exported methods
-keep class com.example.mylib.** { *; }
App Size Optimization¶
1. Strip unneeded symbols (automatic in release builds):
2. Use only required architectures:
3. Enable link-time optimization:
4. Split APKs by ABI:
app/build.gradle.kts:
android {
splits {
abi {
isEnable = true
reset()
include("arm64-v8a", "armeabi-v7a")
isUniversalApk = false
}
}
}
Troubleshooting¶
Common Issues¶
NDK Not Found¶
Solution:
# Install NDK via Android Studio: Tools → SDK Manager → SDK Tools → NDK
# Or set manually
export ANDROID_NDK=$ANDROID_HOME/ndk/25.2.9519653
# Verify
ccgo check android --verbose
Architecture Mismatch¶
Solution:
Ensure AAR contains the architecture your device/emulator uses:
# Check AAR contents
unzip -l my-android-lib-release.aar | grep "\.so$"
# Rebuild with correct architecture
ccgo build android --arch arm64-v8a
C++ Standard Mismatch¶
Solution:
Ensure consistent C++ standard across dependencies:
Missing Symbols¶
Solution:
Check that all source files are compiled:
Docker Build Issues¶
Docker Not Running¶
Solution:
Image Pull Failure¶
Solution:
# Retry with manual pull
docker pull ccgogroup/ccgo-builder-android:latest
# Or use local build
cd ccgo/dockers/
docker build -t ccgo-builder-android -f Dockerfile.android .
Performance Issues¶
Slow First Build¶
Normal: First build compiles all dependencies (~10-30 minutes).
Optimization:
# Use prebuilt dependencies (future feature)
ccgo install --prebuilt
# Enable ccache
export USE_CCACHE=1
export CCACHE_DIR=$HOME/.ccache
Incremental Build Not Working¶
Best Practices¶
1. Version Management¶
2. Architecture Selection¶
[android]
# Production: arm64-v8a + armeabi-v7a (covers 99%+ devices)
architectures = ["arm64-v8a", "armeabi-v7a"]
# Development: arm64-v8a only (faster builds)
# architectures = ["arm64-v8a"]
3. STL Choice¶
[android]
# Prefer c++_static (no runtime dependency)
stl = "c++_static"
# Use c++_shared only if multiple native libraries share STL
# stl = "c++_shared"
4. Dependency Management¶
[dependencies]
# Pin to specific versions for reproducibility
spdlog = { git = "https://github.com/gabime/spdlog.git", tag = "v1.12.0" }
# Use CCGO.lock for exact dependency resolution
5. Testing¶
# Build and test locally
ccgo build android --arch arm64-v8a
ccgo test android
# Test AAR in sample app before publishing
6. CI/CD¶
# .github/workflows/android.yml
name: Android Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Android
run: |
pip install ccgo
ccgo build android --docker
- name: Test
run: ccgo test android --docker
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: android-libs
path: target/android/*.zip
Examples¶
Complete Project¶
See ccgo-now for a complete Android project example.
Minimal Example¶
// include/mylib/mylib.h
#pragma once
#include <string>
namespace mylib {
std::string get_greeting();
int add(int a, int b);
}
// src/mylib.cpp
#include "mylib/mylib.h"
namespace mylib {
std::string get_greeting() {
return "Hello from C++!";
}
int add(int a, int b) {
return a + b;
}
}
Build:
Use in Android:
class MainActivity : AppCompatActivity() {
init {
System.loadLibrary("mylib")
}
external fun getGreeting(): String
external fun add(a: Int, b: Int): Int
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val greeting = getGreeting()
val result = add(2, 3)
Log.d("MyLib", "$greeting, 2+3=$result")
}
}