Kotlin Multiplatform 如何同时支持 Android 与 iOS:从 commonMain 到 XCFramework

开篇:共享代码能编译,不等于两端都能消费

跨平台应援灯牌控制应用同时面向 Android 和 iOS 用户,同一条业务规则如果分别写在 Kotlin、Swift 和 Flutter 中,时间久了就会出现输入处理不一致、错误文案不一致和修复遗漏。Kotlin Multiplatform 的价值,是把适合共享的规则放进 commonMain,再让各平台通过自己的交付方式消费它。

Kotlin Multiplatform 的难点通常不在 commonMain 里写出一个类,而在于共享代码如何进入 Android 应用、如何生成 iOS 能够链接的框架,以及框架还没有接入时应用怎样给出可诊断的结果。

本文以跨平台应援灯牌控制应用的 Flutter + KMP + OpenHarmony 多端工程为背景。当前示例中的 shared 模块提供了一个最小但完整的路径:commonMain 定义问候规则,commonTest 验证规则,Android 以项目依赖消费,iOS 通过 Shared.xcframework 接入。本文只讨论共享模块的交付边界,不重复介绍 Flutter 页面布局。读完本文,你会知道一段共享规则从源码到 Android 依赖、再到 iOS XCFramework 的完整路径,也能判断问题发生在业务代码、框架产物还是宿主链接阶段。

在这里插入图片描述

文章目标和工程边界

本文会回答四个问题:

  1. commonMain 代码应该放什么。
  2. Android target 和 iOS target 如何声明。
  3. Android 项目依赖与 iOS XCFramework 的接入方式有什么不同。
  4. 共享框架未链接时,Flutter 如何收到明确错误。
    在这里插入图片描述

1. 先看 shared 模块的消费关系

Android Settings 中把 shared 纳入同一个 Gradle 工程:

include(":app")
include(":shared")
project(":shared").projectDir = file("../shared")

应用模块再通过项目依赖消费:

dependencies {
    implementation(project(":shared"))
}

Android 这条路径不需要复制源码,也不需要把 KMP 类手动导出成普通 JAR。Gradle 负责把 shared 的 Android target 编译进应用。源码位置是 android/settings.gradle.kts 和 android/app/build.gradle.kts。

2. commonMain 只放可共享规则

共享服务的实现保持平台无关:

package com.example.lesson59.shared

class GreetingService {
    @Throws(IllegalArgumentException::class)
    fun greet(name: String): String {
        val normalizedName = name.trim()
        require(normalizedName.isNotEmpty()) { "Name must not be blank." }
        return "你好,$normalizedName!这条消息来自 KMP 共享模块。"
    }
}

这个类没有 Android Context、iOS UIKit 或 Flutter 类型,因此可以放进 commonMain。输入校验和返回文案属于业务规则,应该只维护一份;平台层只负责调用和错误翻译。

3. Gradle target 决定可交付平台

当前 shared/build.gradle.kts 声明了 Android、JVM 和三个 iOS target:

kotlin {
    androidTarget {
        compilations.configureEach {
            compileTaskProvider.configure {
                compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
            }
        }
    }

    jvm()

    val xcFramework = XCFramework("Shared")
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64(),
    ).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"
            isStatic = true
            xcFramework.add(this)
        }
    }
}

Android target 解决宿主依赖,JVM target 方便本地测试,iOS 三个 target 覆盖 Intel 模拟器、真机和 Apple Silicon 模拟器。XCFramework 把这些二进制合并为一个 iOS 可消费的交付物。这个配置的输入是 Kotlin 编译目标,输出是不同平台的库产物。

在这里插入图片描述

4. commonTest 验证共享规则

共享规则应该在进入平台之前先验证:

class GreetingServiceTest {
    private val service = GreetingService()

    @Test
    fun trimsNameAndReturnsGreeting() {
        assertEquals(
            "你好,Flutter!这条消息来自 KMP 共享模块。",
            service.greet("  Flutter  "),
        )
    }

    @Test
    fun rejectsBlankName() {
        assertFailsWith<IllegalArgumentException> {
            service.greet("   ")
        }
    }
}

测试覆盖了两条边界:带空格的正常输入会被规整,只有空格的输入会失败。它不能证明 Flutter 通道、Android Activity 或 iOS 框架链接成功,但能确保平台层拿到的是一套确定的业务规则。

5. Android 通过项目依赖进入 KMP

Android 宿主在 MainActivity 创建 GreetingService:

class MainActivity : FlutterActivity() {
    private val greetingService = GreetingService()

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "getGreeting" -> {
                        val name = call.argument<String>("name").orEmpty()
                        runCatching { greetingService.greet(name) }
                            .onSuccess(result::success)
                            .onFailure {
                                result.error("INVALID_NAME", it.message, null)
                            }
                    }
                    else -> result.notImplemented()
                }
            }
    }
}

MainActivity 只做两件事:把 Flutter 消息交给 shared,把 Kotlin 异常转换成 Flutter 错误。它没有重新实现 trim 和空值判断,因此避免了 Android 规则偏离 commonMain。

6. iOS 通过 XCFramework 接入

项目的 shared/README.md 给出了 iOS 产物命令:

./gradlew :shared:assembleSharedDebugXCFramework

产物路径为:

build/shared/XCFrameworks/debug/Shared.xcframework

生成框架后,需要把它加入 Runner 的 Frameworks, Libraries, and Embedded Content。iOS 的 AppDelegate.swift 使用条件编译,因此源码在框架尚未链接时仍然可以保留清晰的错误分支,而不是直接假设所有开发机都有框架。

7. canImport 保护了什么

#if canImport(Shared)
import Shared
#endif

当框架存在时,Swift 可以调用 GreetingService().greet(name:);当框架不存在时,代码进入 KMP_UNAVAILABLE 分支:

result(FlutterError(
  code: "KMP_UNAVAILABLE",
  message: "Shared.framework has not been linked.",
  details: nil
))

这个错误码把“业务输入失败”和“框架接入缺失”分开,给 CI 和开发者留下了明确排查入口。

8. 版本边界

配置当前值
Dart SDK>=3.9.2 <4.0.0
Flutter本地工具为 3.35.8-ohos-1.0.3-beta
Android Gradle Plugin8.9.1
Kotlin2.3.10
Android JVM11
iOS 交付物Shared.xcframework

Kotlin、Gradle、Xcode 和 Flutter 版本变化时,优先检查 target、框架任务和宿主链接,不要先修改业务类。

9. KMP 交付的故障排查

Android:

cd D:\lesson\lesson59\android
.\gradlew.bat :shared:jvmTest :shared:assembleDebug

iOS:

cd android
./gradlew :shared:assembleSharedDebugXCFramework
问题可能原因检查方式
shared 找不到Settings 没有 include检查 settings.gradle.kts
JVM target 不一致Kotlin 编译目标未统一查看 Android 与 KMP compilerOptions
iOS 找不到 Shared框架未生成或未加入 Runner检查 XCFramework 路径和 Xcode 链接项
Swift 调用参数报错KMP 导出签名变化对照生成框架中的 Swift API

当前 Flutter analyze 和 Flutter test 已通过;KMP Gradle 命令和 iOS 框架构建需要对应工具链,仍需在相应环境中执行。

10. 共享模块的验收清单

  • 业务规则只在 commonMain 维护一份。
  • Android 通过 implementation(project(“:shared”)) 消费模块。
  • iOS target 覆盖真机和两类模拟器。
  • commonTest 覆盖正常输入和非法输入。
  • iOS 框架未接入时能返回 KMP_UNAVAILABLE。
  • 平台层只做桥接,不重复业务校验。

共享模块的交付边界

KMP 的交付边界可以按三层理解:commonMain 保存业务规则,Gradle target 负责生成平台产物,Android/iOS 宿主负责把产物接入 Flutter。把这三层分开后,共享代码是否可用、平台链接是否成功、Flutter 通道是否正常就能分别验证。

因此,发布记录最好同时保留 commonTest 结果、Gradle 任务结果和宿主启动结果。只有三者都具备,才可以把“共享模块已交付”写成完整结论。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐