Kodein-DI on TornadoFX
You can use Kodein-DI as-is in your TornadoFX project, but you can level-up your game by using the libraries kodein-di-framework-tornadofx-jvm.
Kodein-DI does work on TornadoFX as-is.
The kodein-di-framework-tornadofx-jvm extensions add multiple TornadoFX-specific utilities to Kodein-DI.Using or not using this extension really depends on your needs. |
Have a look at the TornadoFX demo project to help you going further!
Install
kodein-di-framework-tornadofx-jvm:-
Add those lines in your
dependenciesblock in your applicationbuild.gradlefile:Gradle Groovy scriptimplementation 'org.kodein.di:kodein-di-framework-tornadofx-jvm:7.11.0'
Gradle Kotlin scriptimplementation("org.kodein.di:kodein-di-framework-tornadofx-jvm:7.11.0") -
Declare your
AppasDIAwareand implement a DI containerExample: a TornadoFX Application declaration,DIAware, using TornadoFX DI as external sourceclass TornadoApplication : App(MainView::class), DIAware { (1) override val di: DI get() = DI { (2) installTornadoSource() (3) } }1 Setting the AppasDIAwarewill help you define a global DI container2 Define a DI container, accessible from almost anywhere in your application 3 Branch DI upon TornadoFX dependency injection, to be able to access those dependencies through your DI container (see documentation)
In order to leverage the power of DI and the helpers we made, you should really consider making your App as DIAware.
|
Retrieving
If you defined your App as DIAware, you will normally be able to retrieve a DI container, and dependencies, from almost anywhere in your code.
You may also want to access your TornadoFX components (such as View, Fragment, Controller, etc) through a DI container, to help you with that check the external source section.
|
Closest DI pattern
The idea behind this concept, is to be able to retrieve a DI container, from an outer class. For example, making our App as DIAware give us the ability to have a global DI container that can be retrieve from multiple places, like:
-
Components (
View,Fragment,Controller, etc) -
Nodes, graphical object of the
App
From a TornadoFX perspective
In TornadoFX there is no hierarchy between the different components. In fact, their is no bound between View, Fragment, Controller or ViewModel, except if you handle it yourself. However, they are available through the DI container of TornadoFX (and the one of DI if you use external source helper), and they all have a reference to the App.
So, if we made our App as DIAware, we can get the global DI container from any Component thanks to:
Component that get the DI container from the App, if its DIAwarefun Component.kodeinDI() = kodeinDI { app }
|
TornadoFX
|
In your code you can access the global DI container like the following snippets.
Controllerclass PersonController : Controller() {
val repository: PersonRepository by kodeinDI().instance() (1)
}
| 1 | The kodeinDI() function that will get the nearest DI container, from the App |
Viewclass PersonEditorView : View() {
val controller: PersonController by kodeinDI().instance() (1)
}
| 1 | The kodeinDI() function will get the nearest DI container, from the App |
All TornadoFX Component can access the global DI container ) by calling the function kodeinDI(),
but in order to do that your App must be DIAware.
|
From a JavaFX perspective
In JavaFX, an Application is made of a Stage (the window), a Scene (the container for all content) and a graph of Node. All Node are grouped by view (in the case of TornadoFX: View or Fragment).
So, the Node hierarchy can apply to the closest DI pattern, limited to the scope of a View or Fragment for now. If none of the parent Node have a DI container, thanks to the TornadoFX facilities, we can fallback onto the global DI container from the App.
Nodeclass EditorTabFragment : Fragment() {
override val root = hbox {
form {
val author: String by kodeinDI().instance("author") (1)
label { author }
}
}
}
| 1 | The kodeinDI() function will get the nearest DI container, from the parent Node, or from the App |
If none of the parent nodes have a DI container, we try to get to the one in the App, only if this App is DIAware.
|
Being DIAware
Having your classes set as DIAware have multiple advantages. For example this allow to cache the DI container or simplify the way we retrieve our dependencies.
class PersonListController : Controller(), DIAware { (1)
override val di: DI = kodeinDI() (2)
val personEditorController: PersonEditorController by instance() (3)
fun editPerson(person: Person) {
personEditorController.editPerson(person) (4)
}
/*...*/
}
| 1 | Set the Controller as KodeinAware |
| 2 | Retrieve the App DI container from the kodeinDI() extension function |
| 3 | Retrieve dependency using the instance() function, as the DI container is part of the context |
| 4 | Use the dependency |
| Because DI is lazy, the container and the dependencies will be retrieve at call site only. |
To benefit from the DI optimization, and the facilities we provide, we highly recommend
that you make your classes DIAware when its possible.
|
Extension of Kodein-DI
This section will cover how we can extend the use of DI container in a TornadoFX application, like:
-
Getting control of dependency injection over TornadoFX
-
Defining specific DI containers in a TornadoFX / JavaFX graph
-
Overriding a parent DI container in a TornadoFX component / JavaFX node
Using TornadoFX Dependency Injection as external source
TornadoFX integrates a dependency injection mechanism to work with its Component.
This section will show you how you can interact with the Tornado DI container, by using Kodein-DI.
Our goal is to provide you some tools to be able to integrate Kodein-DI as your main DI container,
thus by learning and knowing only one mechanism.
-
Defining the external source
In order to use Kodein-DI upon TornadoFX you should make your
AppasDIAware, then by using theinstallTornadoSource()extension function, in yourdiproperty override, you’ll be able to get through the external source from anywhere in yourApp.Example: Installing the TornadoFX DI container as external source for Kodein-DIclass TornadoApplication : App(MainView::class), DIAware { override val di: DI = DI { installTornadoSource() (1) } }1 Branch DI upon TornadoFX dependency injection, to be able to access those dependencies through your DI container We highly recommend that you make your AppasDIAwarein order to benefit from the use of Kodein-DI upon TornadoFX DI container. -
Retrieving TornadoFX
Componentthrough Kodein-DIOnce you’ll have installed the TornadoFX source, you will be able to retrieve transparently TornadoFX
Componentthrough Kodein-DI, as it were one of your own dependencies.Example: Retrieving a Component from DIclass PersonListController : Controller() { ... } (1) class PersonListView : View() { private val listController: PersonListController by kodeinDI().instance() (2) }1 PersonListControlleris not bound on the DI container, but as aControllerits accessible from TornadoFX DI container2 Retrieve PersonListControllerthrough the DI external source -
Using TornadoFX scopes to retrieve binding through the external source
Like Kodein-DI, TornadoFX as scopes to contextualize and attach state to its dependencies. As we can retrieve TornadoFX dependencies through DI external source, we also can retrieve them using scopes with DI.
Example: Retrieving a Component from DI with a Scopeclass PersonScope(person: Person) : Scope() { (1) val model = PersonModel(person) } //... class EditorTabFragment : Fragment() { override val scope = super.scope as PersonScope (2) } //... val editor: EditorTabFragment by kodeinDI().on(personScope).instance() (3) //...1 Define a Scopethat will help contextualizeComponent2 Every Componenthas a scope that can be override, this allow TornadoFX to handle multiple instances with multiple contexts3 We can retrieve a scoped instance of EditorTabFragmentwith the functionon(personScope), where personScope is aPersonScope
Defining DI container in FX’s Node
For some need we could want to define DI containers into the Node hierarchy. To do so we provide an extension function to create a DI container attached to a Node. This is done by adding the DI container to the properties of the Node, thus we will be able to access it from any child in the hierarchy.
Nodeclass MyView : View() {
override val root = hbox { (1)
kodeinDI { (2)
bind<Random>() { singleton { SecureRandom() } }
}
form { (3)
val random by kodeinDI().instance<Random>() (4)
}
}
}
| 1 | Defining the root Node for a View |
| 2 | Declaring a new DI container into the root Node, with its binding |
| 3 | Defining a child Node |
| 4 | Calling the kodeinDI() extension function to access the nearest DI container, in this case the one defined at <2>, then retrieve a dependency |
Extending the nearest DI container
Some times, we might want to extend an existing DI container in a dedicated area, without impacting the whole application. For example, in a login form, we might want to have credentials binding, that would be only available on this form and its children.
To do so, we have facilities to extend a DI container by calling the subDI extension function, available on Component and Node classes.
-
In the case of a
ComponentExample: Extending the nearest DI container for aComponentclass LoginController : Controller(), DIAware { (1) override val di: DI = subDI { (2) bind<CredentialsDao> { singleton { CredentialsDao() } } (3) } // ... val dao by instance<CredentialsDao>() (4) }1 Defining your ComponentasDIAwarewill help keeping track of your extended DI container2 Extending the nearest DI container, here the Appone3 Defining binding only available for the extended DI container 4 Retrieve the CredentialsDaofrom the local DI containerMaking your ComponentasDIAwareis optional, but it will help you keeping track of your extended container. Otherwise, you might want to store a reference of your extended container in a local variable. -
In the case of a
NodehierarchyExample: Extending the nearest DI container for aNodeclass LoginView : View() { override val root = hbox { (1) subDI { (2) bind<LoginController>() { singleton { instance() } } (3) } form { val controller by kodeinDI().instance<LoginController>() (4) } } }1 Defining the root Nodefor aView2 Extending the nearest DI container, here the Appone, but stored in theNodeproperties3 Defining binding only available for the extended DI container 4 Retrieve the CredentialsDaofrom the nearest DI container, in that case the extended container stored in the hbox properties
In the case of a Component you have to handle the reference of the extended container yourself. But, in the case of a Node the subDI will manage it, by storing the reference to the extended container, into the Node properties, like for the FX container feature
|
TornadoFX scopes
DI-DI provides a standard scope for any object (TornadoFX or not).
The WeakContextScope will keep singleton and multiton instances as long as the context (= object) lives.
That’s why the ComponentScope and NodeScope are just wrappers upon WeakContextScope with the respective targets Component and Node.
-
ComponentScopeExample: DefiningComponentscoped dependenciesval di = DI { bind<EditingState>() { scoped(ComponentScope).singleton { EditingState() } } (1) }1 A EditingStateobject will be created for each Component that will ask for.Example: Retrieving `Component scoped dependenciesclass EditorTabFragment : Fragment() { private val editingState: EditingState by kodeinDI().on(this).instance() (1) }2 Scope is this -
NodeScopeExample: DefiningNodescoped dependenciesval di = DI { bind<EditingState>() with scoped(NodeScope).singleton { EditingState() } (1) }1 A EditingStateobject will be created for eachNodethat will ask for.Example: Retrieving `Node scoped dependenciesclass PersonEditorController : Controller() { val personEditorView: PersonEditorView by kodeinDI().instance() fun editPerson(person: Person) { val tab = personEditorView.tabPane.tab("Tab Title") val editingState: EditingState by kodeinDI().on(tab).instance() (1) } }2 Scope is tab a Nodeelement, everyTabwould have a different instance ofEditingState