Weird behavior when splitting dependencies into different modules
I've encountered a weird behavior when separating my dependencies into different modules. First, I had all my dependencies into one module:
val module = module {
//Initializables
single(named(NamedDependencies.SqliteDb)) {
DatabaseManager.Companion
}.bind<Initializable>()
single(named(NamedDependencies.WindowsKeyboard)) {
WindowsKeyboardListenerService.Companion
}.bind<Initializable>()
//Helpers
factoryOf(::DefaultDatabaseObserver).bind<DatabaseObserver>()
singleOf(::ToDoDeletionSchedulerService).bind()
factoryOf(::ToDoInfoSorter).bind()
factory { { InetSocketAddress(ConnectionService.DEFAULT_HOST, ConfigurationManager.port) } }
singleOf(::KtorConnectionService).bind<ConnectionService>()
single {
HttpClient(OkHttp) {
install(Logging)
install(HttpTimeout)
install(ContentNegotiation) {
json()
}
}
}
singleOf(::KtorServerService).bind()
single { p ->
WindowsKeyboardListenerService(p[0], p[1])
}.bind<KeyboardListenerService>()
// Databases
singleOf(::DatabaseManager).bind<DatabaseManager>()
//DAOs
single { ApplicationDao.Companion }.bind<ApplicationDaoClass>()
single { ExecutableDao.Companion }.bind<ExecutableDaoClass>()
single { ToDoDao.Companion }.bind<ToDoDaoClass>()
single { ScheduledToDoDeletionDao.Companion }.bind<ScheduledToDoDeletionDaoClass>()
// Repositories
factoryOf(::SqliteApplicationsRepositoryImpl).bind<ApplicationsRepository>()
factoryOf(::SqliteExecutablesRepositoryImpl).bind<ExecutablesRepository>()
factoryOf(::SqliteToDosRepositoryImpl).bind<ToDosRepository>()
//Services
factoryOf(::ApplicationsService).bind<IApplicationsService>()
factoryOf(::ApplicationsGrouperService).bind()
factoryOf(::ToDosService).bind<IToDosService>()
factoryOf(::UpdateCheckerService)
single { ConfigurationService }.bind()
// ViewModels
factoryOf(::ConfigurationViewModel).bind()
factory { p -> EmptyErrorViewModel(get(), get(), p.getOrNull()) }
factory { p -> ToDosViewModel(get(), get(), p.getOrNull(), p.getOrNull()) }
factory { p -> ToDoEditionViewModel(get(), p.getOrNull(), p.getOrNull()) }
factoryOf(::ApplicationsViewModel).bind()
}
I refactored the code, so all relationed dependencies would go in a module, separated from other modules, and combined everything into one mainModule like this:
val initializableComponentsModule = module {
single<Initializable>(named(NamedDependencies.SqliteDb)) {
DatabaseManager.Companion
}
single<Initializable>(named(NamedDependencies.WindowsKeyboard)) {
WindowsKeyboardListenerService.Companion
}
}
val helpersModule = module {
factoryOf(::DefaultDatabaseObserver).bind<DatabaseObserver>()
singleOf(::ToDoDeletionSchedulerService).bind()
factoryOf(::ToDoInfoSorter).bind()
factory<() -> InetSocketAddress> { { InetSocketAddress(ConnectionService.DEFAULT_HOST, ConfigurationManager.port) } }
singleOf(::KtorConnectionService).bind<ConnectionService>()
single {
HttpClient(OkHttp) {
install(Logging)
install(HttpTimeout)
install(ContentNegotiation) {
json()
}
}
}
singleOf(::KtorServerService).bind()
single { p ->
WindowsKeyboardListenerService(p[0], p[1])
}.bind<KeyboardListenerService>()
}
val databaseModule = module {
singleOf(::DatabaseManager).bind<DatabaseManager>()
}
val daosModule = module {
single<ApplicationDaoClass> { ApplicationDao.Companion }
single<ExecutableDaoClass> { ExecutableDao.Companion }
single<ToDoDaoClass> { ToDoDao.Companion }
single<ScheduledToDoDeletionDaoClass> { ScheduledToDoDeletionDao.Companion }
}
val repositoriesModule = module {
factoryOf(::SqliteApplicationsRepositoryImpl).bind<ApplicationsRepository>()
factoryOf(::SqliteExecutablesRepositoryImpl).bind<ExecutablesRepository>()
factoryOf(::SqliteToDosRepositoryImpl).bind<ToDosRepository>()
}
val servicesModule = module {
factoryOf(::ApplicationsService).bind<IApplicationsService>()
factoryOf(::ApplicationsGrouperService).bind()
factoryOf(::ToDosService).bind<IToDosService>()
factoryOf(::UpdateCheckerService)
single { ConfigurationService }.bind()
}
val viewModelsModule = module {
factoryOf(::ConfigurationViewModel).bind()
factory { p -> EmptyErrorViewModel(get(), get(), p.getOrNull()) }
factory { p -> ToDosViewModel(get(), get(), p.getOrNull(), p.getOrNull()) }
factory { p -> ToDoEditionViewModel(get(), p.getOrNull(), p.getOrNull()) }
factoryOf(::ApplicationsViewModel).bind()
}
val mainModule = module {
includes(
initializableComponentsModule,
helpersModule,
databaseModule,
daosModule,
repositoriesModule,
servicesModule,
viewModelsModule
)
}
In the first version, when I invoke this code, it worked fine:
viewModel(
koinInject<ToDoEditionViewModel>(parameters = {
parametersOf(
applicationId, todoId // Both of these parameters can be null Longs
)
})
)
Now I'll explain the behavior of both previous and current version: Before:
- Pass applicationId and todoId (applicationId = any number, 1 for this instance, todoId = null)
- factory method receives 1 and null. And calling p.getOrNull() passes arguments in order
- ToDoEditionViewModel gets 1 and null
Now (after refactor):
- Pass applicationId and todoId (applicationId = 1, todoId = null)
- factory method receives 1 and null (logged p.values list). Calling p.getOrNull() gets 1 and 1. Second value (null), is ignored or lost
- ToDoEditionViewModel gets 1 and 1
The project was started in compose desktop at a version where ViewModel class didn't exist yet in CMP. That's why I don't use viewModel factory methods of Koin and use a custom viewModel function at compose code, where dependency is injected.
Koin version: koin-core: 4.0.0 koin-compose: 4.0.0