Kotlin / Gradle DSL integration to manage dependency version variables - android-studio

Kotlin / Gradle DSL integration to manage dependency version variables
My question is about the best way to manage dependency version variables in IntelliJ Idea / Android Studio using Gradle
Kotlin DSL
The secondary purpose is to have the IDE perform version upgrades upon clicking the suggestion to change to newer
version automatically instead of manually editing the versioning file entry.
the simple way
definition
build.gradle.kts
plugins {
application
kotlin("jvm") version "1.7.10"
}
dependencies {
implementation("io.ktor:ktor-server-core-jvm:2.1.1")
}
Comments
This is working but "clumsy" and does not comply to single source of truth (SSOT)
I have seen several ways that attempt having a SSOT for the dependency versions
by Project
Definition
gradle.properties
ktor_version = 2.1.0
kotlin_version = 1.7.10
build.gradle.kts
val ktor_version: String by project // NOTE the complaint Property name 'ktor_version' should not contain underscores
val kotlin_version: String by project
plugins {
application
kotlin("jvm") version "1.7.10" // NOTE you cannot replace this with the variable!
// 'val kotlin_version: String' can't be called in this context by implicit receiver. Use the explicit one if necessary
}
dependencies {
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
}
Comments
This does not update the suggested version numbers via "Project Structure/Suggestions" in AS/IJ
my inline NOTES indicate several issues with this method!
by buildSrc java Module
Definition
object Ktor {
private const val ktorVersion = "2.0.0"
const val core = "io.ktor:ktor-client-core:${ktorVersion}"
const val android = "io.ktor:ktor-client-android:${ktorVersion}"
}
implementation in build.gradle.kts
implementation(Ktor.android)
Comments
This does not update the suggested version numbers via "Project Structure/Suggestions" in AS/IJ, is seemed to
be totally de-coupled, and it is a nightmare to manually find and update the versions manually to the latest!
However: doing it by buildSrc java Module makes it much easier to re-use in other projects
by Gradle Version Catalogs
Definition
libs.versions.toml // in the gradle directory next to wrapper
[versions]
plugin-kotlin = "1.7.10"
ktor = "2.1.0"
[libraries]
plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "plugin-kotlin" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
[bundles]
plugins = ["plugin-android", "plugin-kotlin", "plugin-kotlin-serialization", "plugin-sqldelight"]
implementation in build.gradle.kts(project)
buildscript {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
dependencies {
classpath(libs.bundles.plugins)
}
}
implementation in build.gradle.kts(shared)
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.ktor.client.core)
}
}
}
Comments
This is by far the favorite of mine, the reason is clear, it allows SSOT, and it allows grouping/bundling to make
"sets" that is more understandable later on
However!
This does not update the suggested version numbers via "Project Structure/Suggestions" in AS/IJ
it is a nightmare to manually find and update the versions manually to the latest!
Main question
Is there a better way of managing dependency versions?
Is there a way that works inside the IDE AS/IJ suggestions to update the versions correctly at the definition
location?

Related

Choosing different libraries for compilation and runtime

I have two libraries with same classes defined in each one. However they have some different contents (methods/constants).
For example:
Library 1:
package com.test.package;
Class A {
// only method signatures
public void methodA() {
}
public void methodB() {
}
}
Library 2:
package com.test.package;
Class A {
public void methodA() {
// some logic that MUST be executed to provide backward compatibility
}
}
My application uses Library 1 and Library 2 and run in devices which have com.test.package.ClassA, but com.test.package.ClassA.methodB() will only exist in newer releases in framework. Said that, I need the Library 1 to be used to compile my application and the Library 2 to execute a different implementation of methodA().
I have tried to do this in Android Studio using .jar and .aar libraries format, but none of them worked for me.
Is it possible to set this configuration in an Android Studio project?
I am building both Library 1 and 2, and I cannot add methodB() in Library 2.
For a simple Java application, you can do this by unlinking the compile and runtime configurations. I set up an example repository here.
The idea is shown in this commit, but can be described as manually resetting the runtime configuration so that it doesn't include the contents of the compile configuration. After doing so, you can just include your runtime library variation in the runtime configuration.
The application's build.gradle becomes something like:
apply plugin: 'application'
mainClassName = 'my.package.MyAppClass'
configurations.runtime.extendsFrom = [] // Reset runtime configuration
dependencies {
compile 'my.group:my.artifact:2.0' // Library 1, with the new method
runtime 'my.group:my.artifact:1.0' // Library 2, without the method
}
For Android, this can be a little more complicated. The problem is that there's no runtime configuration for Android (because you don't execute it on your computer, unless you're using Robolectric or something similar).
I think there are a few workarounds you can probably use, but one initial suggestion would be to create a wrapper library that abstracts away the dependency on the other libraries. This wrapper library you can compile with the newest library version (Library 1, with the new method). You could then include the wrapper library in the Android app while setting it as a non-transitive dependency and including the other library version:
dependencies {
compile 'my.group:my.wrapped.artifact:0.1' {
transitive = false // Don't include dependencies of the wrapper
// i.e., don't include version 2.0 of the lib.
}
compile 'my.group:my.artifact:1.0'
}
This should work because by setting the dependency as non-transitive Gradle doesn't recursively include the dependencies of the wrapper library, so the version used to compile the wrapper isn't included (in theory) in the APK. You can therefore add the old version without causing a conflict.
An example is set up in the same repository, under the Android branch. Firstly, two Java libraries are created. Then an Android library is created to wrap around the compile-time library. An example activity is created to show how using the wrapper library uses the compile-time library. Then, the latest commit shows how the app is configured to use the wrapper library (which compiles with the newest library) but forces the old library to be included instead in the runtime.
Hope this helps =)

Create external link in Javadoc using Gradle in Android Studio

I have created a Gradle task that generates a javadoc using Doclava:
My code (the arguments of some of my methods) references classes defined in Android. When Javadoc is built, these references link correctly to the Android online reference. However, when I use the #ling tag to link to Android references, it does not work and I get something like:
configurations {
jaxDoclet
classpaths
}
dependencies {
// For Doclava JavaDoc
jaxDoclet("com.google.doclava:doclava:1.0.6")
classpaths files('build/intermediates/classes/debug')
classpaths project.files(android.getBootClasspath().join(File.pathSeparator))
}
task javadoc(type: Javadoc) {
source = android.sourceSets.main.allJava
source += fileTree("build/generated/source/r/debug")
title = null
options {
docletpath = configurations.jaxDoclet.files.asType(List)
doclet "com.google.doclava.Doclava"
bootClasspath new File(System.getenv('JAVA_HOME') + "/jre/lib/rt.jar")
classpath += configurations.classpaths.files.asType(List)
addStringOption "public"
addStringOption "federate android", "http://d.android.com/reference"
addStringOption "federationxml android", "http://doclava.googlecode.com/svn/static/api/android-10.xml"
}
}
warning 101: Unresolved link/see tag "Runnable" in com....
In similar questions in SO, it was advised to use -link and -linkoffline flags. However, when I do that I get:
javadoc: error - invalid flag: -linkoffline
I am using Android Studio 1.5.1 and Gradle 2.11.
Update
It seems that Doclava may not support -link and -linksoffline according to these tickets. If I use the default doclet, links work correctly.

How to depend another project's androidTestCompile?

I have a multi-project test issue: I want one project to depend on another project's androidTestCompile.
I have tried this way:
androidTestCompile project(':CommonTest').sourceSets.androidTest
But android studio says it could not find property androidTest on SourceSet container.
Help
Here's an approach that might work. I've not tried it as I don't have a multi-project Android project. You'll need to tweak the code for your project names and specify the dependsOn in your current projects task for Android Tests.
task action(dependsOn: ":producer:action") << {
println("Consuming message: ${rootProject.producerMessage}")
}
See the topic 24.6.1.2. Declaring dependencies in https://docs.gradle.org/current/userguide/multi_project_builds.html
Another clue is in the Gradle Java Tutorial which explains how to establish dependencies between projects in a multi-project build https://docs.gradle.org/current/userguide/tutorial_java_projects.html#N14E23
dependencies {
compile project(':shared')
}
So I'd hazard a guess that something like
dependencies {
androidTestCompile project(':CommonTest')
}
(which you've tried, albeit with more parameters) is on the right track.

What the difference in applying gradle plugin

I don't understand gradle plugins block
apply plugin: 'someplugin1'
apply plugin: 'maven'
and other one:
plugins {
id 'org.hidetake.ssh' version '1.1.2'
}
In first block We have some plugin name. in second one package and version. I don't understand where I should use first block and when second one.
The plugins block is the newer method of applying plugins, and they must be available in the Gradle plugin repository. The apply approach is the older, yet more flexible method of adding a plugin to your build.
The new plugins method does not work in multi-project configurations (subprojects, allprojects), but will work on the build configuration for each child project.
I would think that as functionality progresses, the plugins configuration method will overtake the older approach, but at this point both can be and are used concurrently.
As already mentioned by #cjstehno the apply plugin is a legacy method that you should avoid.
With the introduction of the plugins DSL, users should have little reason to use the legacy method of applying plugins. It is documented here in case a build author cannot use the plugins DSL due to restrictions in how it currently works.
With the new plugins block method, you can add a plugin and control when to apply it using an optional parameter apply:
plugins {
id «plugin id» version «plugin version» [apply «false»]
}
You would still use the legacy method in situations where you want to apply an already added but not applied plugin in your plugins block. E.g, in the master project a plugin xyz is added but not applied and it should be applied only in a subproject subPro:
plugins {
id "xyz" version "1.0.0" apply false
}
subprojects { subproject ->
if (subproject.name == "subPro") {
apply plugin: 'xyz'
}
}
Notice that you don't need the version anymore. The version is required in the plugins block unless you are using one of the Core Gradle plugins, such as java, scala, ...
I spent some time understanding the difference while trying to create a Spring Boot application, and that's why I am answering this again after a while. The following example for using Spring Boot plugin helped me a lot:
What should currently be used:
plugins {
id "org.springframework.boot" version "2.0.1.RELEASE"
}
What had been used before Gradle 2.1:
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:2.0.1.RELEASE"
}
}
apply plugin: "org.springframework.boot"
These are two different ways to use Gradle plugin。
The apply plugin way: First resolve plugin you needed from root build.gradle like:
buildscript {
repositories {
// other repositories...
mavenCentral()
}
dependencies {
// other plugins...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.44'
}
Then in the build.gradle of your Android Gradle modules apply the plugin:
apply plugin: 'com.android.application'
apply plugin: 'com.google.dagger.hilt.android'
The plugins way:combine resovle and apply where in your root build.gradle like:
plugins {
// other plugins...
id 'com.google.dagger.hilt.android' version '2.44' apply false
}
Then in the build.gradle of your Android Gradle modules apply the plugin:
plugins {
// other plugins...
id 'com.android.application'
id 'com.google.dagger.hilt.android'
}
android {
// ...
}
Now ( In Gradle 6) you can give repositories name for plugins without using build script.
In settings.gradle, we can add plugin pluginManagement
pluginManagement {
repositories {
maven {
url '../maven-repo'
}
gradlePluginPortal()
ivy {
url '../ivy-repo'
}
}
}
Reference: https://docs.gradle.org/current/userguide/plugins.html#sec:custom_plugin_repositories
I would like to point out though, that is it not required for a plugin to be published remotely to be able to use it!
It can also be a UNPUBLISHED locally available plugin (be it convention plugins or otherwise) just as well.
In case one wishes to refer to such an unpublished locally-available plugin,
you'll have to include it's so-called "build" within the desired component/build (identified via the settings.gradle(.kts)-file) like so:
pluginManagement {
includeBuild '<path-to-the-plugin-dir-containing-the-settings-file>'
}
Afther that is done, one may use the local plugin within the plugins {}-DSL-block via its pluginId.
If the plugin needs a version then it's safer to put the version number in the pluginManagement block in your settings.gradle file, rather than in plugins block.
By safer I mean that you won't encounter an error like plugin request for plugin already on the classpath must not include a version. That can happen if you includeFlat a project into another project that uses the same plugin and your plugin versions are in the plugins block.
So rather than:
plugins {
id 'pl.allegro.tech.build.axion-release' version '1.10.3'
}
Do:
plugins {
id 'pl.allegro.tech.build.axion-release'
}
and then in your settings.gradle file:
pluginManagement {
plugins {
id 'pl.allegro.tech.build.axion-release' version '1.10.3'
}
}
I'm going to add a little twist to what's been said. Gradle introduced the concept of a plugins block as a technique to speed up and optimize the build process. Here's what Gradle's documentation says:
This way of adding plugins to a project is much more than a more convenient syntax. The plugins DSL is processed in a way which allows Gradle to determine the plugins in use very early and very quickly. This allows Gradle to do smart things such as:
Optimize the loading and reuse of plugin classes.
Provide editors detailed information about the potential properties and values in the buildscript for editing assistance.
This requires that plugins be specified in a way that Gradle can easily and quickly extract, before executing the rest of the build script. It also requires that the definition of plugins to use be somewhat static.
It's not just a newer way of dealing with plugins, it's a way of improving the build process and/or user's editing experience.
In order for it to work, it needs to be specified at the top of the build, but it also needs to be specified after the buildscript block if one is included. Why is that? Because the code in the build scripts is evaluated in the order its written. The buildscript block must be evaluated before the plugins block is evaluated. Remember, the buildscript block is about setting up of the plugin environment. Hence the rule that the plugins block must be specified after the buildscript block.
The new plugins block not only specifies the plugins that the project is using, but it also specifies whether the plugin is applied. By default, all plugins in the plugins block are automatically applied, unless it is specifically declared not to be applied (i.e., adding "apply false" after the plugin declaration in the plugins block).
So why would you declare a plugin and not apply it. There are two main reasons that I can think of:
1.) so you can declare the version of the plugin you want used. After you've declared a plugin, the plugin is now on the "classpath". Once a plugin is on the classpath you no longer need to specify the version of the plugin when you apply it later. In multiproject builds, that makes supporting buildscripts a little easier. (i.e., you only have one place where the plugin version is specified.)
2.) Sometimes, you may have a plugin, that requires certain things defined before they are applied. In that case, you can declare a plugin in the plugins block, and defer the plugin from being applied until after you define the things that the plugin requires as input. For example, I have a custom plugin that looks for a configuration named "mavenResource". In the dependencies block I'll added a dependency like: "mavenResource(maven_coordinate)". That plugin will find all the dependencies contained in the mavenResource configuration and copy the associated maven artifact to the projects "src/main/resources" directory. As you can see, I don't want to apply that plugin until after the mavenResource configuration is added to that project, and the mavenResource dependencies are defined. Hence, I define my custom plugin the plugins block, and I apply it after the project dependencies have been defined. So, the concept that applying a plugin is old style and wrong is a misconception.
Some of you might wonder what it means to apply a plugin. It's pretty straightforward. It means that you call the plugin's apply function passing it the Gradle Project object for the project where the plugin is being applied. What the plugin does from there on is totally at the discretion of the plugin. Most commonly, the apply function usually creates some Gradle tasks and adds them to the Gradle build task dependency graph. When Gradle starts its execution phase, those tasks will get executed at the appropriate time in the build process. The plugin apply function can also do things like deferring some of it work until afterEvaluate. That's a way to allow other things in the build script to be setup even though they are defined later on in the buildscript. So, you might ask why I didn't do that trick in my custom plugin. What I've observed is that the next subproject starts processing after the root project finishes being evaluated. In my case, I needed the resource added before the next subproject began. So, there was a race condition, that I avoided by not doing the afterEvaluate technique and specifically applying the plugin once the things I needed setup was completed.

Gradle plugin for default properties

I am trying (and failing :) ) to create a gradle plugin that has a default set of versions for dependencies and can be overridden in the gradle.build file that is calling my plugin. Ideally something like the sudo-code below
MyDefaultPropertiesPlugin.groovy
project.versions.springBoot="1.0.0-RELEASE"
MyPlugin.groovy
project.apply plugin: MyDefaultPropertiesPlugin
compile("org.springframework.boot:spring-boot-starter-web:${project.versions.springBoot}")
build.gradle
versions.springBoot = "1.1.0-RELEASE"
project.apply plugin "my.plugin"
I attempted to do using extensions but ran into isssue's via the ordering when overriding. (versions doesnt exist)
I would greatly appreciate any advise on this, maven would be easy, but my gradle knowledge is still evolving :)
Thanks in advance for any insight!
Plugins have to defer accessing the build model until after build scripts have been evaluated. Easiest solution is to use project.afterEvaluate {}, but there are others. For more information, see answers to similar questions here or on http://forums.gradle.org.
Came up with a pretty workable if not perfect solution, I will update if i think of anything better, my gradle is at a learning level, so please commend if this can be improved.
This allows me to define a set of versions and clients to overwrite those versions with a simple property
MyDefaultVersionsPlugin.groovy
class MyDefaultVersionsPlugin implements Plugin<Project>{
project.extensions.create('versions', MyVersions, project)
}
class MyVersions{
String spring
String slf4j
public MyVersions (Project project){
spring = setVersion(project,'springVersion', 'x.x.x.x')
slf4j = setVersion(project,'slf4jVersion', 'x.x.x.x')
}
private static String setVersion(Project project, String name, String version){
if(project.hasProperties(name)){
return project.getProperties().get(name)
}
else {
return version
}
}
}
MyPlugin.groovy
project.apply plugin: MyDefaultVersionsPlugin
compile("org.springframework.boot:spring-boot-starter-web:${project.versions.spring}")
build.gradle
buildscript { ext { springVersion = 'x.x.x.x'} }

Resources