gradle-best-practices icon indicating copy to clipboard operation
gradle-best-practices copied to clipboard

Explain "Don't use Kotlin lambdas in your public API"

Open TWiStErRob opened this issue 3 years ago • 2 comments

I've been reading your guide, pretty cool summary. I got hung up on this suggestion, could you please expand on it a bit more or link to some resources?

Don't use Kotlin lambdas in your public API

Why is that? What will break?

Gradle enhances the bytecode at runtime to provide a nicer DSL experience for users of your plugin.

That's great, but what if my users are using includeBuild(gradle/plugins), at that point my plugin will be a normal dependency and Gradle doesn't provide a nice DSL magic transformation. Any suggestion to support both?

TWiStErRob avatar Sep 16 '22 15:09 TWiStErRob

Don't use Kotlin lambdas in your public API

I think this should go with

Don't use Groovy closures in your public API

The rationale is that one is pretty much unusable from the other language - also both from Java.

When you use Action<T>, it stays idiomatic in plugins implemented in plain Java, Groovy or Kotlin.

In the Groovy DSL (build scripts and precompiled scripts), there's some bytecode magic to also add Closure based overloads mostly for backwards compatibility.

In the Kotlin DSL (build scripts, precompiled scripts and .kt files in source sets with the kotlin-dsl applied), Action<T> is declared as a "lambda with receiver" equivalent to T.() -> Unit in order to remove the need to use it..


@TWiStErRob, if your users are using includeBuild(gradle/plugins) they should get what Gradle provides according to the following, otherwise it's a bug

eskatos avatar Sep 23 '22 13:09 eskatos

@eskatos Ah, thanks, that's some proper magic there 🙇🏻‍♂️!!!

Sorry, I confused two of my projects (or in some places I didn't apply kotlin-dsl). I double-checked now and I see it actually working in practice:

  • In one project I have includeBuild(gradle/plugins) and in gradle/plugins/build.gradle.kts have implementation(...) dependencies on Detekt Gradle Plugin, and then it defines plugins that configure Detekt reports. This fun reports only defines Action<T> overload, yet the usage compiles as if it was T.() overload. This works fine as you described.
  • In another project (also defining plugins) I have a standard java-gradle-plugin + kotlin(jvm) subproject that pulls in the same dependency and if I put the same code in there, I have to use it:
    project.tasks.withType<Detekt>().configureEach {
    	it.reports {
    		it.html.required.set(true) // human
    		it.txt.required.set(true) // console
    	}
    }
    
    otherwise it doesn't compile. This is what I was referring to. So the answer here is that I need to apply kotlin-dsl to these modules to get the magic of @HasImplicitReceiver interface Action<T>.

It was always confusing not being able to use them the same way, thanks for clarifying! (Now I understand some IntellIJ errors I saw related to this when sync was broken! TIL)

TWiStErRob avatar Sep 23 '22 14:09 TWiStErRob