A vendor sends you one opaque .aar file and says, “drop this into the app.” That sounds simple until Gradle sync succeeds but the release build loses a class, a native library is missing on one device, or R8 removes the SDK entry point. The dependable approach is to treat the AAR as a binary dependency with a known contract—not as a mysterious Android Studio module.

AAR, JAR, or Maven coordinate?

  • An AAR is a ZIP-based Android library archive. It can contain compiled classes, resources, a manifest, assets, native libraries, lint checks, Prefab packages, and consumer shrinker rules.

  • A JAR contains JVM classes and resources but does not provide Android resources or an Android manifest.

  • A Maven coordinate such as com.vendor:sdk:2.4.1 points to an artifact plus metadata, which lets Gradle resolve transitive dependencies and versions.

  • A local AAR is reasonable for a proprietary SDK, an offline build, or a short evaluation. It becomes harder to govern as teams and versions multiply.

1. Put the binary inside the consuming module

Android project rootbash
mkdir -p app/libs
cp /path/to/vendor-sdk-2.4.1.aar app/libs/
The app module now owns a versioned copy at app/libs/vendor-sdk-2.4.1.aar.

Why the location and filename matter

  • app/libs is a convention, not a Gradle magic directory; the dependency declaration below is what makes the file participate in the build.

  • A versioned filename makes reviews, cache diagnosis, and rollback much less ambiguous than sdk.aar.

  • mkdir -p is safe and idempotent; cp replaces an existing file with the same destination name, so compare checksums before upgrading.

  • Confirm that your license permits committing the binary. Private artifact storage is often preferable to Git for commercial SDKs.

2. Declare the AAR in Kotlin DSL

app/build.gradle.ktskotlin
dependencies {
    implementation(files("libs/vendor-sdk-2.4.1.aar"))
}

What this one line actually does

  • files(...) creates a file dependency relative to the module directory.

  • implementation places the AAR on the app compile and runtime classpaths without exposing it as a public API dependency.

  • Android Gradle Plugin merges the AAR manifest and resources while packaging its bytecode and compatible native libraries into the app.

  • The declaration does not discover libraries that the AAR itself was compiled against; add those separately from vendor documentation.

Groovy DSL equivalent

app/build.gradlegroovy
dependencies {
    implementation files('libs/vendor-sdk-2.4.1.aar')
}

Same dependency, different build-script language

  • Use the syntax matching the file already in the module: .gradle.kts is Kotlin DSL and .gradle is Groovy DSL.

  • Do not paste both variants into one project.

  • Keep the declaration in the module that consumes the SDK, normally :app, rather than the root build file.

  • After editing, run a Gradle sync or a wrapper build so the declaration is evaluated.

When a file tree is appropriate

app/build.gradle.ktskotlin
dependencies {
    implementation(
        fileTree(mapOf(
            "dir" to "libs",
            "include" to listOf("*.jar", "*.aar"),
        )),
    )
}

Convenient, but less explicit

  • The patterns select every matching JAR and AAR in app/libs; the internal asterisk in *.aar is a glob character, not emphasis markup.

  • A file tree is useful for a controlled bundle of related binaries but hides the exact dependency list from code review.

  • Prefer individual files(...) declarations when each binary has its own version, license, owner, or transitive requirements.

  • Do not combine an individual declaration and a matching file tree or the artifact may be added twice.

Prefer repository metadata for a real SDK

settings.gradle.ktskotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven(url = uri("https://packages.vendor.example/android"))
    }
}

Repository boundaries belong in settings

  • Replace the example URL with the vendor’s documented HTTPS repository; it is intentionally not a working endpoint.

  • dependencyResolutionManagement centralizes repositories and works with projects that restrict module-level repository declarations.

  • Use repository credentials from Gradle properties, environment-backed credentials, or a secret manager—never commit tokens in this file.

  • Restrict repository content to the vendor group when possible to reduce accidental or malicious dependency substitution.

app/build.gradle.ktskotlin
dependencies {
    implementation("com.vendor:sdk:2.4.1")
}

Why coordinates scale better

  • The three fields are group, artifact, and version.

  • Gradle reads repository metadata to resolve declared transitive dependencies and reconcile repeated modules.

  • A fixed version is reproducible; avoid dynamic selectors such as 2.+ in production builds.

  • Use dependency locking or verification when your supply-chain policy requires stronger reproducibility.

The trap: an AAR does not bundle every dependency

A loose AAR file gives Gradle the archive, not the POM that normally describes its dependency graph. If the SDK uses OkHttp, Kotlin coroutines, Play services, or another vendor library without embedding it, your app must declare the compatible dependency explicitly. Ask the producer for a dependency list, supported AGP/Kotlin versions, minimum SDK, required permissions, consumer rules, and supported ABIs.

app/build.gradle.kts (illustrative only)kotlin
dependencies {
    implementation(files("libs/vendor-sdk-2.4.1.aar"))
 
    // Add only dependencies required by the vendor documentation.
    implementation("com.example:vendor-runtime-dependency:1.7.0")
}

Never guess the missing coordinate

  • The second coordinate is a placeholder that demonstrates placement; it must be replaced or removed.

  • A compile-time Unresolved reference can mean the class is absent from the AAR or its dependency is missing.

  • A runtime NoClassDefFoundError often means compilation saw an API that packaging or runtime resolution did not supply.

  • Align versions with the producer’s tested matrix instead of selecting the newest release blindly.

Inspect the archive before trusting it

Android project rootbash
unzip -l app/libs/vendor-sdk-2.4.1.aar
sha256sum app/libs/vendor-sdk-2.4.1.aar
Expect AndroidManifest.xml and, depending on the SDK, classes.jar, res/, assets/, jni/, proguard.txt, lint.jar, api.jar, or prefab/ entries, followed by a SHA-256 digest.

The archive tells you what can enter the app

  • unzip -l lists names without extracting or executing the library.

  • classes.jar carries JVM bytecode; res/ and the manifest enter Android merge steps.

  • jni/<abi>/*.so reveals which CPU architectures have native binaries.

  • proguard.txt or consumer rules can affect app shrinking; review them as third-party build input.

  • Compare the SHA-256 value with a checksum delivered through a trusted vendor channel and record it with the upgrade review.

App consumer versus library producer

  • implementation in a library controls API exposure but does not magically copy an arbitrary local AAR into the generated AAR.

  • Publishing with Maven metadata lets consumers retrieve both artifacts as a dependency graph.

  • api exposes dependency types to consumers and should be used only when those types are part of your public API.

  • Wrapping or merging third-party binaries may violate licensing and can create duplicate resources/classes; obtain explicit permission and test the result.

Build and inspect the resolved graph

Android project rootbash
./gradlew :app:assembleDebug :app:lintDebug
./gradlew :app:dependencies --configuration debugRuntimeClasspath
The build should complete, lint should report no new SDK integration errors, and the runtime classpath report should contain expected repository dependencies. File dependencies can appear without rich transitive metadata.

A sync is not a release test

  • Use the checked-in Gradle wrapper so local and CI builds use the project’s configured Gradle version.

  • assembleDebug exercises compilation, manifest/resource merge, dexing, and packaging for that variant.

  • lintDebug catches manifest, API-level, resource, and integration issues that compilation can miss.

  • The dependency report explains selected versions but cannot invent dependency metadata for a loose AAR.

  • Repeat with the actual release variant because shrinking, signing, build constants, and resources may differ.

Release, R8, resources, and native code

  • Run a minified release build and exercise SDK entry points reached through reflection, JNI, serialization, or manifest registration.

  • Prefer consumer rules shipped by the library producer. Add app keep rules only for behavior you can explain and test.

  • Inspect manifest-merger reports for exported components, permissions, authorities, features, and conflicting attributes.

  • Prefix library resources where possible; app resources and multiple AARs share merge namespaces and can collide.

  • For native SDKs, test every shipped ABI and device architecture. An AAR containing only arm64-v8a cannot serve an x86_64 emulator process.

  • Check minSdk, compile requirements, Kotlin/JVM bytecode target, namespace behavior, and AAR metadata compatibility supplied by the producer.

Common failures, translated

  • Could not find ...aar: the path is wrong relative to the consuming module, filename case differs, or the file is absent in CI.

  • Duplicate class: the AAR embeds a class also supplied by another JAR/AAR, or the same binary is declared twice.

  • NoClassDefFoundError: a runtime dependency is missing, excluded, incompatible, or stripped from the final variant.

  • Manifest merger failed: the library declares a conflicting component, provider authority, SDK constraint, or manifest attribute.

  • Android resource linking failed: resources conflict, an expected dependency resource is missing, or the binary is incompatible with the build setup.

  • UnsatisfiedLinkError: the native library is absent for the running ABI, a dependent shared object is missing, or JNI names/signatures do not match.

  • Direct local .aar file dependencies are not supported when building an AAR: publish dependencies with metadata or restructure modules instead of expecting nested binary fusion.

  • Class works in debug but not release: inspect mapping/usage output and vendor consumer rules; reflection or JNI probably needs targeted preservation.

A practical acceptance checklist

  • The artifact came from an authenticated source, its checksum and version are recorded, and its license is approved.

  • The dependency is declared once in the correct module and builds on a clean checkout in CI.

  • Vendor transitive dependencies are explicit or arrive through trusted repository metadata.

  • Debug and minified release variants pass compile, lint, install, startup, and feature tests.

  • Manifest components, permissions, resources, consumer rules, native ABIs, minimum SDK, and privacy/data behavior have been reviewed.

  • The team has an owner, upgrade procedure, rollback version, vulnerability-monitoring path, and source for release notes.

Primary references