The first AAR you build often feels like magic: one Gradle task turns a module into a single file. The surprises arrive in the consuming app—resource names collide, a manifest permission appears, R8 removes a reflective class, or a transitive library vanishes. This walkthrough starts with the module, but treats the consumer experience as the real deliverable.
What belongs in an AAR
Compiled Kotlin/Java classes in
classes.jar.An Android manifest merged into the consuming application.
Resources, assets, public resource declarations, and symbol metadata.
Optional JARs, native libraries organized by ABI, lint checks, Prefab packages, and consumer shrinker rules.
The only mandatory archive entry is
AndroidManifest.xml; actual contents depend on the library.
AAR versus JAR
Choose an AAR when the library uses Android resources, manifest components, assets, Android APIs, native packaging, or Android-specific build metadata.
Choose a Kotlin/JVM or Java JAR when the code is platform-independent; this improves reuse and often speeds tests/builds.
Neither format alone is a dependency catalog. Maven/Gradle metadata carries coordinates, versions, variants, and transitive relationships.
An AAR cannot be installed or launched like an APK.
1. Register the library module
include(":app", ":mylibrary")The module path becomes its build identity
Android Studio’s New Module wizard normally adds this entry automatically.
The command-line build depends on settings, not on IDE project files.
The default physical directory is
mylibrary/at the project root.Use stable, lowercase, descriptive module paths and avoid cyclic relationships.
2. Configure a releaseable Android library
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.example.mylibrary"
compileSdk = 35
defaultConfig {
minSdk = 23
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
}
}
}
dependencies {
implementation("androidx.annotation:annotation:<approved-version>")
}Every setting becomes a promise to consumers
com.android.libraryproduces AAR variants instead of an installable APK.namespaceowns generated resources/classes and must be globally unambiguous in the build.minSdkcontributes to the final app’s minimum platform constraint.compileSdkselects Android APIs available at compile time; use the project’s approved current value.consumerProguardFilespackages rules that the consuming app’s R8 run applies.<approved-version>is a placeholder: pin a reviewed dependency version or use the project version catalog.
3. Design the smallest useful public API
package com.example.mylibrary
class GreetingFormatter {
fun format(name: String): String {
require(name.isNotBlank()) { "name must not be blank" }
return "Hello, ${name.trim()}"
}
}A library API needs behavior contracts
The public class and method are visible to consumers.
requirefails fast withIllegalArgumentExceptionfor invalid caller input.The method is deterministic and framework-free, so it can be unit tested quickly.
Document threading, nullability, exceptions, lifecycle, data/privacy, and backward compatibility for real APIs.
Keep implementation classes
internalor private so future refactoring does not break clients.
4. Keep Android resources collision-resistant
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="lynxgreeting_default_name">Developer</string>
</resources>Resources share the final app namespace
A distinctive prefix reduces collisions with the app and other AARs.
The app can override a resource with the same name during merge, so do not treat resource values as tamper-proof configuration.
Place private implementation resources outside the documented public resource surface.
Use
public.xmldeliberately when consumers are expected to reference selected resources.Never put secrets or credentials in resources; APK contents are inspectable.
5. Keep the manifest quiet
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />Every manifest entry enters the host build
An empty manifest is valid when the library declares no Android components or permissions.
Add activities, providers, services, receivers, permissions, features, and queries only when the library truly requires them.
Use manifest placeholders for consumer-specific authorities or values and document each required placeholder.
Review exported state and intent filters as security-sensitive public surface.
Test the merged manifest in a sample app rather than inspecting only the library source.
6. Add consumer R8 rules only where justified
# Keep only APIs discovered dynamically by documented reflection/JNI.
# Example placeholder—replace with a precise rule when your library needs one.
# -keep class com.example.mylibrary.reflect.ModelAdapter { *; }Consumer rules run in someone else’s release build
Commented examples do not change shrinking behavior.
Prefer annotations, generated adapters, or direct references over broad keep rules.
Never ship
-keep class com.example.mylibrary.** { *; }without evidence; it blocks optimization and hides missing-rule design problems.Test a minified consumer app and inspect R8 diagnostics/mapping where permitted.
Library minification and consumer-app minification solve different problems.
7. Unit-test the public behavior
package com.example.mylibrary
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class GreetingFormatterTest {
private val formatter = GreetingFormatter()
@Test fun trimsAndFormatsName() {
assertEquals("Hello, Ada", formatter.format(" Ada "))
}
@Test fun rejectsBlankName() {
assertFailsWith<IllegalArgumentException> { formatter.format(" ") }
}
}Test the contract, not private lines
The success test records formatting behavior.
The failure test records the exception type for invalid input.
Local tests run on the JVM and suit framework-free logic.
Use instrumented tests or Robolectric only when Android runtime behavior requires them.
A published library also needs a consumer/sample app test to catch manifest, resources, R8, and packaging issues.
8. Build the release AAR reproducibly
./gradlew :mylibrary:testDebugUnitTest :mylibrary:lintRelease :mylibrary:assembleReleaseExpect BUILD SUCCESSFUL and an AAR such as mylibrary/build/outputs/aar/mylibrary-release.aar.Use the wrapper and qualify the module
The Gradle wrapper fixes the project’s Gradle distribution for developer and CI builds.
The task path avoids building unrelated modules.
Unit tests and release lint run before packaging.
assembleReleasecreates the release variant but does not publish it to a repository.Run from a clean checkout in CI and archive test/lint reports with the artifact.
9. Inspect what you are about to ship
unzip -l mylibrary/build/outputs/aar/mylibrary-release.aar
sha256sum mylibrary/build/outputs/aar/mylibrary-release.aarReview the archive entries, then record the SHA-256 digest alongside the release artifact and provenance.The binary is the final truth
Listing the ZIP does not execute the artifact.
Confirm expected manifest, classes, resources, consumer rules, lint checks, assets, and native ABIs.
Look for accidental debug files, secrets, internal assets, or bundled dependencies.
A checksum detects byte changes but does not establish trust unless delivered through an authenticated channel.
Reproducible-build goals require controlling toolchains, inputs, timestamps, and publication steps beyond this checksum.
10. Test it from a consumer app
dependencies {
implementation(project(":mylibrary"))
}Source-module consumption shortens feedback
A project dependency keeps source changes and app integration in one build graph.
implementationavoids leaking library dependencies onto unrelated compile classpaths.Build debug and minified release variants of the app.
Exercise APIs, resources, manifest components, configuration changes, process recreation, and supported API levels.
Before external release, repeat from a separate project using the published coordinate.
Dependencies: implementation or api
Use
implementationwhen a dependency remains behind your library’s public surface.Use
apionly when dependency types intentionally appear in public parameters, return types, supertypes, properties, or generic bounds.apiincreases consumer compile visibility and upstream recompilation.Avoid local loose AAR dependencies in a published library workflow; normal AAR output does not simply fuse arbitrary AARs together.
Publish dependency metadata so consumers receive required artifacts and version relationships.
Publish metadata, not just a file
For use outside the source build, publish through a Maven-compatible repository with a group, artifact, version, AAR, POM, and Gradle Module Metadata as appropriate. Metadata allows Gradle to resolve transitive dependencies and variants; sending only an AAR makes every consumer manually reconstruct that knowledge.
Adopt semantic versioning only with a documented compatibility policy.
Generate API references and release notes from the public surface.
Sign artifacts when your repository/supply-chain policy requires it.
Test the published coordinate from an empty consumer project.
Do not publish snapshots under immutable release versions.
Frequent failures
No AAR output: the module applies the application plugin or the wrong variant/task was requested.
Manifest merger failed: the library introduces conflicting components, SDK requirements, authorities, or attributes.
Resource linking failed: names collide, a resource dependency is hidden/missing, or compile SDK/build tools are incompatible.
Works in debug, crashes in release: reflection/JNI/serialization needs precise consumer rules or a dependency is absent.
Consumer cannot resolve a class: the AAR was copied without its transitive dependency metadata.
Duplicate classes: a dependency is embedded and also resolved externally, or multiple artifacts supply the same bytecode.
Native crash on one device: the required ABI/shared object is absent or its dependencies cannot load.
App minSdk is forced upward: the library or one dependency declares a higher platform floor.
Release checklist
Unique namespace, resource prefix, documented public API, and compatibility policy are reviewed.
Manifest permissions/components/placeholders and data/privacy behavior are documented.
Dependencies use intentional implementation/api scopes and publish complete metadata.
Unit, lint, sample app, minified release, API-level, ABI, accessibility, and upgrade tests pass.
Consumer rules are precise and no credentials/internal artifacts enter the AAR.
Version, changelog, license, SBOM/provenance, checksum/signature, owner, vulnerability response, and rollback path are recorded.
Official references
Create an Android library documents module creation, AAR anatomy, resources, manifests, consumer rules, and dependency consumption.
Prepare your library for release covers namespace, SDK constraints, AAR metadata, APIs, testing, and release preparation.
Upload your library explains Maven publication and dependency metadata.
Add build dependencies defines Android dependency configurations and verification guidance.
Comments and corrections