8

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
Sira Lam
  • 5,179
  • 3
  • 34
  • 68
  • 1
    When you use implementation, you also have to add dependent libraries of your library. So in this case you have to add those dependent libraries in your app module. – Chandrakanth Mar 29 '18 at 09:41
  • @Chandrakanth Are you sure this is the case? As far as I know, `implementation` only means app itself cannot access dependency of dependency. Otherwise, how come it can completely replace `compile` which is being deprecated? – Sira Lam Mar 29 '18 at 09:43
  • @SiraLam Check out this one. https://stackoverflow.com/questions/44493378/whats-the-difference-between-implementation-and-compile-in-gradle – Mr. Borad Mar 29 '18 at 09:53
  • @Mr.Borad Checked that already, and that's why I said "As far as I know, implementation only means app itself cannot access dependency of dependency.". Since `app` does not call any method of `TransitionsEverywhere`, theoretically `implementation` should work. – Sira Lam Mar 29 '18 at 09:55
  • As you mentioned in error, you are trying to use com.transitionseverywhere.TransitionSet which belongs to third party library...Am I correct? – Chandrakanth Mar 29 '18 at 09:59
  • @Chandrakanth Which is strange, because I simply called a public method of my own library. (Edited the question) – Sira Lam Mar 29 '18 at 10:01
  • @Chandrakanth And, if a public method which uses a dependency means it should use `compile` or `api`, it will entail that only the top level module (`app` in most cases) can use the keyword `implementation`?! – Sira Lam Mar 29 '18 at 10:04
  • 1
    When you used implementation that's mean that lib only for right code. it will not compile when you building code. And when you used compile that means while building the project that lib code also builds in you project. Now when you get the error at runtime at time code is exist but that dependant lib is not. Hope you have clear now. – Mr. Borad Mar 29 '18 at 10:04
  • I have the exact same issue. What is your solution? Did you just changed all `implementation` to `api`? – WarrenFaith Jun 04 '19 at 06:18
  • 1
    @WarrenFaith Oh that has been a year. Now I know exactly why - when a your project has dependency A which uses implementation to include dependency B, dependency B will not be visible to your project. But since I have included dependency A through online repository, your project's gradle has no idea that dependency A is requesting dependency B. So if you request dependency B from project it will solve; but the best way is to use `api` instead of `implementation` on your library. In short, imo, every module should use `api`. Only top level project should use `implementation`. – Sira Lam Jun 04 '19 at 08:13

1 Answers1

4

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
}
WarrenFaith
  • 57,492
  • 25
  • 134
  • 150
  • Honestly I think you absolutely has much much more knowledge on this topic than me, and from my point of view very few Android Developers would be able to really understand your solutions (A majority of them would be able to understand your key points and sum up though). So I just want to ask do you think the need of doing this is actually a bad-design of Gradle? – Sira Lam Jun 05 '19 at 03:26
  • On the contrary. I am investigating unknown areas for me and I really think that I miss the point where the POM entries are generated automatically and correctly. So what I did is mainly working around missing information I have – WarrenFaith Jun 05 '19 at 08:39
  • @WarrenFaith i included the script to generate the pom file as you mentioned. I still keep getting the same error? any update on this? i am badly stuck with this since weeks – Kaveri Sep 05 '19 at 13:21
  • @Kaveri Have you checked that your generated pom file now includes the dependencies? Also check if the gradle cache does not use the old versions (the cache is set by default to 24h). (check the edit of my answer at the bottom) – WarrenFaith Sep 06 '19 at 08:34