Kapsule
Minimalist dependency injection library for Kotlin.
Why create another dependency injection library? Here are the objectives pursued by Kapsule:
- Simple features that most projects will have use for
- Alternative for projects whose dependency injection needs are quite basic
- Keep the method count to a minimum
- Dependency injection shouldn't take thousands of methods to implement
- No annotation processing
- No need for
lateinit
on properties and they can be private and read-only
- No need for
- No magic, keep everything as a hard reference
- Reading code is easier when you can click through all the references in your IDE
- Utilize the power of Kotlin
- Use language features to simplify code instead of focusing on Java compatibility
To accomplish all of these, Kapsule is based on delegation and delegated properties.
Table of Contents
Getting Started
Download
To use Kapsule in your project, include it as a dependency.
Using Gradle:
dependencies {
compile "net.gouline.kapsule:kapsule-core:1.1"
}
Or Maven:
<dependency>
<groupId>net.gouline.kapsule</groupId>
<artifactId>kapsule-core</artifactId>
<version>1.1</version>
</dependency>
Create a Module
Define a module to provide the injected values.
This can be any Kotlin class, so feel free to initialize properties however you like (including lazy
expressions and custom getters).
class Module {
val name = "SomeName"
val manager get() = Manager()
}
Our simple example provides the same instance of name
and a new instance of Manager
for every property that requires it.
Store Module Instance
Store the root module in your application context (this will depend on your framework).
On Android, you would use the Application
instance for this. Don't forget to declare the CustomApplication
class in AndroidManifest.xml
.
class CustomApplication : Application() {
private var module = Module()
companion object {
fun module(context: Context) = (context.applicationContext as Application).module
}
}
The static function module()
is how you will access the stored module from activities and fragments.
Inject Properties
Now the injection target needs to be adjusted as follows:
- Implement
Inject<Module>
with the module you're injecting - Declare the dependencies using
required
(oroptional
) references - Retrieve the module instance from the application context
- Call
inject()
on the module to initialise the values
Looking at the Android example, let's say you have a class ExampleActivity
that needs these injected values. The function passed for each declaration retrieves the value from Module
that the given property expects.
class ExampleActivity : AppCompatActivity(), Injects<Module> {
private val name by required { name }
private val manager by required { manager }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inject(CustomApplication.module(this))
}
}
That's it, properties name
and manager
can now be used!
Advanced Setup
The steps above show the most basic setup, which can be extended for more advanced use cases.
Module Implementations
The basic setup uses one module called Module
, but what if you need another implementation that returns stub values for tests?
You can define an interface and provide two different implementations:
interface Module {
val name: String
val manager: Manager
}
The module used before can be the main implementation:
class MainModule : Module {
override val name = "SomeName"
override val manager get() = Manager()
}
And another TestModule
can return the stub values:
class TestModule : Module {
override val name = "SomeTestName"
override val manager get() = TestManager()
}
Your injection routine stays the same, except now Module
is a generic interface, you just have to provide the appropriate module through your application context (i.e. what is depicted as Application.module
in the example).
Multiple Modules
You may also want to define multiple modules (with some logical separation) combined into one interface used for injection. Let's say you have a CoffeeModule
as follows:
interface CoffeeModule {
val coffeeType: String
}
And another TeaModule
:
interface TeaModule {
val teaType: String
}
As described in the previous section, you can have one or more implementations for each module (omitted for brevity) that you can combine them into one Module
:
class Module(coffee: CoffeeModule, tea: TeaModule) :
CoffeeModule by coffee, TeaModule by tea
Now your Module
contains all the properties and functions of CoffeeModule
and TeaModule
courtesy of Kotlin's delegation support.
When you're instantiating the global module (stored in your application context), you can provide the required implementation for each submodule:
class Application {
val module = Module(
coffee = MainCoffeeModule() // or TestCoffeeModule()
tea = MainTeaModule() // or TestTeaModule()
)
}
Optional Delegates
So far you've only seen non-null values, but what happens if you need to inject a nullable value? You can use the optional
function on your Kapsule:
val firstName by required { firstName }
val lastName by optional { lastName }
Given both fields are strings, firstName
is String
, while lastName
is String?
.
Unlike non-null properties, nullable ones can be read even before injection (the former would throw KotlinNullPointerException
), they will just be null.
Variable Delegates
In most cases you would make the injected properties val
, however there's no reason it can't be a var
, which would allow you to reassign it before or after injection.
var firstName by required { firstName }
init {
firstName = "before"
kap.inject(Application.module)
firstName = "after"
}
Note that any delegates can be injected repeatedly, regardless of whether they're val
or var
, because the initialized value is contained within the delegate and it's a nullable var
.
Transitive Dependencies
Consider UserDao
and an authenticator Auth
that depends on it. Except the former is provided by DataModule
, but the latter comes from LogicModule
.
class Module(
data: DataModule, logic: LogicModule) :
DataModule by data, LogicModule by logic
interface DataModule {
val userDao: UserDao
}
interface LogicModule {
val auth: Auth
}
This is where the transitive injection comes in, the UserDao
can be injected into the LogicModule
.
class MainDataModule : DataModule {
override val userDao get() = UserDao()
}
class MainLogicModule : LogicModule, Injects<Module> {
private val userDao by required { userDao }
override val auth get() = Auth(userDao)
}
Looks good, but it won't work without two modifications to Module
:
- It has to implement
HasModules
to suggest that it contains submodules HasModules
requires you to define the module instances
class Module(
data: DataModule, logic: LogicModule) :
DataModule by data, LogicModule by logic,
HasModules {
override val modules = setOf(data, logic)
}
Finally, when instantiating the module, you need to call transitive()
on the module to traverse the tree and inject the Module
into any submodules depending on it.
val module = Module(MainDataModule(), MainLogicModule()).transitive()
Note that circular dependencies are not supported, because resolution is performed iteratively and in a single pass. Take care in defining your dependency structure to avoid this situation.
Complex Dependencies
Traditionally, injected values are kept in the same structure as they are provided by the modules. However, considering that injection functions in Kapsule are essentially just future values that will become accessible after injection happened, you can do anything you would do outside of that function, inside.
Consider the example module implementations from the transitive dependencies section:
class MainDataModule : DataModule {
override val userDao get() = UserDao()
}
class MainLogicModule : LogicModule, Injects<Module> {
private val userDao by required { userDao }
override val auth get() = Auth(userDao)
}
You'll notice that the userDao
is being provided by the MainDataModule
as a new instance each time. What if you wanted to reuse the same instance through the whole life of the module? You would just assign it as a value, rather than return it from a custom getter.
class MainDataModule : DataModule {
override val userDao = UserDao()
}
But what about if you want to do the same with auth
in MainLogicModule
? You can't just assign Auth(userDao)
to auth
, because Auth()
constructor is called when the MainLogicModule
is instantiated and by that time the userDao
hasn't been injected yet (remember, that gets done in the transitive()
call). That means you need to instantiate auth
in the same future, when userDao
will become available.
class MainLogicModule : LogicModule, Injects<Module> {
override val auth by required { Auth(userDao) }
}
Notice that you no longer need the userDao
separately for that. Also note that required
/optional
choice now depends on what the return of the new function is, rather than whether or not userDao
by itself is.
You can do other stuff in your injection functions, like convert the injected value into something else that you need for the current context.
class ExampleActivity : AppCompatActivity(), Injects<Module> {
private val authHttpClient by required { auth.httpClient }
...
}
In the above example, we're not just injecting the auth
dependency from before, but also retrieving its httpClient
property.
While you're not technically limited by what you can do inside these injection functions, try not to overdo it. It's similar to data bindings, where you can perform calculations and other complex calls right in the template file, but you know you shouldn't.
Manual Injection
While the more convenient way to inject modules is by implementing the Injects<Module>
interface, you may want to split the injection of separate modules (e.g. for testing). This can be done by creating separate instances of Kapsule<Module>
and calling the injection methods on it.
class Screen {
private val kap = Kapsule<Module>()
private val name by kap.required { name }
private val manager by kap.required { manager }
init {
kap.inject(Application.module)
}
}
Samples
For sample projects using Kapsule, see the samples directory.
Javadocs
- Version 1.0
- Version 0.3
- Version 0.2
- Version 0.1
License
This project is licensed under the terms of the MIT license. See the LICENSE file.