Hilt Injection in Companion Objects
Asked Answered
D

1

10

I am new to Hilt/Dagger and I have not found a good example of injecting in a companion object. Here is my singleton and data class below. I am trying to use the TargetNumber manager in the create() function. I am looking for syntax help and explaination of why.

@InstallIn(SingletonComponent::class)
@Module
data class TargetNumberManager(
    val maxNumber: Int = 3,
    val prefix: String = "ZZ"
) : ITargetNumberManager {
    private var currentNumber = 0

    @Singleton
    @Provides
    override fun getNextTargetNumber(): String {
        val targetNumberBuilder: StringBuilder = java.lang.StringBuilder()

        targetNumberBuilder.append(prefix)

        val lengthOfNumber = currentNumber.toString().length

        for (i in lengthOfNumber..maxNumber step 1) {
            targetNumberBuilder.append(0)
        }

        targetNumberBuilder.append(currentNumber++)

        return targetNumberBuilder.toString()
    }
}

// Data Class

data class Target(
    @PrimaryKey
    val targetNumber: String,
    val targetType: TargetType?,
    val numOfElement: Int?,
    val location: Coordinate?
) : java.io.Serializable {

    @AndroidEntryPoint
    companion object {
        fun create(): Target {
            @Inject var targetNumberManager : TargetNumberManager
            val nextNumber = targetNumberManager.getNextTargetNumber()
            return Target(nextNumber, null, null, null)
        }
    }
}
Dialyser answered 8/2, 2021 at 13:51 Comment(0)
O
3

It seems like what you want is to ask Dagger to create an instance of an object. Let me explain how can you do that with EntryPoints.

My Notes on your Dagger Module

In your class TargetNumberManager:

  • I would use plain Kotlin classes for Dagger modules and avoid using interfaces or data classes. Because you won't be creating an instance of your module: Dagger will create it.
  • Avoid using variables in provides functions (such as getNextTargetNumber) because you are providing a dependency that is marked as @Singleton: this means Dagger will only call your getNextTargetNumber() function 1 time. It's better to separate business logic (such as counting into managers, use cases etc.) and dependency injection code (such as provides functions into Dagger modules).

You can separate that counting logic and Dagger module like this:

@Singleton
data class TargetNumberManager @Inject constructor(
    val maxNumber: Int = 3,
    val prefix: String = "ZZ"
) : ITargetNumberManager {
    private var currentNumber = 0

    override fun getNextTargetNumber(): String {
        val targetNumberBuilder: StringBuilder = java.lang.StringBuilder()

        targetNumberBuilder.append(prefix)

        val lengthOfNumber = currentNumber.toString().length

        for (i in lengthOfNumber..maxNumber step 1) {
            targetNumberBuilder.append(0)
        }

        targetNumberBuilder.append(currentNumber++)

        return targetNumberBuilder.toString()
    }
}

Note that:

  • I removed @InstallIn(SingletonComponent::class) and @Module annotations. This means this class is not a module anymore. Because TargetNumberManager looks like an implementation of a business logic and it's not related with dependency injection.
  • I added @Inject annotation to the constructor. This means Dagger will instantiate this class. In order for Dagger to create an instance of TargetNumberManager, it needs to find the dependencies of TargetNumberManager which are maxNumber and prefix variables. You need to provide them too.

For now we can extract them to constants:

@Singleton
data class TargetNumberManager @Inject constructor() : ITargetNumberManager {
    private var currentNumber = 0

    override fun getNextTargetNumber(): String {
        val targetNumberBuilder: StringBuilder = java.lang.StringBuilder()

        targetNumberBuilder.append(PREFIX)

        val lengthOfNumber = currentNumber.toString().length

        for (i in lengthOfNumber..MAX_NUMBER step 1) {
            targetNumberBuilder.append(0)
        }

        targetNumberBuilder.append(currentNumber++)

        return targetNumberBuilder.toString()
    }
    
    companion object {
        private const val MAX_NUMBER: Int = 3
        private const val PREFIX: String = "ZZ"
    }
}

With these changes in TargetNumberManager Dagger can create it when you want to inject it somewhere. It's marked as Singleton so Dagger will provide the same instance whenever you inject it.

Dagger needs to know which implementation you want to use, when you try to inject an instance of ITargetNumberManager. Because you have an interface and an implementation, but there may be other implementations of the same interface, so we need to tell Dagger which one use. We do that by using @Binds annotation. @Binds methods must be either abstract or inside an interface. Here I used abstract class for Dagger module:

@InstallIn(SingletonComponent::class)
@Module
abstract class MyModule {

    @Binds
    abstract fun bindTargetNumberManager(impl: TargetNumberManager): ITargetNumberManager
}

Now Dagger knows which implementation to inject when you ask ITargetNumberManager. Note that I didn't put @Singleton here because the implementation class TargetNumberManager is already marked as @Singleton. So it will be Singleton whenever you inject the type TargetNumberManager AND ITargetNumberManager.

How to get an instance from Dagger with EntryPoints

  • In plain Dagger you create component interfaces. Hilt includes everything from plain Dagger, so there are also components in Hilt. In Hilt there are EntryPoint interfaces. Hilt discovers these interfaces and makes the component interface extend these EntryPoint interfaces. Using an EntryPoint is basically as if you added functions to component interfaces. See: https://dagger.dev/hilt/entry-points
  • Your component interface and your EntryPoint can include a function that returns an instance of your object. By adding a function that has a return type of your dependency, you are simply asking Dagger to create an instance for you.

A typical EntryPoint looks like below:

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
    fun getTargetNumberManager(): ITargetNumberManager
}

When you need ITargetNumberManager: get an instance of this MyEntryPoint then get an instance of ITargetNumberManager:

val myEntryPoint: MyEntryPoint = EntryPoints.get(applicationContext, MyEntryPoint::class.java)
val targetNumberManager: ITargetNumberManager = myEntryPoint.getTargetNumberManager()

That's it! If you want to do that in your Target class:

data class Target(
    @PrimaryKey
    val targetNumber: String,
    val targetType: TargetType?,
    val numOfElement: Int?,
    val location: Coordinate?
) : java.io.Serializable {

    companion object {
        fun create(applicationContext: Context): Target {
            val myEntryPoint: MyEntryPoint = EntryPoints.get(applicationContext, MyEntryPoint::class.java)
            val targetNumberManager: ITargetNumberManager = myEntryPoint.getTargetNumberManager()
            val nextNumber = targetNumberManager.getNextTargetNumber()
            return Target(nextNumber, null, null, null)
        }
    }
}

Hilt needs to know the Application Context because it keeps the SingletonComponent inside the Application class. So it will go there to get the component. After it gets the component, it will cast to your EntryPoint interface. Then you can call the functions you added into your EntryPoint.

My Other Notes:

  • AndroidEntryPoint is ONLY for Android entry points, such as: Activity, Fragment, View, Service, BroadcastReceiver. NOT for companion objects or other type of classes. See: https://dagger.dev/hilt/android-entry-point.html
  • @Inject cannot be used in variables inside functions. You can add @Inject to constructors instead.

I hope this helps. I tried to explain each point. Let me know if you have further questions!

Octo answered 22/5, 2023 at 12:59 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.