Kotlin Multiplatform Development 1.6.2 Help

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:

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:

Kotlin Multiplatform shared module

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.

Module configurations

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:

Single shared module

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.

Pros

Cons

  • A simple design with just a single module reduces cognitive load. You don't need to think about where to put your functionality or how to split it logically into parts.

  • Works great as a starting point.

  • Compilation time increases as the shared module grows.

  • This design doesn't allow having separate features or having dependencies only on the features the app needs.

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.

Pros

Cons

  • Separation of concerns for shared code.

  • Better scalability.

  • More complicated setup, including the umbrella framework setup.

  • More involved dependency management across modules.

To set up an umbrella module, you add a separate module that depends on all feature modules and generate a framework from this module:

Umbrella framework

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.

Repository configurations

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:

Monorepo configuration
Monorepo configuration

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.

Pros

Cons

  • Easy to set up with the help of wizards.

  • iOS developers can easily work with Kotlin Multiplatform code since all of the code is located in the same repository.

  • iOS developers need to set up and configure unfamiliar tools.

  • This approach often doesn't work for existing apps already stored in different repositories.

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:

Two repository configuration

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:

Three repository configuration

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:

Many repository configuration

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:

Local source distribution

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.

Pros

Cons

  • Both Android and iOS team members can easily edit Kotlin Multiplatform code, ensuring that creating and maintaining shared code is a shared responsibility. This helps prevent teams' isolation and encourages collaboration.

  • This approach doesn't require separate versioning and publishing of the shared code.

  • The development workflow is quicker, as iOS team members don't have to wait for artifacts to be created and published.

  • Team members need to set up a full development environment on their machines.

  • iOS developers have to learn how to use Android Studio and Gradle.

  • Managing changes becomes difficult as more code is shared and the team grows.

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:

Remote artifact distribution

Pros

Cons

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.

  • Slower workflow for iOS developers, as the process of editing and building the shared code involves publishing and versioning.

  • Debugging shared Kotlin code is difficult on iOS.

  • The likelihood of iOS team members contributing to shared code significantly decreases.

  • The maintenance of the shared code rests entirely on participating team members.

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.

Last modified: 07 November 2023