Benjamin
Benjamin

Reputation: 7368

Provide function dependency using Dagger 2

I would like to provide a function as dependency using Dagger 2:

@Module
class DatabaseModule {

    @Provides
    @Singleton
    fun provideDatabase(application: Application, betaFilter: (BetaFilterable) -> Boolean): Database {
        return Database(application, BuildConfig.VERSION_CODE, betaFilter)
    }

    @Provides
    @Suppress("ConstantConditionIf")
    fun provideBetaFiler(): (BetaFilterable) -> Boolean {
        return if (BuildConfig.FLAVOR_audience == "regular") {
            { it.betaOnly.not() }
        } else {
            { true }
        }
    }

}

Unfortunately, it does not seem to work:

[dagger.android.AndroidInjector.inject(T)] kotlin.jvm.functions.Function1<? 
super com.app.data.BetaFilterable,java.lang.Boolean> 
cannot be provided without an @Provides-annotated method.

What am I missing here?

Upvotes: 16

Views: 4885

Answers (3)

Julian A.
Julian A.

Reputation: 11470

Yes, this can be done in Kotlin.

You need to add @JvmSuppressWildcards at the injection site to ensure the signature matches. (source)

I wrote the following to verify it:

import dagger.Component
import dagger.Module
import dagger.Provides
import javax.inject.Singleton

class G constructor(val function: Function1<Int, Boolean>)

@Singleton
@Module
class ModuleB {
    @Provides
    fun intToBoolean(): (Int) -> Boolean {
        return { it == 2 }
    }
    @JvmSuppressWildcards
    @Provides fun g(intToBoolean: (Int) -> Boolean): G {
        return G(intToBoolean)
    }
}

@Singleton
@Component(modules = [ModuleB::class])
interface ComponentB {
    fun g(): G
}

val componentB = DaggerComponentB.create()
val g = componentB.g()
println(g.function(2)) // true
println(g.function(3)) // false

Background: Examining @Kiskae's response, it seems the problem is that a parameter of function type in Kotlin becomes contravariant on its own parameter types when the code is converted to Java bytecode. If this doesn't make sense to you, don't worry. It's not necessary to understand it to use the technique I show above.

Upvotes: 24

Benjamin
Benjamin

Reputation: 7368

As pointed out in Kiskae's answer, it is not possible to make this work using kotlin. You can make it work using java though:

@Module
public class DatabaseModuleJava {

    @Provides
    @Singleton
    public Database provideDatabase(Application application, Function1<BetaFilterable, Boolean> betaFilter) {
        return new Database(application, BuildConfig.VERSION_CODE, betaFilter);
    }

    @Provides
    @Singleton
    @SuppressWarnings("ConstantConditions")
    public Function1<BetaFilterable, Boolean> provideBetaFiler() {
        if (BuildConfig.FLAVOR_audience.equals("regular")) {
            return betaFilterable -> !betaFilterable.getBetaOnly();
        } else {
            return betaFilterable -> true;
        }
    }

}

Upvotes: 3

Kiskae
Kiskae

Reputation: 25603

It does not work because in order to allow functions with supertypes to be called in place of the lambda ((Any) -> Boolean can be used as well as (BetaFilterable) -> Boolean) a function as a parameter generates bytecode to allow this.

The following code:

object Thing

fun provide(): (Thing) -> Boolean {
    TODO()
}

fun requires(func: (Thing) -> Boolean) {
    TODO()
}

Results in the following signatures:

signature ()Lkotlin/jvm/functions/Function1<LThing;Ljava/lang/Boolean;>;

declaration: kotlin.jvm.functions.Function1<Thing, java.lang.Boolean> provide()

signature (Lkotlin/jvm/functions/Function1<-LThing;Ljava/lang/Boolean;>;)V

declaration: void requires(kotlin.jvm.functions.Function1<? super Thing, java.lang.Boolean>)

The subtle difference between -LThing (? super Thing) and LThing (Thing) makes the types incompatible for Dagger.

I don't believe it is possible to make this work, you will need to define a separate interface that does not have the same ? super/? extends properties as Function1 has.

Upvotes: 3

Related Questions