Hi everyone, i am building a kotlin multiplatform ...
# multiplatform
s
Hi everyone, i am building a kotlin multiplatform library, i am at a point where i need to use di to keep the tests maintainable. I already read about koin but it seems it is not designed for using it only in the shared module. Is there a good way to do di in the shared module, like injecting object classes as singleton to other classes that use this singleton? Or are there any guides that describe how to use koin only in shared, without initializing it in the app itself?
p
Did you already take a look at https://insert-koin.io/docs/reference/koin-mp/kmp and the example project "hello-kmp" at https://github.com/InsertKoinIO/hello-kmp/? (I'm sorry that doesn't directly answer your question and the example is 2 years old and not using tests.) If you only want to use the shared module for koin I think you should be able to use the androidMain source set in shared. (Create an [abstract] Application class where koin is initialized in onCreate in shared/src/androidMain to be inherited/used in the androidApp module.) You may also find help in #koin.
s
mhm not sure if i get it right but, i want to use koin in shared, so no initkoin in iosApp or AndroidApp. I just want the dependency injection feature to connect different classes/objects inside of the shared or even commonMain code.
p
I think you would still have to init koin somewhere. If your di isn't using any platform secific implementations it should all be possible in shared/src/commonMain
s
So i could just define my modules and use startkoin in the init of the library facade in the commonMain part?
p
Copy code
override fun onCreate() {
    super.onCreate()

    startKoin {
        // Log Koin into Android logger
        androidLogger()
        // Reference Android context
        androidContext(this@MainApplication)
        // Load modules
        modules(appModules(databaseModule))
    }
}
This is what my onCreate looks like. I Implemented it in the androidApp module. The databaseModule is a val within androidApp because the implementation of that uses android specific dependencies and provides a custom DatabaseService interface so the other modules in the shared module can consume this DatabaseService. On the iOS side I have a helper file/fun in shared/src/iosMain like this:
Copy code
fun initKoin(databaseService: DatabaseService) {
    val databaseModule =
        module {
            single { databaseService }
        }
    startKoin {
        modules(appModules(databaseModule))
    }
}
That is used in iosApp/iosApp/IOSApp.swift like this:
Copy code
DIHelperKt.doInitKoin(databaseService: DatabaseIOS(...))
In theory I could have moved the code from onCreate() into the shared module (androidMain sourceSet). If your library facade has a init function you can use from ios and android that should to the trick.
j
I've done a similar setup before, where I use Koin in the shared module only and integrate it into Hilt & Swinject: https://medium.com/better-programming/multiplatform-dependency-injection-making-koin-dagger-hilt-and-swinject-work-together-on-android-e17b98bd8f7b
s
ok thanks a lot @p-schneider that really helps. thanks for your effort and informations. To be more specific what i want to achieve: Basically i have some objects like an eventManager / StateMachine for the internal state of my library that is called from everywhere in commonMain, iosMain and AndroidMain, i also have a Logger object that wraps napier for future features that is also used everywhere. and some classes use some functions from other objects to reduce parameter inheritance over multiple classes. So if i want to test this library i need to mock some of those classes and i want to inject the mock classes instead of the real classes in my test cases.
j
Ah, I misunderstood. Then Philipp's approach is what you need 🙂
p
In my case I was injecting different databaseModules on iOS and Android but the rest of the modules are defined in shared/src/commonMain. (This is that my fun appModules in shared/src/commonMain looks like:)
Copy code
fun appModules(databaseModule: Module): List<Module> {
    @OptIn(KoinInternalApi::class)
    val databaseServiceCount =
        databaseModule.mappings.values.count {
            it.beanDefinition.primaryType == DatabaseService::class
        }
    @OptIn(KoinInternalApi::class)
    <http://logger.info|logger.info> { "Logging Test ${databaseModule.mappings.keys.joinToString()}" }
    require(databaseServiceCount == 1) {
        "Module $databaseModule should provide 1 DatabaseService, found $databaseServiceCount"
    }
    return listOf(
        platformModule,
        databaseModule,
        stateMachineModule,
    )
}
so basically just a list of modules, with some extra checks to make sure the "database module" is actually providing a database. So you should be able to use koin only in shared, just make sure koin is correctly initialized when needed. @Jacob Ras thanks for your link, i bookmarked it for later
j
You could initialize Koin in an
init
block in a class'
companion object
. If there are one or more classes that are the primary entry points into using your shared module, you could call the Koin initialization function from each of them, with a check that it's only executed once. You might consider using an isolated context as well, so your module dependencies don't leak into other modules that might also be using Koin for dependency injection.
👍 2
223 Views