How to make a multi-module kotlin multiplatform library for iOS?
Asked Answered
D

1

8

I'm trying to make a Kotlin Multiplatform library to be used in both Android, JavaScript and iOS. This library is composed of multiple modules so that it can be easily extensible. My problem right now is only with Kotlin native.

This is how the project is setup

  • A module :common
object MySingleton {
    fun doSomethingWithMyInterface(value: MyInterface) {
        // ...
    }
}

interface MyInterface {
  fun doSomething()
}
  • Other modules implementing MyInterface
class MyInterfaceExample : MyInterface {
    override fun doSomething() {
        // ...
    }
}

I have setup the build.gradle.kts file for :common as follow:

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("maven-publish")
}

kotlin {
    targets {
        jvm()
        js().browser()
        ios("ios") {
            binaries {
                framework("CommonModule") {
                    baseName = "common"
                }
            }
        }
    }

    cocoapods {
        frameworkName = "CommonModule"
        summary = "My common module"
        homepage = "-"
        license = "-"

        ios.deploymentTarget = "10.0"
    }

    sourceSets {
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosMain by getting {
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
        }
    }
}

And my other module as follow:

plugins {
    id("com.android.library")
    id("maven-publish")
    kotlin("multiplatform")
    kotlin("native.cocoapods")
}

android {
    compileSdkVersion(30)
}

kotlin {
    targets {
        js().browser()
        ios("ios") {
            binaries {
                framework("ExampleImplementation", listOf(DEBUG)) {
                    baseName = "example"
                    freeCompilerArgs += "-Xobjc-generics"
                    export(project(":common")) {
                        isStatic = true
                    }
                }
            }
        }
        android("android") {
            publishLibraryVariants("release")
        }
    }

    cocoapods {
        frameworkName = "ExampleImplementation"
        summary = "Example implementation of MyInterface"
        homepage = "-"
        license = "-"

        useLibraries()

        ios.deploymentTarget = "10.0"
        pod("MyDependency")
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                api(project(":common"))
            }
        }
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosMain by getting {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
        }
        val androidMain by getting {
            dependsOn(commonMain)
            dependencies {
                implementation("group:artifactId:1.0.0")
            }
        }
        val jsMain by getting {
            dependencies {
                implementation(npm("my_dependency", "1.0.0"))
            }
        }
    }
}

Both :common and :example modules generate a podspec file which adds a script to be executed by cocoapods manager:

<<-SCRIPT
                set -ev
                REPO_ROOT="$PODS_TARGET_SRCROOT"
                "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" :example:syncFramework \
                    -Pkotlin.native.cocoapods.target=$KOTLIN_TARGET \
                    -Pkotlin.native.cocoapods.configuration=$CONFIGURATION \
                    -Pkotlin.native.cocoapods.cflags="$OTHER_CFLAGS" \
                    -Pkotlin.native.cocoapods.paths.headers="$HEADER_SEARCH_PATHS" \
                    -Pkotlin.native.cocoapods.paths.frameworks="$FRAMEWORK_SEARCH_PATHS"
            SCRIPT

When I add those modules in an iOS project like so:

pod 'common', :path => '~/Project/MyLibrary/common/'
pod 'example', :path => '~/Project/MyLibrary/example/'

After I do a pod install, I can see both frameworks being added.

The problem

The framework generated for example doesn't depend on the framework generated for common. Yet, :example generates an interface of its own called ExampleCommonMyInterface which have the same signature as MyInterface in :common.

Also, both of them includes some KotlinBase classes which prevents me from building as I encounter an issue for duplicated classes.

I tried to only include :example but then it's missing my MySingleton class (which is not a Singleton in Swift but that's another issue) and other classes which :example doesn't depend on directly. Also, at some point, I would like to have more than one module depending on :common so only including :example would only work temporarily.

I have tried many things for several days now but none of them works. Also, the documentation is scattered between the documentation for kotlin multiplatform and kotlin native which made it hard for me to find relevant information regarding what I'm trying to achieve.

I understand that Kotlin multiplatform is still not stable and what I'm trying to achieve may not be possible at the moment. Nevertheless, I would greatly appreciate if someone can shed some light on what the problem is and how I can solve it.

Dead answered 4/3, 2021 at 8:21 Comment(0)
R
12

At the moment (Kotlin 1.5-M1), Kotlin/Native does not support generating binary frameworks where one depends on another. There are only two working configurations supported right now:

  1. using multiple independent K/N generated frameworks; or
  2. exporting all your Kotlin modules as one monolithic K/N generated framework (also known as the "umbrella framework" approach in the community).

If you are planning to organize your code into a hierarchy of Kotlin modules, (2) is your only option. You either:

  1. use only the framework product from the example module, whose framework product already contains all the code of all its dependencies (incling the transitive ones and the Kotlin runtime), as you might have observed; or
  2. create a new e.g. iosExport module that exports all the modules that should be API visible to your iOS codebase.

Reference

The issue tracking improvements in this area: https://youtrack.jetbrains.com/issue/KT-42247

This topic has been briefly touched on by the official KMM documentation:

https://kotlinlang.org/docs/mpp-build-native-binaries.html#export-dependencies-to-binaries [...] For example, assume that you write several modules in Kotlin and then want to access them from Swift. Since usage of several Kotlin/Native frameworks in one Swift application is limited, you can create a single umbrella framework and export all these modules to it.

Rajiv answered 4/3, 2021 at 20:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.