PVoLan
PVoLan

Reputation: 1030

Gradle and nested non-transitive dependencies

Here is a test project: click

I have a test Gradle Android project with three modules: app, library_a, library_b. app depends on library_a, then library_a depends on library_b:

build.gradle (app)

dependencies {
    ...
    compile (project(":library_a")){
        transitive = false;
    }
}

build.gradle (library_a)

dependencies {
    ...
    compile (project(":library_b")){
        transitive = false;
    }
}

Note that I set transitive = false because I don't want classes from library_b to be accessed from app

Every module has just one class, code is pretty simple:

app:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
        ClassA classA = new ClassA();
        classA.doSomething();
    }
}

library_a:

public class ClassA
{
    public void doSomething(){
        Log.i("Test", "Done A!");

        ClassB classB = new ClassB();
        classB.doSomething();
    }
}

library_b:

public class ClassB
{
    public void doSomething(){
        Log.i("Test", "Done B!");
    }
}

Well, here is the problem: I'm building my project with gradlew. Apk is compiling successfully, but when I run it I get NoClassDefFoundError.

I/Test﹕ Done A!
E/AndroidRuntime﹕ FATAL EXCEPTION: main
    java.lang.NoClassDefFoundError: ru.pvolan.library_b.ClassB
            at ru.pvolan.somelibrary.ClassA.doSomething(ClassA.java:12)
            ...

If I set transitive = true in both .gradle files, it runs ok, but, as I noted above, I don't want dependency to be transitive, as far as I don't want ClassB can be accessed from MainActivity - only ClassA.

What am I doing wrong?

Upvotes: 5

Views: 2323

Answers (3)

Justin Rhoades
Justin Rhoades

Reputation: 711

This is a problem that Gradle has simplified in Gradle v3.4.

If you convert library A to use v3.4 there is a simple fix.

Gradle 3.4 changes the "compile" configuration to a set of configurations "api" and "implementation".

First you should upgrade gradle to 3.4 and use the java-library plugin in lieu of the java plugin.

You should use the "api" configuration on any jar that is explicitly used in the API method calls (return type, input parameters, etc).

For all other jars that you want to "hide" (like Library B) you should use the "implementation" configuration. As Library B is only used within the body of implementation methods there is no need to expose it to any other jars at compile time; however it still needs to be available at runtime so Library A can use it.

To implement this your Library A script should replace

apply plugin: 'java' 

dependencies {
    ...
    compile (project(":library_b")){
        transitive = false;
    }
}

with

apply plugin: 'java-library'

dependencies {
    implementation project(":library_b")
}

This change will tell Gradle to include Library B as a runtime dependency of app, so that app cannot compile against it, but Library B still will be available at runtime for Library A to use. If for some reason app ends up needing Library B in the future, it would be forced to explicitly include Library B in it's dependency list to ensure it gets the desired version.

See this description from Gradle itself for more details and examples: https://blog.gradle.org/incremental-compiler-avoidance

Upvotes: 2

QArea
QArea

Reputation: 4981

Do you use multidex? When I had a problem like this I used multidex and called class from different module. I could fix it only by turning off multidex and running proguard.

UPD

android {
compileSdkVersion 21
buildToolsVersion "21.1.0"

defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 21
        ...
// Enabling multidex support.
        multiDexEnabled true
    }
    ...
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
} 

more about multi dex https://developer.android.com/tools/building/multidex.html

and about proguard http://developer.android.com/tools/help/proguard.html

Upvotes: 0

Mark Vieira
Mark Vieira

Reputation: 13486

The problem is that library_b is a required dependency. You can't simply exclude it, since you need it to be on the classpath at runtime. You are effectively misrepresenting your actual dependencies in order to enforce a code convention and therefore losing any advantage of leveraging a dependency management system like Gradle. If you want to enforce class or package blacklist I'd suggest using a source analysis tool like PMD. Here's an an example of a rule to blacklist specific classes.

If that is not possible for some reason you can get your above example to "work" by simply adding library_b to the runtime classpath of app.

dependencies {
    runtime project(':library_b')
}

Upvotes: 0

Related Questions