This error has a frustrating habit: the compiler points at Android O, so the tempting fix is to raise minSdk to 26 and move on. That can abandon users without fixing the build contract. Read the full D8/R8 diagnostic first—the ordinary Java 8 desugaring case and the MethodHandle case need different decisions.

What “invoke-custom” means

invoke-custom is a DEX instruction used for dynamic call-site behavior. Java compilers commonly emit invokedynamic for lambdas and related constructs; Android build tools translate supported bytecode through desugaring before producing DEX for older devices. The message appears when unsupported invocation bytecode reaches dexing for a minimum API below 26.

1. Apply the standard Kotlin DSL fix

app/build.gradle.ktskotlin
android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
 
    // For Kotlin plugin versions that still use kotlinOptions:
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Why this belongs in the module

  • sourceCompatibility permits Java 8 source features for Java compilation.

  • targetCompatibility selects the emitted Java class-file level consumed by Android build tools.

  • jvmTarget aligns Kotlin bytecode with the Java target for Kotlin plugin versions using this DSL.

  • Place the block in each Android application or library module that uses Java 8 features directly or through dependencies.

  • A current project may target Java 17 instead; use one deliberately aligned level supported by its AGP, Kotlin plugin, and toolchain.

Groovy build-script equivalent

app/build.gradlegroovy
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
 
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Use only the syntax your module expects

  • Use this form for build.gradle; use the previous form for build.gradle.kts.

  • Do not add another android block if one exists—merge compileOptions into it.

  • Pure Java modules use Java plugin/toolchain configuration rather than the Android extension.

  • Sync is only configuration validation; run the failing variant afterward.

2. Reproduce with the Gradle wrapper

Android project rootbash
./gradlew :app:assembleDebug --stacktrace
A successful build ends with BUILD SUCCESSFUL. On failure, inspect the first D8/R8 cause and the class or dependency named near it.

The first actionable cause matters most

  • The wrapper uses the Gradle version committed with the project.

  • :app:assembleDebug isolates one module and variant; replace app or the variant when the failure occurs elsewhere.

  • --stacktrace adds diagnostic context but can expose local paths in logs, so sanitize before sharing.

  • Do not treat “Gradle sync succeeded” as proof that dexing and packaging work.

3. Find which dependency contributes the bytecode

Android project rootbash
./gradlew :app:dependencies --configuration debugRuntimeClasspath
./gradlew :app:dependencyInsight --dependency suspected-library --configuration debugRuntimeClasspath
The reports show the selected dependency graph and why the suspected component/version was chosen.

Trace selection before changing versions

  • The runtime classpath is the relevant graph for classes packaged into the debug app.

  • Replace suspected-library with an artifact or group fragment from the failure.

  • dependencyInsight reveals version conflict resolution and the request path.

  • A local JAR/AAR has no POM metadata; inspect its classes and obtain its compatibility matrix from the producer.

  • Repeat for release runtime classpath when only the release variant fails.

Language desugaring and API desugaring are not the same

  • Language desugaring rewrites supported bytecode constructs such as lambdas, method references, default interface methods, and try-with-resources.

  • Core library desugaring supplies implementations and rewrites calls for selected newer Java library APIs on older Android versions.

  • Setting Java compatibility alone does not make every JDK API available on every Android release.

  • Raising the JDK that runs Gradle is separate again; for example, AGP 8.x requires JDK 17 to run even when app bytecode targets another level.

When core library desugaring is actually needed

app/build.gradle.ktskotlin
android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
}
 
dependencies {
    coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:<compatible-version>")
}

Choose the version from the official compatibility table

  • Core library desugaring is for supported APIs such as parts of java.time, streams, and other documented Java APIs on older Android releases.

  • <compatible-version> is intentionally a placeholder; pin a release compatible with the project’s AGP instead of copying a stale number.

  • This dependency belongs in the special coreLibraryDesugaring configuration, not ordinary implementation.

  • It does not make arbitrary JDK internals or unsupported invocation methods portable.

  • For minSdk 20 or lower, consult current Android documentation for multidex requirements.

The important exception: MethodHandle.invoke

Android’s documented desugar support does not cover signature-polymorphic MethodHandle.invoke or MethodHandle.invokeExact for devices below API 26. If the diagnostic explicitly names one of those methods, Java 8 compileOptions is not a compatibility layer for it.

  • If the call is yours, replace it with an older-platform-compatible design or guard an API-26 implementation behind an API check and separate class loading as appropriate.

  • If unused dependency code contains the call, release shrinking may remove it—but verify the final app and do not rely on debug/release behaving identically.

  • If reachable dependency code requires it, upgrade/downgrade to a compatible release or choose another library.

  • Raise minSdk to 26 only when the product intentionally ends support for older devices and the whole application is tested under that new baseline.

Modern Java and Kotlin projects

app/build.gradle.kts (modern aligned example)kotlin
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
 
android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

Toolchain, source level, and device runtime are separate

  • The toolchain chooses a JDK compiler consistently across developer machines and CI.

  • Source and target compatibility control Java syntax and class output.

  • The JDK running Gradle must meet the AGP release requirement.

  • Android API availability is governed by compileSdk, minSdk, runtime checks, and documented API desugaring—not simply the toolchain number.

  • For Kotlin versions/configurations using newer compiler options, align the Kotlin JVM target with the Java target through the supported Kotlin DSL.

Why the old top-level fix can fail

  • The Android compileOptions block is a module setting; adding it to an unrelated root script does not configure the failing module.

  • A library module can be the failing compilation unit even when the final app already has Java 8 enabled.

  • Old AGP/Gradle combinations may not support the required desugaring behavior and should be upgraded using their compatibility tables.

  • The error may originate in a precompiled dependency rather than app source.

  • An explicit MethodHandle.invoke diagnostic is an unsupported-operation case, not an ordinary missing-lambda setting.

Do not “fix” it by changing minSdk casually

  • minSdk declares the oldest Android API on which the app is allowed to install.

  • Changing 21 to 26 removes Android 5, 6, and 7 devices from eligibility.

  • It does not repair mismatched Java/Kotlin targets or outdated build tooling.

  • Raise it only through a product/platform support decision backed by usage data, dependency requirements, release notes, and device tests.

A clean diagnostic sequence

  1. Capture the complete D8/R8 error, failing task, variant, class name, and dependency path.

  2. Confirm the failing module has aligned Java and Kotlin targets.

  3. Check that the project’s AGP, Gradle, JDK, and Kotlin versions are mutually supported.

  4. Rebuild the exact task with the wrapper and stacktrace.

  5. Inspect the relevant runtime classpath and dependency insight.

  6. Determine whether the bytecode is a supported language feature, a supported Java library API needing core desugaring, or an unsupported MethodHandle call.

  7. Apply the narrow fix and test both debug and minified release on the oldest supported API.

Troubleshooting map

  • Still fails after compileOptions: the setting is in the wrong module, the build toolchain is too old, or the bytecode comes from an unsupported dependency construct.

  • Only one module fails: configure that Android library/application module rather than only :app.

  • Only release fails: inspect R8 diagnostics, keep rules, dependency variants, and release runtime classpath.

  • Works after minSdk 26: this confirms the bytecode/runtime requirement but does not prove abandoning older devices is the right fix.

  • Kotlin target mismatch warning: align the Kotlin JVM target and Java target using DSL appropriate to the installed Kotlin plugin.

  • java.time missing on old devices: verify the API is in the official desugaring table and configure compatible core library desugaring.

  • Local AAR/JAR triggers it: request the producer’s minSdk, bytecode target, AGP/JDK matrix, and a compatible build; loose binaries do not expose rich metadata.

Verification before closing the issue

Android project rootbash
./gradlew clean :app:assembleDebug :app:assembleRelease :app:lintDebug
Both APK variants and lint should complete. Install and exercise the affected path on an emulator/device at the project minimum API as well as a current Android release.

A clean build removes stale evidence

  • clean deletes generated build outputs, so the next build is slower but confirms the fix is reproducible.

  • Building debug and release catches variant-specific dexing and shrinking differences.

  • Lint can identify unsupported API use that bytecode conversion alone does not explain.

  • A build success is incomplete without executing the lambda, method reference, library call, or reflection path that originally triggered the dependency.

  • Run the same wrapper tasks in CI from a clean checkout.

Official references