Skip to content

kotlin_provide

klee0kai edited this page Dec 17, 2023 · 2 revisions

Providing

To use objects from everywhere, you need to use it in DI. In order for DI to provide objects, their generation must first be described.

As described above, all objects must be declared in a module in order to provide them.

@Module
abstract class SevenPlanetModule {

    open fun earth(): Earth {
        return Earth()
    }

}

In many cases, a simple constructor call can be ignored. The annotation processor will independently find and call the constructor for the object, taking into account the parameters used.

@Module
abstract class SevenPlanetModule {

    abstract fun solarSystem(sun: Sun, earth: Earth): SolarSystem

}

Note that the order of the arguments is important to the object providing function. If the argument set does not match the constructor, the library will throw an error.

If method arguments can specify dependencies for creating an object. Dependencies must be specified via arguments so that they are also cached in DI. Dependent objects must also be provided by the DI component.

You can also provide parameterized types, but you cannot provide primitives and their object implementations.

@Module
abstract class SevenPlanetModule {

    abstract fun usb_miniusb(): Wire<Usb, MiniUsb>
}

Qualifiers

DI has tools for differentiating the provision of objects of the same type among themselves. The easiest way to do this is either by using the existing @Named qualifier or by declaring your own.

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class MyQualifier

You can also declare qualifiers with additional properties and attributes. This will allow you to combine qualifiers into groups.

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class ThreadQualifier(val type: ThreadType = ThreadType.Main) {

    enum class ThreadType {
        Main,
        Default,
        IO
    }

}

Next, it is enough to indicate the qualification when presenting the object.

@Module
open class ThreadsModule {

    @ThreadQualifier(type = ThreadQualifier.ThreadType.Main)
    @Provide(cache = Provide.CacheType.Strong)
    open fun mainThreadPoolExecutor(): ThreadPoolExecutor {
        return ThreadPoolExecutor(
            0,
            1,
            0,
            TimeUnit.MILLISECONDS,
            LinkedBlockingDeque()
        ) { runnable: Runnable? -> Thread(runnable) }
    }

}

You can now choose which dependencies to use for your component using an argument qualifier.

@Module
abstract class PresentersModule {

    @MyQualifier
    abstract fun provideFeaturePresenter(
        @ThreadQualifier(type = ThreadQualifier.ThreadType.Main) executor: ThreadPoolExecutor
    ): FeaturePresenter

}

Qualifiers can also be used for injection fields.

Identifiers

In some cases, there may not be enough qualifiers to separate objects, for example, the number of instances of the same class may be undefined. For such cases, stone offers a provider ID tool. Identifiers allow you to generate objects of the same type several times, as well as cache them using a unique key - identifier.

@Module
abstract class PresentersModule {

    abstract fun provideFeaturePresenter(
        screenId: ScreenId,
        loginId: LoginId
    ): FeaturePresenter

}

The code above, for example, allows you to reuse FeaturePresenter with the same values screenId and loginId and distinguish between different instances based on these keys.

To use classifiers, you must declare them in the @Component annotation.

@Component(identifiers = [ScreenId::class, LoginId::class])
interface AppComponent {

    fun presentersModule(): PresentersModule

}

In this case, the DI component independently determines what is a dependency and what is an identifier when resolving dependencies. Qualifiers cannot be provided. Additionally, they must override hashCode and equals to be used as a key.

data class LoginId(val tag: String)

data class ScreenId(val tag: String)

Once you have defined all the identifiers for your DI component, you can inject all your objects with an identifier. Moreover, the number of identifiers during injection is not limited in any way; for each matching dependency, its own identifier will be transferred.

@Component(identifiers = [ScreenId::class, LoginId::class])
interface AppComponent {

    fun inject(screen: FeatureScreen, loginId: LoginId, screenId: ScreenId)

}

Identifiers can also be explicitly specified and used in methods for providing objects with dependencies.

@Component(identifiers = [ScreenId::class, LoginId::class])
interface AppComponent {

    fun featurePresenter(loginId: LoginId, screenId: ScreenId): FeaturePresenter

}

Be careful, if there is no identifier during injection, it will simply be replaced with null. You cannot use multiple identifiers of the same type in one injection location. Each identifier must have its own type. When providing with dependencies, the identifier is also passed to the dependent classes to provide a unique instance of it.

Binding

Some objects cannot be created in the application; they may be provided by external libraries. They can be linked in the Library and provided in the project via DI.

You can declare a binding object using a providing method in a module using the @BindInstance annotation.

@Module
interface SunSystemModule {

    @BindInstance(cache = BindInstance.CacheType.Weak)
    fun sun(sun: Sun? = null): Sun

}

An annotation can indicate the following caching methods

  • Weak - caching weak links
  • Soft - caching soft link
  • Strong - caching strong link

For ease of use, you can also expose the object representation in the component directly.

@Component
interface GodWorkspaceComponent {

    fun sunSystem(): SunSystemModule

    fun sun(): Sun

}

Then you can use DI.sunSystem().sun(null) instead of the full entry shorthand DI.sun(). Binding is initialized by calling DI.sunSystem().sun( new Sun() ). However, calling an identical method with a null parameter does not nullify the binding, but only provides the value. The next entry will be working

val DI = Stone.createComponent(GodWorkspaceComponent::class.java)
val sun = Sun()
DI.sunSystem().sun(sun)

val sunFromDI = DI.sunSystem().sun(null)
println("sunFromDI $sunFromDI")

For a more abbreviated notation, you can declare the binding without declaring it in the module. It is enough to make the same entry completely in the component.

@Component
interface SunSystemComponent {

    @BindInstance
    fun sun(sun: Sun? = null): Sun

}

The rule for using the null parameter is also preserved here

val DI = Stone.createComponent(SunSystemComponent::class.java)
val sun = Sun()
DI.sun(sun)

val sunFromDI = DI.sun()
println("sunFromDI $sunFromDI")

Resetting object binding can be done through cache control.

As of version 1.0.3, binding does not support either qualifiers or identifiers.

Inject

In many cases, constantly turning to DI to get some object becomes inconvenient, especially if there are several such objects in the class. A good way in this case would be to use injecting objects from the component. By declaring an injection method in a component without any annotations.

@Component(identifiers = [ScreenId::class, LoginId::class])
interface AppComponent {

    fun inject(screen: FeatureScreen)

}

You can inject variables and methods inside this class. The methods are called once, when the injection is called.

class FeatureScreen {

    @Inject
    lateinit var presenter: FeaturePresenter

    @Inject
    fun init(presenter: FeaturePresenter) {
    }

    fun start() {
        val DI = Stone.createComponent(AppComponent::class.java)
        DI.inject(this)
    }
}

You can also use identifiers as arguments for injection. This allows you to build your own unique chain of objects for each new screen according to the identifier key.

@Component(identifiers = [ScreenId::class, LoginId::class])
interface AppComponent {

    fun inject(screen: FeatureScreen, owner: StoneLifeCycleOwner, loginId: LoginId, screenId: ScreenId)

}

In the example above, we injected objects into featureScreen, taking into account the identifiers loginId and screenId, and also set up protection against the destruction of objects when re-creating featureScreen using the life cycle control parameter lifeCycleOwner.

Clone this wiki locally