Dependency Injection
Typewriter uses the Koin library internally to provide dependency injection.
Extensions can register components that other parts of the engine or extensions may depend on.
The module plugin scans your code for @Singleton and @Factory annotations and automatically registers them with Koin when the extension loads.
Registering Singletons
A component annotated with @Singleton is created once and shared across the entire application.
This can be a class or an object.
@Singleton
class ExampleService {
fun greet(player: Player) {
player.sendMessage("Hello ${player.name}")
}
}
Objects can be singletons as well and the object instance itself is registered.
@Singleton
object ExampleObject {
const val prefix: String = "[Typewriter]"
}
A top level function can also provide a singleton value:
@Singleton
fun providePrefix(): String = "[Typewriter]"
You can retrieve these values by implementing KoinComponent and using inject.
The KoinJavaComponent.get helper works in both Kotlin and Java classes.
class ExampleSingletonUsage : KoinComponent {
private val service: ExampleService by inject()
fun welcome(player: Player) {
service.greet(player)
// Or if you want to directly get the prefix inline you can do:
val prefix: String = KoinJavaComponent.get(String::class.java)
player.sendMessage(prefix)
}
}
Registering Factories
Classes or top level functions annotated with @Factory create a new instance every time they are requested.
Use a
factory when the dependency should not be shared across callers.
@Factory
class ExampleRunner(
// Parameters are automatically injected by Koin,
// make sure to register these types with @Singleton or @Factory
private val service: ExampleService,
) {
fun start(player: Player) {
service.greet(player)
}
}
A top level function can also declare a factory binding:
@Factory
fun createGson(): Gson = GsonBuilder().setPrettyPrinting().create()
Retrieving a factory produces a fresh instance each time.
class TrackerManager : KoinComponent {
// You can inject a factory like this,
// it will create a new instance every time you get the field
val exampleRunner: ExampleRunner by inject()
fun create(): ExampleRunner =
// Or you can get it from the Koin scope directly
KoinJavaComponent.get(ExampleObject::class.java)
}
Named Bindings
You can assign a name to any @Factory or @Singleton component using the @Named annotation.
This allows multiple bindings of the same type.
@Factory
@Named("exampleParser")
fun provideParser(): Gson = GsonBuilder().create()
Use @Inject on a constructor or function parameter to request a specific named dependency.
@Factory
class ParserUser(
@Inject("exampleParser") val parser: Gson,
)
Named dependencies can also be fetched manually:
class NamedUsage : KoinComponent {
private val parser: Gson by inject(named("exampleParser"))
}
class NamedUsageJava {
private val parser: Gson =
KoinJavaComponent.get(Gson::class.java, named("exampleParser"))
}
Parameters
Constructor parameters annotated with @Parameter are provided when the object is created.
This is useful when you want to pass a dynamic value to the factory.
Other constructor parameters are resolved from Koin.
@Factory
class GreetingTracker(
@Parameter val player: Player,
private val service: ExampleService,
) {
fun greet() {
service.greet(player)
}
}
When requesting the dependency, supply the parameter using parametersOf.
class ParameterManager : KoinComponent {
fun trackerFor(player: Player): GreetingTracker =
getKoin().get(parameters = { parametersOf(player) })
}
Fetching All Bindings
To retrieve every binding of a given type use getAll<T>():
class GetAllExample : KoinComponent {
fun services(): List<ExampleService> =
getKoin().getAll()
}