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.)
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?!
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.
To eliminate other factors, below are things that I have tried before changing implementation
to compile
, all no luck:
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
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”.
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.
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.
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:
implementation
dependencies contains the api
ones as well, just run the task listDependencies()
to see the outputruntime
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 tooimplementation
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 runtimeEdit
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With