Kotlin Multiplatform and Swift - Overcoming Interoperability Challenges for Multiplatform Development
Tags: android, ios, kotlinmultiplatform, koin
Published: 2023-07-16 10:21:19

[Kotlin Multiplatform Mobile (KMM)](https://kotlinlang.org/lp/multiplatform/) is a popular framework for developing multi-platform mobile applications with shared code between Android and iOS. 
However, one limitation of KMM is that it doesn't allow direct use of Swift libraries: this is because Kotlin doesn't have direct interoperability with the Swift language; instead, it relies on interoperability with Objective-C.

As a result, KMM doesn't have built-in support for Swift-only libraries that don't have an Objective-C API. This means that developers can't use Swift libraries directly in a KMM project without first creating a bridging library that provides a Kotlin interface to the Swift code.

Creating a bridging library requires writing a separate library for each Swift library that needs to be used in the project: this can be time-consuming and requires knowledge of both Kotlin and Swift.

## Using a Swift library with Dependency Injection and Koin

Imagine the classical situation where you want to use a third party service and they provide you a native Android SDK and a native iOS SDK (written in Swift).
If you are going to develop two native apps, you face no issues, each platform implements and uses their respective SDK.
If you are going to develop a KMM app, you end up having some issues due to the fact the iOS SDK is written in Swift.

Let's see how we can use [Koin](https://insert-koin.io/) to achieve Dependency Injection (DI) and incorporate a third-party iOS Swift SDK in a KMM project.
Koin supports KMM development, making it the ideal choice for KMM projects.

# Shared code

If you are going to use a library which already supports KMM, you are going to declare it as a dependency in the `commonMain` source set of the `shared` module `build.gradle.kts` file.
This is not our case, we have a library for Android and a separate one for iOS.  
For the sake of example, let's imagine we have `com.weather.sdk:1.0.0` for Android and `WeatherSDK` for iOS: they provide weather information given a city.

So what do we do?

## Create a wrapper

To avoid scattering the use of the weather SDK throughout the project and allow for future flexibility to swap it out with another SDK, we create a wrapper interface for it. This approach provides a layer of abstraction that enables us to decouple the SDK implementation from the rest of the project, making it easier to maintain and modify in the future.

```kotlin
interface WeatherDataSource {
    suspend fun getWeather(cityName: String): WeatherInfo
}
```

Do not take care of the function signature, it's just an example. Keep the focus on the fact we are creating an interface to decouple from the Weather SDK.

Now, we need to implement this interface both for Android and for iOS. The implementantions will use the respective Weather SDK to fetch data.
Let's start from Android first, which is easier.

## Android setup

First, set the Android library as dependency in the `androidMain` source set.

```kotlin
val androidMain by getting {
    dependencies {
        implementation("com.weather.sdk:1.0.0")
    }
}
```

This makes us able to use `com.weather.sdk` into the `androidMain` source set.

Next step is to create the implementation for `WeatherDataSource`, so we create `AndroidWeatherDataSource`, where we use the `weatherSdk` class of Weather SDK to fetch weather information.  
*This is the link between our code and the third party SDK.*

```kotlin
class AndroidWeatherDataSource(
    private val weatherSdk: WeatherSDK
): WeatherDataSource {
    suspend fun getWeather(cityName: String): WeatherInfo {
        //use weatherSdk to fetch weather information!
    }
}
```

Now, still in the `androidMain` source set, we create this function to init Koin:

```kotlin
/**
 * [context] is the Android app context
 * [appModules] is a list of modules defined in the app layer (e.g. viewModels)
 */
fun initKoin(context: Context, appModules: List<Module>) {
    startKoin {
        androidLogger()
        androidContext(context)
        modules(appModules + sharedModules + weatherModule)
    }
}
```

Koin will be setup using:
- `appModules`: modules created in the app layer, e.g. for viewModels
- `sharedModules`: modules created in the `commonMain` source set to be shared between platforms
- `weatherModule`: module where we declare dependencies for `com.weather.sdk`

We will concentrate on `weatherModule`, let's see its code:

```kotlin
val weatherModule = module {
    singleOf(::AndroidWeatherDataSource) bind WeatherDataSource::class
}
```

We declare `AndroidWeatherDataSource` as single instance for whenever we require `WeatherDataSource`.

Now, let's move to iOS!

## iOS setup

iOS setup it's a little bit more cumbersome than the Android one, but let's go step by step to see what's needed.

Since the iOS library is written in Swift and doesn't have Objective-C bindings, we can't declare it in the `iOSMain` source set, we can't [`cinterop`](https://kotlinlang.org/docs/native-c-interop.html) it neither.
The only way to use it is from the iOS project that AndroidStudio/KMMPlugin created for us.

First thing, as we did for Android, it's to create the implementation for `WeatherDataSource`, so we create `IOSWeatherDataSource`:

```swift
import WeatherSDK
import shared

class IOSWeatherDataSource: WeatherDataSource {
    final let weatherSDK: WeatherSDK

    init(weatherSDK: WeatherSDK) {
        self.weatherSDK = weatherSDK
    }

    func getWeather(cityName: String) async throws -> WeatherInfo {
        //use weatherSdk to fetch weather information!
    }
}
```

Now, let's go back to our `shared` module into `iOSMain` source set: here, as well as for Android, we need to create a function to init Koin.
Right now, Koin API can't be used in the iOS project, so we need both to create a function to init Koin and to expose all Koin dependencies to the iOS project.  
To do so we create a class `DependenciesProviderHelper`:

```kotlin
class DependenciesProviderHelper : KoinComponent {

    /**
     * [weatherDataSource] is the iOS implementation to fetch weather information
     */
    initKoin(
        weatherDataSource: WeatherDataSource
    ) {
        val iosDependenciesModule = module {
            single {
                weatherDataSource
            } bind WeatherDataSource::class
        }
        startKoin {
            modules(iosDependenciesModule + sharedModules)
        }
    }

    //usecases
    val getWeatherInfoUseCase: GetWeatherInfoUseCase by inject()

}
```

Since it's currently not possible to use Koin APIs from iOS, the `initKoin()` function can't have a list of Koin `Module` like the Android one. However, we can still provide the real implementations of our dependencies and create a Koin module in the shared code. Then, we can use `startKoin()` with both the iOS dependencies and the shared modules.

Imagine then that this *DataSource* will be used in a *Repository* that will be used in a *UseCase*, here in `DependenciesProviderHelper` we also expose `getWeatherInfoUseCase` to be able to use it from the iOS project.

## Start Koin!

Both the projects are now setup correctly, Dependency Injection is in place, real implementations have been created, we just miss one last thing: to finally start Koin!

On Android, you start Koin in your application class:

```kotlin
class MyApplication : Application() {
    
    override fun onCreate() {
        super.onCreate()
        initKoin(this@MyApplication, appModules)
    }

}
```

On iOS, you do the same:

```swift
@main
struct MyApplication: App {

    init() {
        let iosWeatherDataSource = //create iOSWeatherDataSource
        //DependenciesProviderHelper should be a singleton
        DependenciesProviderHelper().initKoin(
            weatherDataSource: iosWeatherDataSource
        )
    }

}
```

# Instances injected by Koin

What instances does Koin provide in our code and how does it provide them, based on the code above?
`AndroidWeatherDataSource` is defined as a single instance for `WeatherDataSource` on Android, which means that Koin will handle its instantiation and provide it whenever it is needed in our code. Because it is defined as single, the instance will remain the same throughout the lifecycle of our app.
On iOS, the approach is similar, but Koin will not instantiate `IOSWeatherDataSource` on its own. Instead, we will need to instantiate it ourselves and provide the instance to `startKoin()` before Koin can manage it and provide it whenever it is needed.


# Considerations

We have come to the end of how to use a Swift third-party library in a Kotlin Multiplatform project. While it's still necessary to write two different native implementations (one for iOS and one for Android), these implementations are restricted in the *DataSource* level, while *UseCases* and *Repositories* can still be shared and written into the `commonMain` source set.

This means that the *UseCases* can use shared *Repositories*, and tests can be written to cover the *UseCases* and *Repositories* code just once. If there's a bug in the *DataSources*, it will be spotted by these tests.

The shared code also provides the advantage of being able to implement functionality once, such as a cache in the *Repository* layer, instead of writing it twice for both platforms.

While there are limitations and challenges to using Swift third-party libraries in a Kotlin Multiplatform project, the benefits can make it a worthwhile investment for developers looking to build multiplatform apps.