Bind list of objects using Guice + Kotlin
Asked Answered
O

3

9

I'm writing a JavaFX application in Kotlin with the following controller definition:

class MainController {

    @Inject private lateinit var componentDescriptors: List<ComponentDescriptor>
    /* More code goes here */

}

I'm using Guice for Dependency management. And I'm trying to inject the list of class instances loaded via java.util.ServiceLoader. My problem is to define a binding that will inject the list of loaded object instances into the declared field. I tried annotation based provisioning:

internal class MyModule: AbstractModule() {

    override fun configure() { }

    @Provides @Singleton
    fun bindComponentDescriptors(): List<ComponentDescriptor> = 
            ServiceLoader.load(ComponentDescriptor::class.java).toList()

}

and multibinding extension (switched List to Set in field definition of corse):

internal class MyModule: AbstractModule() {

    override fun configure() {
        val componentDescriptorBinder = Multibinder.newSetBinder(binder(), ComponentDescriptor::class.java)
        ServiceLoader.load(ComponentDescriptor::class.java).forEach {
            componentDescriptorBinder.addBinding().toInstance(it)
        }
    }

}

but both of these approaches leads to the same error:

No implementation for java.util.List<? extends simpleApp.ComponentDescriptor> was bound.
  while locating java.util.List<? extends simpleApp.ComponentDescriptor>
    for field at simpleApp.MainController.componentDescryptors(MainController.kt:6)
  while locating simpleApp.MainController

1 error
    at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:1042)
    at com.google.inject.internal.InjectorImpl.getProvider(InjectorImpl.java:1001)
    at com.google.inject.internal.InjectorImpl.getInstance(InjectorImpl.java:1051)
    at com.gluonhq.ignite.guice.GuiceContext.getInstance(GuiceContext.java:46)
    at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:929)
    at javafx.fxml.FXMLLoader$InstanceDeclarationElement.processAttribute(FXMLLoader.java:971)
    at javafx.fxml.FXMLLoader$Element.processStartElement(FXMLLoader.java:220)
    at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:744)
    at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2707)
    at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2527)
    ... 12 more

I'm starting to suspect that it somehow related to Kotlin gerenic variance and Guice strict type checking. But I don't know how to declare the binding so Guice will know what to inject into this field.

Oversupply answered 14/4, 2016 at 13:49 Comment(0)
S
19

Yes, it happens because of variance but there's a way to make it work.

class MainController {
    @JvmSuppressWildcards
    @Inject
    private lateinit var componentDescriptors: List<ComponentDescriptor>    
}

By default Kotlin generates List<? extends ComponentDescriptor> signature for the componentDescriptors field. The @JvmSuppressWildcards makes it generate a simple parameterized signature List<ComponentDescriptor>.

Sybilla answered 14/4, 2016 at 14:2 Comment(4)
Is there a way to make this work for constructor injecting? I tried putting the annotation before the constructor parameter where I want to inject the Set but it doesn't work. It does work when I put the @JvmSuppressWildcards annotation on the class, but I don't want to do this for every class...Fisherman
@JanWytze Have you tried applying this annotation to a constructor?Sybilla
This annotation is not applicable to target 'constructor'Fisherman
I got it to work putting the annotation before the constructor param rather than before the constructor keyword. class MainController @Inject constructor(val components: Set<@JvmSuppressWildcards ComponentDescriptor>) {}Solingen
R
3

@Michael gives the correct answer and explanation. Here's an example of one strategy for unit testing a Set multibinding for those that like to test their modules:

class MyModuleTest {

  @JvmSuppressWildcards
  @Inject
  private lateinit var myTypes: Set<MyType>

  @Before fun before() {
    val injector = Guice.createInjector(MyModule())
    injector.injectMembers(this)
  }

  @Test fun multibindings() {
    assertNotNull(myTypes)
    assertTrue(myTypes.iterator().next() is MyType)
  }
}
Red answered 19/9, 2016 at 15:32 Comment(0)
T
3

@Michael comment is working. If you want to do the injection in constructor, you need do something like

class MainController @Inject consturctor(
    private var componentDescriptors: List<@JvmSuppressWildcards ComponentDescriptor>  
) {}
Towner answered 10/12, 2018 at 22:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.