Search code examples
kotlingradlegradle-kotlin-dsl

Gradle: cannot create ExtensiblePolymorphicDomainObjectContainer


Suppose I have an extension interface like this:

interface MyExtension {
    abstract val container: ExtensiblePolymorphicDomainObjectContainer<MyBaseClass>
}

When I try to create an instance of this extension via this:

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create<MyExtension>("myExtension")
    }
}

It seems that Gradle (at least as of version 7.6.1) will have trouble applying a MyPlugin due to being unable to create the extension, in turn due to being able to implement the container property properly.

Is it a limitation of Gradle or the Kotlin DSL requiring a workaround (eg. making MyExtension an abstract class and injecting an ObjectFactory specifically), or am I doing something wrong?


Solution

  • Gradle can only create specific types of properties on managed types

    These are

    • Property<T>
    • RegularFileProperty
    • DirectoryProperty
    • ListProperty<T>
    • SetProperty<T>
    • MapProperty<K, V>
    • ConfigurableFileCollection
    • ConfigurableFileTree
    • DomainObjectSet<T>
    • NamedDomainObjectContainer<T>

    In Gradle v8.2, ExtensiblePolymorphicDomainObjectContainer<T> will be supported. Until then they must be created manually using ObjectFactory.

    One way of doing this is by injecting the ObjectFactory into the constructor:

    import javax.inject.Inject
    import org.gradle.api.*
    imports org.gradle.kotlin.dsl.*
    
    abstract class MyExtension @Inject constructor(
      private val objects: ObjectFactory,
    ) {
      val container: ExtensiblePolymorphicDomainObjectContainer<MyBaseClass> =
        objects.polymorphicDomainObjectContainer()
    }
    

    Pro tip: Kotlin DSL accessors for NamedDomainObjectContainers

    Note that even though Gradle will automatically create NamedDomainObjectContainer properties it's usually a good idea to create one manually and add it as an extension.

    abstract class MyExtension @Inject constructor(
      private val objects: ObjectFactory,
    ) : ExtensionAware {
    // *all* types that Gradle instantiates are automatically ExtensionAware,
    // but adding the interface explicitly helps with IDE hints
      
      val namedContainer: NamedDomainObjectContainer<MyBaseClass> =
        objects.domainObjectContainer(MyBaseClass::class).also {
          extensions.add("namedContainer", it)
        }
    }
    

    This is verbose and weird - so why bother?

    By adding namedContainer as an extension, Gradle will automatically generate some nice type-safe Kotlin DSL accessors for any element that is added.

    So, if in your plugin you add an element.

    abstract class MyPlugin : Plugin<Project> {
      override fun apply(project: Project) {
        val extension = project.extensions.create<MyExtension>("myExtension")
        extension.namedContainer.register("anElement") { ... }
      }
    }
    

    In any build script, there will be an easy way to access the element in a type-safe and IDE friendly way

    // build.gradle.kts
    plugins {
      id("my.plugin")
    }
    
    myExtension {
      namedContainer.anElement { // auto generated accessor
      }
    }