Choosing a configuration for your Kotlin Multiplatform project
When you add Kotlin Multiplatform to an existing project or start a new one, there are different ways to structure your code. Typically, you create one or more Kotlin Multiplatform shared modules and use them from your Android and iOS apps.
To choose the best approach for your specific case, consider the following questions:
How do you consume an iOS framework generated by a Kotlin Multiplatform module from the iOS app? Do you integrate it directly, through CocoaPods, or by using the Swift package manager (SPM)?
Do you have one or several Kotlin Multiplatform shared modules? What should be an umbrella module for several shared modules?
Answering these questions will help you pick the best configuration for your project.
Connect a Kotlin Multiplatform module to an iOS app
To use a Kotlin Multiplatform shared module from an iOS app, you first need to generate an iOS framework from this shared module. Then, you should add it as a dependency to the iOS project:
It's possible to consume this framework as a local or remote dependency.
You can add a dependency on a Kotlin Multiplatform module framework to the iOS project in one of the following ways:
Direct integration. You connect the framework directly by adding a new run script phase to the build of the iOS app. See Connect the framework to your iOS project to learn how to do that in Xcode.
When you create a project with the Android Studio wizard, choose the Regular framework option to have this setup generated automatically.
CocoaPods integration. You connect a framework through CocoaPods, a popular dependency manager for Swift and Objective-C projects. It can be either a local or remote dependency. For more information, see Use a Kotlin Gradle project as a CocoaPods dependency.
To set up a workflow with a local CocoaPods dependency, you can either generate the project with a wizard, or edit the scripts manually.
Using SPM. You connect a framework using the Swift package manager (SPM), an Apple tool for managing the distribution of Swift code. There's no official support for SPM yet, but you can use community-supported solutions to set up a remote dependency on a Swift package containing the Kotlin Multiplatform module framework.
There are two module configuration options that you can use in Kotlin Multiplatform projects: single module or several shared modules.
Single shared module
The simplest module configuration contains only a single shared Kotlin Multiplatform module in the project:
The Android app can depend on the Kotlin Multiplatform shared module as a regular Kotlin module. However, iOS can't use Kotlin directly, so the iOS app must depend on the iOS framework generated by the Kotlin Multiplatform module.
Several shared modules
As your shared module grows, it's a good idea to break it into feature modules. This helps you avoid the scalability issues related to having just one module.
The Android app can depend on all feature modules directly, or only on some of them if necessary.
The iOS app can depend on one framework generated by the Kotlin Multiplatform module. When you use several modules, you need to add an extra module depending on all of the modules you're using, called an umbrella module, and then you need to configure a framework containing all of the modules, called an umbrella framework.
To set up an umbrella module, you add a separate module that depends on all feature modules and generate a framework from this module:
The Android app can either depend on the umbrella module for consistency or on separate feature modules. An umbrella module often contains useful utility functions and dependency injection setup code.
You can export only some of the modules to the umbrella framework, typically when the framework artifact is consumed as a remote dependency. The main reason for this is to keep the size of the final artifact down by making sure auto-generated code is excluded.
A known constraint of the umbrella framework approach is that the iOS app can't use only some of the feature modules – it automatically consumes all of them. For possible improvements to this functionality, describe your case in KT-42247 and KT-42250.
Why do you need an umbrella framework?
While it's possible to include several frameworks generated from different Kotlin Multiplatform shared modules in your iOS app, we do not recommend this approach. When a Kotlin Multiplatform module is compiled into a framework, the resulting framework includes all of its dependencies. Whenever two or more modules use the same dependency and are exposed to iOS as separate frameworks, the Kotlin/Native compiler duplicates the dependencies.
This duplication causes a number of issues. First, the iOS app size is unnecessarily inflated. Second, a dependency's code structure is incompatible with the duplicated dependency's code structure. This creates a problem when trying to integrate two modules with the same dependencies within the iOS application. For instance, any state passed by different modules through the same dependency won't be connected. This can lead to unexpected behavior and bugs. See the TouchLab documentation for more details on the exact limitations.
Kotlin doesn't produce common framework dependencies because otherwise there would be duplication, and any Kotlin binary you add to your app needs to be as small as possible. Including the whole Kotlin runtime and all of the code from all dependencies is wasteful. The Kotlin compiler is able to trim the binary to exactly what it needs for a particular build. However, it doesn't know what other builds might need, so trying to share dependencies is unfeasible. We're exploring various options to minimize the effects of this issue.
The solution to this problem is to use an umbrella framework. It prevents the bloating of the iOS app with duplicated dependencies, helps optimize the resulting artifact, and eliminates the frustrations caused by incompatibilities between dependencies.
There are a number of repository configuration options that you can use in new and existing Kotlin Multiplatform projects, using one repository or a combination of several repositories.
Monorepo: everything in one repository
A common repository configuration is called a monorepo configuration. This approach is used in Kotlin Multiplatform samples and tutorials. In this case, the repository contains both Android and iOS apps, as well as the shared module or several modules, including the umbrella module:
Typically, the iOS app consumes the Kotlin Multiplatform shared module as a regular framework by using direct or CocoaPods integration. See Connecting Kotlin Multiplatform module to iOS app for more details and links to tutorials.
If the repository is under version control, the apps and the shared module have the same version.
When the existing Android and iOS apps are already stored in different repositories, you can add the Kotlin Multiplatform part to an Android repository or to a separate repository, instead of merging them.
Two repositories: Android + shared | iOS
Another project configuration is having two repositories. In this case, the Kotlin Multiplatform repository contains both the Android app and the shared module, including the umbrella module, while the Xcode project contains the iOS app:
The Android and iOS apps can be versioned separately, and the shared module is versioned along with the Android app.
Three repositories: Android | iOS | shared
One more option is having a separate repository for the Kotlin Multiplatform modules. In this case, the Android and iOS apps are stored in separate repositories, and the project's shared code can contain multiple feature modules and the umbrella module for iOS:
Each project can be versioned separately. Kotlin Multiplatform modules must also be versioned and published for the Android or JVM platforms. You can either publish feature modules independently or publish only the umbrella module and make the Android app depend on it.
Publishing Android artifacts separately can present additional complexity for Android developers compared to project scenarios where the Kotlin Multiplatform modules are part of the Android project.
When both Android and iOS teams consume the same versioned artifacts, they operate in version parity. From a team perspective, this avoids the impression that the shared Kotlin Multiplatform code is "owned" by the Android developers. For large projects that already publish versioned internal Kotlin and Swift packages for feature development, publishing the shared Kotlin artifacts becomes a part of the existing workflow.
Many repositories: Android | iOS | multiple libraries
When functionality should be shared between multiple apps on multiple platforms, you might prefer having many repositories with Kotlin Multiplatform code. For example, you can store a logging library, which is common for the whole product, in a separate repository with its own versioning.
In this case, you have multiple Kotlin Multiplatform library repositories. If several iOS apps use different subsets of "library projects", each app can have an additional repository containing the umbrella module with the necessary dependencies on the library projects:
Here, each library must be versioned and published for the Android or JVM platforms, as well. The apps and each library can be versioned separately.
Code sharing workflow
The iOS app can consume a framework generated from the Kotlin Multiplatform shared modules as a local or remote dependency. You can use a local dependency by providing a local path to the framework in the iOS build. In this case, you don't need to publish the framework. Alternatively, you can publish an artifact with the framework somewhere and make the iOS app consume it as a remote dependency, like any other third-party dependency.
Local: source distribution
Local distribution is where the iOS app consumes a Kotlin Multiplatform module framework without the need for publishing. The iOS app can either integrate the framework directly or by using CocoaPods.
This workflow is typically used when both Android and iOS team members want to edit the shared Kotlin Multiplatform code. The iOS developers need to install Android Studio and have a basic knowledge of Kotlin and Gradle.
In the local distribution scheme, the iOS app build triggers the generation of the iOS framework. This means that iOS developers can observe their changes to Kotlin Multiplatform code right away:
This scenario is typically used in two cases. First, it can be used in monorepo project configurations as the default workflow, without the need to publish artifacts. Second, it can be used for local development, in addition to the remote workflow. See Setting up a local dependency for local development for more details.
This workflow is most effective when all of the team members are ready to edit code in the whole project. It includes both Android and iOS parts after making changes to the common parts. Ideally, every team member can have Android Studio and Xcode installed to open and run both apps after making changes to the common code.
Remote: artifact distribution
Remote distribution means that the framework artifact is published as a CocoaPod or Swift package using SPM and consumed by the iOS app. The Android app may consume the binary dependency either locally or remotely.
Remote distribution is often used to gradually introduce the technology to existing projects. It doesn't significantly change the workflow and build processes for iOS developers. Teams with two or more repositories mainly use remote distribution to store project code.
As a start, you may want to use KMMBridge – a set of build tools that greatly simplifies the remote distribution workflow. Alternatively, you can always set up a similar workflow on your own:
Non-participating iOS team members don't have to code in Kotlin or learn how to use tools like Android Studio and Gradle. This lowers the barrier of entry for the team significantly.
Setting up a local dependency for local development
Many teams choose remote distribution workflows when adopting Kotlin Multiplatform technology to keep the development process the same for iOS developers. However, it's hard for them to change Kotlin Multiplatform code in this workflow. We recommend setting up an additional "local development" workflow with a local dependency on a framework generated from the Kotlin Multiplatform module.
When developers add new functionality, they switch to consuming the Kotlin Multiplatform module as a local dependency. That allows making changes to common Kotlin code, immediately observing the behavior from iOS, and debugging Kotlin code. When the functionality is ready, they can switch back to the remote dependency and publish their changes accordingly. First, they publish changes to the shared modules, and only after that do they make changes to the apps.
For remote distribution workflows, use either CocoaPods integration or SPM. For local distribution workflow, integrate the framework directly.
If you use CocoaPods, you can alternatively use CocoaPods for local distribution workflow. You switch between them by changing the environmental variable as described in the TouchLab documentation.