koin icon indicating copy to clipboard operation
koin copied to clipboard

Weird behavior when splitting dependencies into different modules

Open MJavoso opened this issue 10 months ago • 0 comments

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:

  1. Pass applicationId and todoId (applicationId = any number, 1 for this instance, todoId = null)
  2. factory method receives 1 and null. And calling p.getOrNull() passes arguments in order
  3. ToDoEditionViewModel gets 1 and null

Now (after refactor):

  1. Pass applicationId and todoId (applicationId = 1, todoId = null)
  2. factory method receives 1 and null (logged p.values list). Calling p.getOrNull() gets 1 and 1. Second value (null), is ignored or lost
  3. 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

MJavoso avatar Feb 11 '25 05:02 MJavoso