My advice would be to stick to constructor arguments as much as possible. It makes dependencies clearer to the reader, it's easier to write tests for it, it's easier to control dependencies from the outside (by passing it to the object via constructor arguments).
So as I do it,
• all dependencies are passed into components via the constructor
• parents create child components via
Koin.get()
so that parents don't need to know the dependencies of their children.
I'm sailing fairly well with this. Simplified it's this:
// All Components are also KoinComponents
interface AppComponentContext : ComponentContext, KoinComponent
// Typesafe creator for new components
inline fun <reified Child : ComponentContext> AppComponentContext.getComponent(key: String) =
get<Child> { parametersOf(childContext(key)) }
class ParentComponent(
context: ComponentContext
) : AppComponentContext, ComponentContext by context {
// ...
// same works for ChildStack, etc
val child = getComponent<ChildComponent>(key = "child")
}
class ChildComponent(
context: ComponentContext,
otherDependency: WidgetFactory // injected by Koin
) : AppComponentContext, ComponentContext by context {
// ...
}
This simple example lacks a bunch of functionality. For example, it's not possible for the Parent to pass any dependencies explicitly which you probably want to do some time to time; but that can be added with some tricks.