KMP: Injecting Swift classes via Koin

In a KMP project, sometimes we handle some aspects in very different ways depending on the platform we are implementing. It becomes even more specific if we are dealing with code written in another language, and that's indeed what happens when we are implementing Dependency Injection for the iOS target with Swift classes, which is in fact something we may have to face in KMP apps. This article will illustrate step by step the process of installing Koin and injecting Swift native classes to be used in the Kotlin iosMain target in a KMP project. I hope you enjoy!
Project Creation and Koin set up
First things first, let's create the KMP project by using the KMP Wizzard. Fill with your project information and select to share UI between iOS and Android platforms. Now we need to install the Koin dependencies in order to our project to work.
Go to the libs.toml file and add the Koin dependencies that shall be installed:
[versions]
koin = "3.5.3"
[libraries]
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin"}
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin"}
We will rely on koin-core
as a general dependency for both platforms and koin-android
for Android specific features.
Now sync, add the following libs to our build.gradle.kts
file and sync again:
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
// Add here
implementation(libs.koin.android)
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.androidx.lifecycle.runtime.compose)
// And here
implementation(libs.koin.core)
}
Create a Kotlin interface for the injected class
Now we are gonna create a Kotlin interface that shall be implemented by our injected Swift class, but a very important detail to pay attention is that the iOSApp can only access files from the composeApp
(Kotlin target) that exist in the iosMain
, otherwise it will not recognize it.
As we also desire to implement our class in the Android target, create an expect
interface and specify(with the same body) in each platform.
In the commonMain
module:
expect interface ILogger {
fun log(s: String)
}
In the androidMain
module:
actual interface ILogger {
actual fun log(s: String)
}
class AndroidLogger: ILogger {
override fun log(s: String) = println("Android Logging $s")
}
In the iosMain
module:
actual interface ILogger {
actual fun log(s: String)
}
We are gonna implement it in Swift, but first, build the project to have our ILogger
interface accessible by the iosApp.
Create the Swift class
Now open the project on Xcode and add a new Swift class. If you import ComposeApp
you will be able to make your class implement the ILogger
interface(now a Swift protocol).
import ComposeApp
final class IosLogger: ILogger {
func log(s: String) {
print("Logging \(s)")
}
}
Now that our class is created, we need to be able to inject into our Kotlin application via a Koin module, and this module should be initialized within our platform specific target.
Create a KoinHelper class
Now we are gonna implement a class that will initialize all of our Koin modules with the respective dependencies. Inside iosMain
, create the following class:
import org.koin.core.context.startKoin
import org.koin.dsl.module
class KoinInitHelper() {
fun initKoin(logger: ILogger) {
startKoin {
modules(module { single<ILogger> { logger } })
}
}
}
Notice this code starts the Koin modules and receives an ILogger
as a parameter, that will be injected via Koin as ILogger
. To make it work, we are gonna call this class in our iOS initialization block.
Initialize Koin in iOS
Back to the iOS app, call the helper class in the starter code of our iOS target:
import SwiftUI
import ComposeApp
@main
struct iOSApp: App {
init() {
// Initialize iOS Koin dependencies
KoinInitHelper().doInitKoin(logger: IosLogger())
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Now we are initializing Koin dependencies each time we start the main iOS scene.
In the Android target, create the same class, but that will be started in the MainActivity
itself.
import org.koin.core.context.startKoin
import org.koin.dsl.module
class KoinInitHelper {
fun initKoin(logger: ILogger) {
startKoin {
module {
single<ILogger> { logger }
}
}
}
}
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
KoinInitHelper().initKoin(AndroidLogger())
setContent {
App()
}
}
}
Call the Logger classes in each target
Now let's call our logger dependency in the main scene of our app to check everything is working as expected. Create a new Composable screen:
import androidx.compose.runtime.Composable
import org.koin.mp.KoinPlatform
@Composable
fun LoggerScreen() {
// Fetch the Koin dependency
val logger = KoinPlatform.getKoin().get<ILogger>()
logger.log("Pedro Alvarez")
}
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
MaterialTheme {
LoggerScreen()
}
}
This is just a blank screen that will call the ILogger
dependency and run the specific implementation for the given platform. If we run that on iOS, you will see the message "Logging Pedro Alvarez" will be called twice.
Conclusion
Dealing with specific code may be a tricky task to each hybrid mobile development platform, and that becomes even more complex if dealing with different programming languages. In this article we saw how to create a Swift specific class for iOS, make it conform to a Kotlin interface(protocol) and then injecting this implementation via Koin to be recognized by the default common interface that works for iOS and Android. I hope you enjoyed!