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

settings.gradle.ktskotlin
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

mylibrary/build.gradle.ktskotlin
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.library produces AAR variants instead of an installable APK.

  • namespace owns generated resources/classes and must be globally unambiguous in the build.

  • minSdk contributes to the final app’s minimum platform constraint.

  • compileSdk selects Android APIs available at compile time; use the project’s approved current value.

  • consumerProguardFiles packages 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

mylibrary/src/main/kotlin/com/example/mylibrary/GreetingFormatter.ktkotlin
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.

  • require fails fast with IllegalArgumentException for 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 internal or private so future refactoring does not break clients.

4. Keep Android resources collision-resistant

mylibrary/src/main/res/values/strings.xmlxml
<?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.xml deliberately when consumers are expected to reference selected resources.

  • Never put secrets or credentials in resources; APK contents are inspectable.

5. Keep the manifest quiet

mylibrary/src/main/AndroidManifest.xmlxml
<?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

mylibrary/consumer-rules.proproguard
# 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

mylibrary/src/test/kotlin/com/example/mylibrary/GreetingFormatterTest.ktkotlin
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

Android project rootbash
./gradlew :mylibrary:testDebugUnitTest :mylibrary:lintRelease :mylibrary:assembleRelease
Expect 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.

  • assembleRelease creates 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

Android project rootbash
unzip -l mylibrary/build/outputs/aar/mylibrary-release.aar
sha256sum mylibrary/build/outputs/aar/mylibrary-release.aar
Review 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

app/build.gradle.kts (same build)kotlin
dependencies {
    implementation(project(":mylibrary"))
}

Source-module consumption shortens feedback

  • A project dependency keeps source changes and app integration in one build graph.

  • implementation avoids 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 implementation when a dependency remains behind your library’s public surface.

  • Use api only when dependency types intentionally appear in public parameters, return types, supertypes, properties, or generic bounds.

  • api increases 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