A project is an Android project when its elide.pkl declares android. Only namespace and compileSdk are
required; everything else has a default or is optional.
amends "elide:project.pkl"
import "elide:Android.pkl" as Android
name = "my-app"
android {
namespace = "com.example.app"
compileSdk = 36
}
artifacts {
["app"] = new Android.AndroidApk { release = false }
}
Settings
| Property | Type | Meaning |
|---|---|---|
namespace | package name | The package R lives in and relative class names resolve against; the default applicationId. Required. |
compileSdk | API level | The platform the app compiles, links and dexes against. Required. |
minSdk | API level | The oldest release the app runs on. Default compileSdk. |
targetSdk | API level | The release the app declares it was tested against. |
buildTools | "36.0.0" | The build-tools release; default resolved from the SDK (Android SDK). |
sdk | path | The SDK root, "managed" or "system" (Android SDK). |
application | object | The generated AndroidManifest.xml (below). |
resources | object | Generated value resources (below). |
device | object | The project’s emulator (Run and debug). |
devices | mapping | Named test devices (Android tests). |
hostTests | artifact name | The artifact host tests run against (Android tests). |
hilt | Boolean | Run Hilt’s bytecode transformation (Android dependencies). |
API levels
The build owns the API levels, and the merged manifest carries them:
| Level | Manifest omits it | Manifest declares another value |
|---|---|---|
minSdk | The build’s is written. | The build fails: the dex would not run where it installs. |
targetSdk | targetSdk, else compileSdk, is written. | The build fails when targetSdk is declared; otherwise the manifest’s stands. |
A library’s manifest records its minimum and never a target.
Namespace and application id
namespace is where R lives. applicationId, declared on an application artifact, is the package the app
installs and publishes as, and defaults to the namespace. A package attribute in an AndroidManifest.xml must
agree with the namespace, or the build fails.
Two artifacts built from the same sources with different applicationIds install side by side, so a debug build
can be installed beside a release build:
artifacts {
["app"] = new Android.AndroidApk {
versionCode = 3
versionName = "1.2.0"
}
["app-dev"] = new Android.AndroidApk {
applicationId = "com.example.app.dev"
release = false
resources { "src/main/res"; "src/debug/res" }
}
}
Kotlin defaults
A project declaring android defaults kotlin.compilerOptions.jvmTarget to 17, since the app runs against
android.jar on a device rather than on the JVM running the build. It also turns off the Kotlin features
kotlinx, defaultPlugins and reflection, whose runtimes would otherwise be dexed into the app. Declaring a
feature turns it back on:
kotlin {
features {
reflection = true
}
}
Generated manifest
android.application describes the application, and the build generates AndroidManifest.xml from it:
android {
namespace = "com.example.app"
compileSdk = 36
minSdk = 30
targetSdk = 36
application {
name = ".App"
label = "@string/app_name"
icon = "@mipmap/ic_launcher"
theme = "@android:style/Theme.Material.Light.NoActionBar"
allowBackup = false
supportsRtl = true
activities {
["MainActivity"] { launcher = true }
[".settings.SettingsActivity"] { label = "@string/settings"; exported = false }
}
services {
[".sync.SyncService"] { foregroundServiceType = "dataSync"; exported = false }
}
receivers {
[".BootReceiver"] {
exported = true
actions { "android.intent.action.BOOT_COMPLETED" }
}
}
providers {
[".data.FilesProvider"] {
authorities { "${applicationId}.files" }
exported = false
grantUriPermissions = true
}
}
permissions {
"android.permission.INTERNET"
"android.permission.RECEIVE_BOOT_COMPLETED"
}
features {
["android.hardware.camera"] { required = false }
}
}
}
| Pkl | Generates |
|---|---|
name, label, icon, roundIcon, theme, allowBackup, supportsRtl | <application> attributes of the same name |
activities | <activity>; launcher = true adds the MAIN/LAUNCHER intent filter and exports it |
services | <service>, with foregroundServiceType |
receivers | <receiver>; actions become its intent filter |
providers | <provider>, with authorities and grantUriPermissions |
permissions | <uses-permission> |
features | <uses-feature>, required by default |
Every component takes exported and permission; activities also take label and theme. Components are keyed
by class name, fully qualified or relative to the namespace (.MainActivity and MainActivity are the same
class). Android 12 and later refuse a component with an intent filter that does not declare exported; a launcher
activity is exported unless it says otherwise.
As with any manifest, the build adds the values it owns to the generated one: uses-sdk, versionCode, versionName,
applicationId, and android:debuggable for a debug build.
Merging with AndroidManifest.xml
src/main/AndroidManifest.xml is optional. When it exists it is merged over the generated document, so XML
overrides and extends Pkl and never the reverse:
- an attribute both declare takes the XML’s value, without a conflict;
- an element only the XML declares is added;
tools:node="remove",tools:node="replace"andtools:removein the XML act on what was generated.
Keep in XML what the model does not name: intent filters beyond the launcher, <meta-data>, <queries>, and
attributes such as android:networkSecurityConfig.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application android:networkSecurityConfig="@xml/network_security_config">
<activity android:name=".MainActivity" android:windowSoftInputMode="adjustResize" />
</application>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" tools:node="remove" />
</manifest>
Libraries’ manifests merge under the application’s with Android’s rules: the application wins, an earlier
declaration beats a later one, and two different values for one attribute are an error naming both files unless
tools:replace settles it. tools:node (merge, replace, remove, removeAll, strict,
mergeOnlyAttributes), tools:remove and tools:overrideLibrary are honored, and no tools: attribute reaches
the APK. A library asking for a higher minSdkVersion than the app is an error that tools:overrideLibrary waives.
Placeholders
${name} in any manifest is replaced from the artifact’s placeholders, plus applicationId. An unknown
placeholder fails the build. A library substitutes only its own and leaves the rest for the application.
["app"] = new Android.AndroidApk {
placeholders { ["appLabel"] = "My App" }
}
String resources
android.resources.strings generates <string> resources of the default configuration:
android {
namespace = "com.example.app"
compileSdk = 36
resources {
strings {
["app_name"] = "My App"
["greeting"] = "Don't panic"
}
}
}
Values are written as given; quotes, apostrophes, backslashes, newlines and a leading @ or ? are escaped for
aapt2. The generated resources are linked after any library and before the project’s own resource directories,
so a string the project’s res/values also declares resolves to the XML, and one a library declares resolves to
the project. Translations and other configurations (values-fr/, values-night/) stay in res/.
Code reads them as any resource: R.string.greeting, stringResource(R.string.greeting).
Project layout
| Path | Holds |
|---|---|
src/main/kotlin, src/main/java | Sources |
src/ | Optional; merged over the generated manifest |
src/main/res | Resources |
src/main/assets | Assets, packaged under assets/ |
src/ | Native libraries |
src/test | Host tests (Android tests) |
src/androidTest | Instrumented tests (Android tests) |
Declaring sources, resources, assets, jniLibs or manifestFile on an artifact replaces the convention for
that field alone. A declared directory or source set that does not exist fails the build.
Artifacts
All Android artifacts take:
| Property | Default | Meaning |
|---|---|---|
name | artifact name plus .apk, .aab or .aar | Published file name. |
sources | every non-test source set | Source sets whose classes it carries. |
manifestFile | src/ | Manifest merged over the generated one. |
resources | src/main/res | Resource directories. |
assets | src/main/assets | Asset directories. |
jniLibs | src/main/jniLibs | Native library directories. |
placeholders | empty | Manifest placeholder values. |
linkArgs | empty | Extra aapt2 link arguments. |
Applications (AndroidApk, AndroidAab) add:
| Property | Default | Meaning |
|---|---|---|
applicationId | namespace | Installed and published package. |
versionCode, versionName | none | Replace any the manifest declares. |
release | true | false is a debuggable debug build. |
signing | none | Keystore (Signing and release). |
minify | false | Shrink with R8 in place of d8. |
proguardFiles | empty | R8 rules, after the libraries’ own. |
abiFilters | empty: every ABI | ABIs whose native libraries ship. |
packaging.excludes | empty | Java resources left out (below). |
dexArgs | empty | Extra d8 or R8 arguments. |
AndroidApk adds instrumentation, which makes it a test APK (Android tests).
AndroidAab adds bundleArgs (extra bundletool build-bundle arguments) and features. AndroidAar adds
consumerProguardFiles.
Native libraries
Native libraries are laid out as <abi>/lib<name>.so under each jniLibs directory and packaged under
lib/<abi>/, stored uncompressed and aligned to 16 KB pages, which Android 15 and later require on 16 KB devices.
The .so files themselves must be linked for 16 KB pages (-z max-page-size=16384). Alignment needs build-tools
35.0.0-rc3 or later; with an older buildTools, the newest installed release that has it is used.
["app"] = new Android.AndroidApk {
abiFilters { "arm64-v8a"; "x86_64" }
}
Java resources
Non-class files in the jars an app depends on ship at the APK’s root, as the Android Gradle Plugin packages them.
META-INF/services files are concatenated; any other duplicated path ships from the first jar carrying it.
Signatures, jar manifests, META-INF/maven, META-INF/proguard, licence and notice files are left out by default.
packaging.excludes leaves out more, with glob patterns matched against the path with a leading /:
["app"] = new Android.AndroidApk {
packaging {
excludes { "/META-INF/{AL2.0,LGPL2.1}"; "**/*.kotlin_builtins" }
}
}
Libraries
An AndroidAar publishes an Android library:
artifacts {
["ui"] = new Android.AndroidAar {
consumerProguardFiles { "consumer-rules.pro" }
}
}
elide build ui writes .dev/artifacts/ui.aar with the library’s AndroidManifest.xml, classes.jar, its
resources as written (res/), R.txt, assets/, jni/<abi>/ and its consumerProguardFiles as proguard.txt.
Resources ship as written, not compiled, because an application links its own and its libraries’ resources in one
pass, which gives every identifier one numbering.
An application consumes a library of the same workspace through an ordinary project reference:
// elide.pkl (workspace root)
amends "elide:project.pkl"
name = "my-workspace"
workspace {
members {
"lib"
"app"
}
}
// app/elide.pkl
amends "elide:project.pkl"
import "elide:Android.pkl" as Android
local lib = project("lib")
name = "app"
android {
namespace = "com.example.app"
compileSdk = 36
}
dependencies {
maven {
packages { lib }
}
}
artifacts {
["app"] = new Android.AndroidApk { release = false }
}
The application reads the library’s classes, resources, manifest, assets and native libraries straight from its
build, never from its .aar: elide build app builds the library’s classes but not its archive. References are
followed transitively. Precedence is Android’s: the application beats its libraries, a nearer reference beats a
transitive one, and an earlier declaration beats a later one. A library’s string is overridden by declaring the same
name in the application.
When workspace members resolve one library at different versions, the newest ships, as Gradle’s conflict
resolution would pick it, and its resources and manifest are the ones linked, so code and R agree. Published
AARs are covered in Android dependencies.
App bundles
An AndroidAab builds the bundle a store splits into per-device APKs:
artifacts {
["bundle"] = new Android.AndroidAab {
versionCode = 3
versionName = "1.2.0"
abiFilters { "arm64-v8a" }
signing {
keystore = "../keys/upload.jks"
alias = "upload"
dname = "CN=Example, O=Example, C=US"
validity = 9125
passwordEnv = "UPLOAD_KEYSTORE_PASS"
}
}
}
elide build bundle writes .dev/artifacts/bundle.aab. A bundle is signed with the upload key when it declares
signing (Signing and release); the store re-signs the APKs it generates with the app
signing key. Nothing installs a bundle directly, so it has no run or debug target.
Dynamic feature modules
An AndroidFeature is a module of a bundle, delivered at install time or on demand. Its sources are a source set of
the app project, and it declares its own manifest, whose package is its R namespace:
amends "elide:project.pkl"
import "elide:Android.pkl" as Android
import "elide:Sources.pkl" as Sources
name = "my-app"
android {
namespace = "com.example.app"
compileSdk = 36
}
sources {
["main"] = new Sources.SourceSetSpec { paths { "src/main/kotlin/**/*.kt" } }
["detail"] = new Sources.SourceSetSpec {
paths { "src/detail/kotlin/**/*.kt" }
dependsOn { "main" }
}
}
artifacts {
["bundle"] = new Android.AndroidAab {
sources { "main" }
features { "detail" }
}
["detail"] = new Android.AndroidFeature {
base = "bundle"
sources { "detail" }
manifestFile = "src/detail/AndroidManifest.xml"
resources { "src/detail/res" }
delivery = "on-demand"
title = "@string/detail_title"
fusing = false
}
}
titlenames a string the base declares.deliveryis"on-demand"(default) or"install-time"; it andfusinggenerate thedist:moduleelement, so do not declare one in XML.- A feature takes no defaults from the layout: declare its
sources, and itsresources,assetsandjniLibswhen it has any. featureDependencies { "common" }declares a feature it depends on; its source set’sdependsOnlists both"main"and"common".- The base owns every Maven and workspace dependency; features dex only their own classes.
elide build android-module-detail builds the feature module alone. On-demand delivery in a shipped app goes
through Play Feature Delivery’s SplitInstallManager.