Skip to content
Daniel Molinero edited this page Aug 29, 2019 · 9 revisions

Toothpick 3.0 comes with KTP: a Kotlin friendly API.

Field injection

In Kotlin, constructor injection is the preferred way to inject your dependencies. However, that is not always possible. For instance, we have no control over the creations of Activities and Fragments, as they are handled by the Android framework directly. For those situations, we provide a new Kotlin API that uses Property Delegation to enable Field Injection.

In the following example, we show how to inject a single dependency using eager injection.

class MyActivity : AppCompatActivity() {

    val myDependency: MyDependency by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        KTP.openScopes(Application, this)
                .installModules(myModule())
                .inject(this)
    }
}

Eager injection means that myDependency will be injected/created as soon as you call inject(this). We also provide the option to do lazy and provider dependencies:

class MyActivity : AppCompatActivity() {

    // This dependency instance will be created the first time you access
    // to it and the same one will be used for the following access 
    val myLazyDependency: MyDependency by lazy()

    // A new instance of this dependency will be used every time you access
    // to it. Equivalent to: 
    // val myProviderDependency get() = MyDependency()
    val myProviderDependency: MyDependency by provider()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        KTP.openScopes(Application, this)
                .installModules(myModule())
                .inject(this)
    }
}

Probably, you have noticed that to perform the injections, we use KTP instead of Toothpick. KTP provides the same functionality as Toothpick but in a Kotlin friendly way. On your java classes you will still use Toothpick.

Regarding compatibility, KTP is 100% compatible with your java dependencies, so on the above examples, MyDependency could be either a Java or a Kotlin class.

It is also possible to use these property delegate methods with named and qualified injections:

    val myDependency: MyDependency by inject("name")
    val myLazyDependency: MyDependency by lazy(MyQualifierAnnotation::class)

Constructor injection

For the rest of your classes, where constructor injection is used, we have introduced a new annotation that makes the class injectable and get its depencies injected with minor changes to your code:

@InjectConstructor
class MyDependency(val myHelper: MyHelper) {  
    // myHelper will be injected by Toothpick
    ...
}

By annotating your class with @InjectConstructor, the class will be visible for Toothpick and the only available constructor will be used for creation/injection.

The above code is equivalent to:

class MyDependency @Inject constructor(val myHelper: MyHelper) {  
    ...
}

If your class contains more than one constructor, you will have to use the latter in order to specify which constructor should Toothpick use.

It is also possible to inject lazy and provider using constructor injection:

@InjectConstructor
class MyDependency(val myHelper: Lazy<MyHelper>, val myOtherHelper: Provider<MyOtherHelper>) {  

    fun doSomething() {
        myHelper.get().helpMeWithSomething()
        myOtherHelper.get().helpMeWithSomethingElse()
    }
}

However, on Kotlin it is not possible to do constructor injection with delegated getter, which means that you will need use use the Lazy and Provider classes and call get() to retrieve the instance. The same as with vanilla Toothpick.

Modules & Bindings

KTP also introduces a new binding API to take advantage of the Kotlin features:

// new way to create a inlined Module
module {  
    // equivalent to bind(MyDependency::class.java)
    bind<MyDependency>()

    // equivalent to bind(IDependency::class.java).to(MyDependency::class.java)
    bind<IDependency>().toClass<MyDependency>()

    // equivalent to bind(IDependency::class.java).toInstance(MyDependency())
    bind<IDependency>().toInstance { MyDependency() }

    // equivalent to bind(IDependency::class.java).toProvider(MyDependency::class.java)
    bind<IDependency>().toProvider(MyDependencyProvider::class)

    // equivalent to bind(IDependency::class.java).toProviderInstance(MyDependencyProvider())
    bind<IDependency>().toProviderInstance(MyDependencyProvider())
    bind<IDependency>().toProviderInstance { 
            // my provider defined in place
            MyDependency()
        }
    
    // And we can still apply all the name, scope and retention modifiers to the above bindings
    bind<IDependency>().withName("name").toClass<MyDependency>()
    bind<IDependency>().withName(QualifierName::class).toClass<MyDependency>()
    
    bind<IDependency>().toClass<MyDependency>().singleton()
    bind<IDependency>().toClass<MyDependency>().singleton().releasable()

    bind<IDependency>().toProvider(MyDependency::class).providesSingleton()
    bind<IDependency>().toProvider(MyDependency::class).providesSingleton().providesReleasable()
    bind<IDependency>().toProvider(MyDependency::class).providesSingleton().providesReleasable().singleton()
}

As you can see on the above example, creating inlined modules now it is easier:

KTP.openScopes(Application, this)
        .installModules(module {
            bind...
         })
        .inject(this)

Scope

For those who like Service Locator style, the Scope class has been extended in order to be able to do the following:

val myDependency: MyDependency = scope.getInstance()
val myDependency: Lazy<MyDependency> = scope.getLazy()
val myDependency: Provider<MyDependency> = scope.getProvider()

The scope here would have to be passed as a constructor parameter property.

Lifecycle & ViewModels

In Toothpick 3, we have introduced have few extensions to make Scopes lifecycle and ViewModels aware. For Java, they are static util methods, but for Kotlin they are extension functions:

// Close Scope when the Activity/Fragment is destroyed
KTP.openScopes(this)
        .closeOnDestroy(this)
        .inject(this)

// Close Scope when the ViewModel attached to the Activity/Fragment is cleared
KTP.openScopes(this)
        .closeOnViewModelCleared(this)
        .inject(this)

// Create binding on the scope to be able to inject the ViewModel
KTP.openScopes(<name>)
        .installViewModelBinding(<BackpackViewModel>this)
        .inject(this)

Or here an advanced example using the new configuration lambda API:

KTP.openScopes(ApplicationScope::class.java)
        .openSubScope(ViewModelScope::class.java) { scope: Scope ->
            scope.installViewModelBinding<BackpackViewModel>(this)
                    .closeOnViewModelCleared(this)
                    .installModules(module {
                        bind<Backpack>().singleton()
                    })
        }
        .openSubScope(this)
        .installModules(module {
            bind<IBackpackAdapter>().toClass<BackpackAdapter>()
        })
        .closeOnDestroy(this)
        .inject(this)

Incremental annotation processing

As the rest of the Toothpick 3 project, KTP is compliant with incremental annotation processing when using kapt.

Links