Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ClassNotFoundException when "implementation" is used for library dependency of a library

I just created a library and uploaded to bintray and jcenter.

In my testing app, this library is added as a module:

implementation project(':dropdownview')

And everything wells well.

After the library module is uploaded to jcenter, I used this instead:

implementation 'com.asksira.android:dropdownview:0.9.1

Then a runtime error occurs when the library tries to call a method that depends on another library:

Caused by: java.lang.ClassNotFoundException: Didn't find class "com.transitionseverywhere.TransitionSet" on path: DexPathList[[zip file "/data/app/com.asksira.dropdownviewdemo-6fj-Q2LdwKQcRAnZHd2jlw==/base.apk"],nativeLibraryDirectories=[/data/app/com.asksira.dropdownviewdemo-6fj-Q2LdwKQcRAnZHd2jlw==/lib/arm64, /system/lib64, /system/vendor/lib64]]

(I was following this guide to publish libraries. I published 3 libraries before using the same method already, they all worked perfectly; but this is the first time I included another 3rd party library dependency in my own library.)

compile vs implementation

And then I tried to change my 3rd party library dependency of my library from

implementation 'com.andkulikov:transitionseverywhere:1.7.9'

to

compile 'com.andkulikov:transitionseverywhere:1.7.9'

(Note that this is NOT the dependency of app to my library, but my library to another library)

And upload again to bintray with version 0.9.2.

implementation 'com.asksira.android:dropdownview:0.9.2

This time it WORKED?!

My Question

Is this some kind of bug of Android Studio / Gradle (But Google is saying that they are going to remove compile by the end of 2018...), or have I done anything wrong?

The full source code of v0.9.1 can be found here.

Note that I didn't access any methods directly from app to TransitionsEverywhere. Specifically, ClassNotFoundException occurs when I tap on the DropDownView, and DropDownView calls expand() which is a public internal method.

More info

To eliminate other factors, below are things that I have tried before changing implementation to compile, all no luck:

  1. Clean and Re-build
  2. Uninstall app + clean and re-build
  3. Make the Application a MultiDexApplication
  4. Instant run has already been disabled
like image 818
Sira Lam Avatar asked Mar 29 '18 09:03

Sira Lam


People also ask

Can we use libraries that we haven’t depended on explicitly?

won’t accidentally use a library that we haven’t depended on explicitly e.g. can’t use Library C in Application as it’s not on the compile classpath less recompilation as when artifacts on the runtime classpath change we don’t need to recompile 3. The Java Library Gradle plugin makes this possible

Why do I get a classdefnotfound error when trying to compile?

When trying to compile, the compiler gets into one of library A’s classes, and when it comes across an object initialization of one of the classes from library B, we get a ClassDefNotFound. Here’s where it’s weird…. This is all if you have library A depend on library B with “implementation”.

What is the difference between API and implementation dependencies?

api – dependencies in the api configuration are part of the ABI of the library we’re writing and therefore should appear on the compile and runtime classpaths implementation – dependencies in the implementation configuration aren’t part of the ABI of the library we’re writing. They will appear only on the runtime classpath.

Why does Jackson-databind have its own dependencies in Maven?

This is because the jackson-databind artifact has declared its own dependencies as compile scope dependencies, which you can see in the Maven Central repository: And according to the Maven docs, anything in the compile scope is also included in the runtime scope Compile dependencies are available in all classpaths of a project.


1 Answers

I had right now the exact same issue and based on your comment I really had the doubt that this is the way it should be. I mean replacing all implementation inside the library with api makes no sense for clean abstractions. Why should I expose the used dependencies of my library to the consumer/app if they are not needed and sometimes even should not be allowed to be used.

I also checked that the generated APK does indeed contain the class that it complains about not being found.

As I had dependency problems earlier I remembered that I improved the generated POM for the library myself.

Before I improved it, the generated pom looked like this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>tld.yourdomain.project</groupId>
    <artifactId>library-custom</artifactId>
    <version>1.2.0-SNAPSHOT</version>
    <packaging>aar</packaging>
    <dependencies/>
</project>

I used the following script to add the dependencies and, based on implementation or api added the right scope to them (based on that nice info)

apply plugin: 'maven-publish'

task sourceJar(type: Jar) {
    from android.sourceSets.main.java.srcDirs
    archiveClassifier = "sources"
}

task listDependencies() {
    // Curious, "implementation" also contains "api"...
    configurations.implementation.allDependencies.each { dep -> println "Implementation: ${dep}" }
    configurations.api.allDependencies.each { dep -> println "Api: ${dep}" }
}

afterEvaluate {
    publishing {
        publications {
            mavenAar(MavenPublication) {
                groupId libraryGroupId
                artifactId libraryArtefactId
                version versionName

                artifact sourceJar
                artifact bundleReleaseAar

                pom.withXml {
                    def dependenciesNode = asNode().appendNode('dependencies')
                    configurations.api.allDependencies
                            .findAll { dependency -> dependency.name != "unspecified" }
                            .each { dependency ->
                        addDependency(dependenciesNode.appendNode('dependency'), dependency, "compile")
                    }

                    configurations.implementation.allDependencies
                            .findAll { dependency -> !configurations.api.allDependencies.contains(dependency) }
                            .findAll { dependency -> dependency.name != "unspecified" }
                            .each { dependency ->
                        addDependency(dependenciesNode.appendNode('dependency'), dependency, "runtime")
                    }
                }
            }
        }

        repositories {
            maven {
                def snapshot = "http://repo.yourdomainname.tld/content/repositories/snapshots/"
                def release = "http://repo.yourdomainname.tld/content/repositories/releases/"
                url = versionName.endsWith("-SNAPSHOT") ? snapshot : release
                credentials {
                    username nexusUsername
                    password nexusPassword
                }
            }
        }
    }
}

def addDependency(dependencyNode, dependency, scope) {
    dependencyNode.appendNode('groupId', dependency.group)
    dependencyNode.appendNode('artifactId', dependency.name)
    dependencyNode.appendNode('version', dependency.version)
    dependencyNode.appendNode('scope', scope)
}

Key parts that you need to understand:

  • if you do not define a scope, "compile" will be assumed
  • implementation dependencies contains the api ones as well, just run the task listDependencies() to see the output
  • with the scope runtime the API is not available in the app/consumer but is part of the classpath. This way the consumer can not access those dependencies directly, only via the methods provided by your own library making those dependencies "invisible" BUT they will be part of the classpath so the app will not crash when those classes of the "invisible" dependencies are loaded by the classloader.

That script above now generates the following pom:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>tld.yourdomain.project</groupId>
    <artifactId>library-custom</artifactId>
    <version>1.2.0-SNAPSHOT</version>
    <packaging>aar</packaging>
    <dependencies>
        <dependency>
            <groupId>tld.dependency</groupId>
            <artifactId>android-sdk</artifactId>
            <version>1.2.3</version>
            <scope>compile</scope> <!-- From api -->
        </dependency>
        <dependency>
            <groupId>tld.dependency.another</groupId>
            <artifactId>another-artifact</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope> <!-- From implementation -->
        </dependency>
        <!-- and much more -->
    </dependencies>
</project>

To sum it up:

  • api ships the classes, makes the dependency accessible for the consumer too
  • implementation ships the classes too but does not make the dependency accessible for the consumer but, with the defined runtime scope, it will still be part of the classpath making the classloader aware that those classes are available during runtime

Edit

If you are changing a lot and test your snapshots, make sure you have disabled the cache for them. Add this to your root build.gradle file:

allprojects {
    configurations.all() {
        // to make sure SNAPSHOTS are fetched again each time
        resolutionStrategy.cacheDynamicVersionsFor 0, 'seconds'
        resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
    }
    // more stuff here
}
like image 81
WarrenFaith Avatar answered Sep 24 '22 07:09

WarrenFaith