Gradle for Android, AARs, and Conditional Dependen

2020-05-21 05:27发布

问题:

Short Form: What are some ways of organizing the code/POM for an AAR, such that apps using that AAR only have dependencies they actually need?

Long Form:

Suppose we have an app that depends upon an Android library project packaged as an AAR (L).

L contains a mix of classes, and any given app (like A) will only use a subset of those classes. For example:

  • L may contain Fragment implementations for native API Level 11 fragments, the backported fragments, and ActionBarSherlock-flavored fragments

  • L may contain Activity implementations for regular activities, FragmentActivity, ActionBarActivity, and ActionBarSherlock-flavored activities

  • L may raise events via LocalBroadcastManager, Square's Otto, and greenrobot's EventBus

  • And so on

These cases have two main commonalities, as I see it:

  1. Apps will usually only care about some of the classes. For example, an app that uses Otto will not care about code that references greenrobot's EventBus, or an app that uses ActionBarActivity will not care about ActionBarSherlock.

  2. If the AAR is an artifact in a repo, apps will not care about all possible upstream dependencies that is needed to build the AAR. For example, an app that is using native API Level 11 fragments will not need support-v4 or actionbarsherlock, even though the AAR itself needs them to build the AAR

If we were to go with JARs instead of AARs and dump dependency management, this is fairly straightforward. Building the JAR would have compile-time dependencies (e.g., support-v4). However, apps using that JAR could skip those dependencies, and so long as those apps do not use classes that truly need those dependencies, life is good.

However, I am having difficulty in seeing how to accomplish the same thing with AARs and Maven artifacts specified in a build.gradle file. If L has a dependencies block referencing the upstream dependencies, apps will in turn download those dependencies transitively when the apps depend upon L.

One solution that I am fairly sure will work is to split L into several libraries. For example, using the fragment scenario, we could have:

  • L1, which contains the implementation for the native API Level 11 version of fragments, plus any common code needed for other scenarios. This library would have no upstream dependencies.

  • L2, which contains the implementation that uses the Android Support package's backport of fragments. L2 would have dependencies on L1 and on support-v4.

  • L3, which contains the implementation that uses Sherlock-flavored fragments. L3 would have dependencies on L1 and actionbarsherlock.

Then, an app would choose whether to depend upon L1, L2, or L3, and therefore would only get the necessary upstream dependencies (if any).

My question is: is that the best solution? Or is there something else in the world of Gradle for Android, AARs, and Maven-style artifacts that would allow apps to depend upon a single L? I am concerned about possible combinatoric explosions of libraries to handle a varied mix of upstream dependencies. I am also concerned about oddball apps that actually do need multiple implementations and whether or not we can reliably specify the dependencies for those (e.g., an app depending on both L1 and L2, because that's what that app's author thinks that app needs).

I know that there are ways for an app to block exclude dependencies (see Joseph Earl's answer for the syntax), so an app could depend upon L but then block the actionbarsherlock upstream dependency if it is not needed. While that could work, for cases where I'm the author of L, I'd rather go the L1/L2/L3 approach, as that seems cleaner.

Any other suggestions?

回答1:

I'm not aware of any dependency management feature that would help with this.

Having a single libraries with many dependencies and the ability to remove unwanted dependencies could work but there are some issues that will come with this:

  • You have to rely on users of L to remove the right dependencies.
  • You'll have classes in the library that won't be easy to strip out with Proguard. Proguard will not remove anything that extends Fragment/Activity/etc, and L should provide a proguard rule files to not remove classes that extend the support Fragment/Activity/etc... This will make it difficult to remove unwanted classes.
  • Some implementation may have additional resources and right now we can't strip out unneeded resources.

I think splitting the library is the right thing to do, however this is going to be complicated to do until we have flavors in library projects. Once we have this I think it'll be much easier to handle. You wouldn't have a base L1 and L2/L3 extending it. Instead you'd have a single library that generate different variants, each with their own dependencies.

With flavor dimensions, you could handle support libs vs event libs combinations though you'd definitively get some explosions if you add more dimensions.

The advantages of a multi-variant library is that it's much easier to share code across a subset of variants without introducing even more sub libraries. You can also have bi-directional references between the main code and the flavors, which you couldn't have with L2/L3 depending on L1.

As for supporting Ant/Eclipse, this is definitively going to be complicated. I would ignore Ant definitively. If someone only cares about command line build they should already move to Gradle. As for Eclipse, well we'll have Gradle support at some point, but I understand if you can't wait.



回答2:

In your application project can exclude transitive dependencies in Gradle (e.g. exclude ActionBarSherlock being included if you know it won't be used), e.g.

 dependencies {
   compile('org.hibernate:hibernate:3.1') {
     // Excluding a particular transitive dependency:
     exclude module: 'cglib' //by artifact name
     exclude group: 'org.jmock' //by group
     exclude group: 'org.unwanted', module: 'iAmBuggy' //by both name and group
   }
 }

You can also ProGuard (and DexGuard) to compact and strip out unused code.

Gradle doesn't yet support marking dependencies as optional as Maven does (which could be used in the library project).



回答3:

Well, the first thing I'd like to mention is Gradle configurations that are actually kinda reflection of Ivy configurations: a module you deploy to some repository can have different configurations and each configuration may have its own set of dependencies. http://www.gradle.org/docs/current/userguide/dependency_management.html#ssub:multi_artifact_dependencies

Yet, Maven does not fit this case, since there are no configurations in Maven world. And artifact classifiers will not allow you to have different dependencies.

What we can do with Maven is remove compile time dependency declarations from POM or mark them with provided scope. AFAIK ability to specify compile-time only dependencies will be available in future releases of Android Gradle plugin. https://code.google.com/p/android/issues/detail?id=58626