Kotlin Multiplatform Development Help

让你的Android应用兼容iOS——教程

[//]: # (title: 让你的Android应用兼容iOS——教程) 学习如何将现有的Android应用改造为跨平台应用,使其同时支持Android和iOS。 你将能够在一处编写代码并针对Android和iOS进行测试。 本教程使用一个[示例Android应用](https://github.com/Kotlin/kmp-integration-sample),该应用包含一个用于输入用户名和密码的单屏界面。凭证会被验证并保存到内存数据库中。 要让你的应用同时兼容iOS和Android,首先需要将部分代码移至共享模块以实现跨平台。之后在Android应用中使用这些跨平台代码,并在新的iOS应用中复用相同代码。 > 如果你不熟悉Kotlin Multiplatform,建议先学习如何[从零创建跨平台应用](multiplatform-setup.md)。 > {style="tip"} ## 准备开发环境 1. [安装所有必要工具并更新至最新版本](multiplatform-setup.md)。 > 本教程中某些步骤需要macOS系统的Mac设备,包括编写iOS专用代码和运行iOS应用。由于苹果公司的要求,这些步骤无法在Windows等其他操作系统上完成。 > {style="note"} 2. 在Android Studio中通过版本控制创建新项目: ```text https://github.com/Kotlin/kmp-integration-sample
  1. 将视图从Android切换至Project

    项目视图

实现代码跨平台

要实现代码跨平台,需按以下步骤操作:

  1. 确定哪些代码需要跨平台

  2. 为跨平台代码创建共享模块

  3. 测试代码共享

  4. 为Android应用添加共享模块依赖

  5. 实现业务逻辑跨平台

  6. 在Android上运行跨平台应用

确定跨平台代码范围

决定Android应用中哪些代码适合与iOS共享,哪些应保持原生。简单原则是:尽可能复用高共享价值的代码。业务逻辑通常对Android和iOS都通用,因此是理想的复用候选。

在示例应用中,业务逻辑存储在com.jetbrains.simplelogin.androidapp.data包中。未来的iOS应用将使用相同逻辑,故应使其跨平台。

待共享的业务逻辑

创建跨平台代码共享模块

用于iOS和Android的跨平台代码将存储在共享模块中。从Meerkat版本开始,Android Studio提供了创建此类模块的向导。

创建共享模块并连接到现有Android应用和未来iOS应用:

  1. 在Android Studio主菜单中选择File | New | New Module

  2. 在模板列表中选择Kotlin Multiplatform Shared Module

  3. 保留库名称shared ,输入包名com.jetbrains.simplelogin.shared

  4. 点击Finish 。向导将创建共享模块、修改构建脚本并启动Gradle同步

  5. 完成后, shared目录将显示如下结构:

    共享目录最终结构
  6. 确保shared/build.gradle.kts文件中的kotlin.androidLibrary.minSdk属性与app/build.gradle.kts中的对应值一致

向共享模块添加代码

commonMain/kotlin/com.jetbrains.simplelogin.shared目录中添加共享代码:

  1. 创建包含以下代码的Greeting类:

    package com.jetbrains.simplelogin.shared class Greeting { private val platform = getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } }
  2. 替换以下文件内容:

    • commonMain/Platform.kt:

      package com.jetbrains.simplelogin.shared interface Platform { val name: String } expect fun getPlatform(): Platform
    • androidMain/Platform.android.kt:

      package com.jetbrains.simplelogin.shared import android.os.Build class AndroidPlatform : Platform { override val name: String = "Android ${Build.VERSION.SDK_INT}" } actual fun getPlatform(): Platform = AndroidPlatform()
    • iosMain/Platform.ios.kt:

      package com.jetbrains.simplelogin.shared import platform.UIKit.UIDevice class IOSPlatform: Platform { override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion } actual fun getPlatform(): Platform = IOSPlatform()

想了解项目结构基础,可参阅Kotlin Multiplatform项目结构基础

为Android应用添加共享模块依赖

要在Android应用中使用跨平台代码,需连接共享模块,转移业务逻辑代码并使其跨平台。

  1. app/build.gradle.kts中添加共享模块依赖:

    dependencies { // ... implementation(project(":shared")) }
  2. 通过IDE提示或File | Sync Project with Gradle Files菜单同步Gradle文件

    同步Gradle文件
  3. app/src/main/java/目录中打开LoginActivity.kt文件

  4. 通过添加Log.i()调用验证共享模块连接:

    override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i("Login Activity", "Hello from shared module: " + (Greeting().greet())) // ... }
  5. 按Android Studio提示导入缺失类

  6. 在工具栏点击app下拉菜单,选择调试图标:

    调试应用列表
  7. Logcat工具窗口搜索"Hello",可找到共享模块的问候语:

    来自共享模块的问候

实现业务逻辑跨平台

现在可将业务逻辑代码提取到Kotlin Multiplatform共享模块并使其平台无关,这对Android和iOS代码复用至关重要。

  1. 将业务逻辑代码从app目录的com.jetbrains.simplelogin.androidapp.data移至shared/src/commonMaincom.jetbrains.simplelogin.shared

    拖放业务逻辑代码包
  2. 选择移动操作并确认重构

    重构业务逻辑包
  3. 忽略所有平台相关代码警告并点击Continue

    平台相关代码警告
  4. 通过以下方式移除Android专用代码:

    用跨平台代码替换Android专用代码

    data目录中尽可能用Kotlin依赖替换JVM依赖:

    1. LoginDataSource类中将login()函数的IOException替换为RuntimeException

      // 替换前 return Result.Error(IOException("Error logging in", e))
      // 替换后 return Result.Error(RuntimeException("Error logging in", e))
    2. 移除IOException导入:

      import java.io.IOException
    3. 用Kotlin正则表达式替换android.utils包的Patterns类:

      // 替换前 private fun isEmailValid(email: String) = Patterns.EMAIL_ADDRESS.matcher(email).matches()
      // 替换后 private fun isEmailValid(email: String) = emailRegex.matches(email) companion object { private val emailRegex = ("[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+").toRegex() }
    4. 移除Patterns类导入:

      import android.util.Patterns

    从跨平台代码连接平台特定API

    LoginDataSource中使用java.util.UUID生成fakeUser的UUID,这在iOS不可用。

    val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")

    虽然Kotlin标准库提供实验性UUID生成类 ,但此处我们练习使用平台特定功能。

    通过expect声明共享代码中的randomUUID()函数,并在各平台源集提供actual实现。了解更多连接平台特定API

    1. login()函数中的调用改为randomUUID()

      val fakeUser = LoggedInUser(randomUUID(), "Jane Doe")
    2. shared/src/commonMain创建Utils.kt并提供expect声明:

      package com.jetbrains.simplelogin.shared expect fun randomUUID(): String
    3. shared/src/androidMain创建Utils.android.kt提供Android实现:

      package com.jetbrains.simplelogin.shared import java.util.* actual fun randomUUID() = UUID.randomUUID().toString()
    4. shared/src/iosMain创建Utils.ios.kt提供iOS实现:

      package com.jetbrains.simplelogin.shared import platform.Foundation.NSUUID actual fun randomUUID(): String = NSUUID().UUIDString()
    5. LoginDataSource.kt中导入randomUUID函数:

      import com.jetbrains.simplelogin.shared.randomUUID

现在Kotlin将为Android和iOS使用平台特定的UUID实现。

在Android上运行跨平台应用

运行Android版跨平台应用验证功能。

Android登录应用

使跨平台应用兼容iOS

完成Android应用跨平台改造后,可创建iOS应用并复用共享业务逻辑。

  1. 在Xcode中创建iOS项目

  2. 配置iOS项目使用KMP框架

  3. 在Android Studio设置iOS运行配置

  4. 在iOS项目使用共享模块

在Xcode中创建iOS项目

  1. 在Xcode中选择File | New | Project

  2. 选择iOS应用模板点击Next

    iOS项目模板
  3. 输入产品名"simpleLoginIOS"点击Next

    iOS项目设置
  4. 选择跨平台应用所在目录作为项目位置

在Android Studio中可见如下结构:

Android Studio中的iOS项目

可将simpleLoginIOS目录重命名为iosApp以保持项目结构一致。重命名前需关闭Xcode。

重命名后的iOS目录

配置iOS项目使用KMP框架

直接设置iOS应用与Kotlin Multiplatform构建框架的集成。其他方法参见iOS集成方法概述

  1. 在Xcode中双击项目名打开设置

  2. Targets中选择simpleLoginIOS ,点击Build Phases标签

  3. 点击**+添加New Run Script Phase**

  4. 展开新建项并粘贴脚本:

    cd "$SRCROOT/.." ./gradlew :shared:embedAndSignAppleFrameworkForXcode
    添加脚本
  5. Run Script阶段移至Compile Sources之前

  6. Build Settings中禁用User Script Sandboxing

    禁用沙盒

  7. 构建项目(Product | Build )。若配置正确将成功构建(可忽略"每次构建都会运行"的警告)

在Android Studio设置iOS运行配置

Xcode配置正确后,可在Android Studio设置iOS运行配置:

  1. 主菜单中选择Run | Edit configurations

  2. 点击加号选择iOS Application

  3. 命名配置为"SimpleLoginIOS"

  4. Xcode project file字段选择simpleLoginIOS.xcodeproj文件位置

  5. Execution target列表选择模拟环境点击OK

    iOS运行配置
  6. 点击运行按钮测试新建配置:

    运行配置列表

在iOS项目使用共享模块

shared模块的build.gradle.kts为每个iOS目标定义了binaries.framework.baseName属性为sharedKit ,这是Kotlin Multiplatform为iOS应用构建的框架名称。

测试集成:

  1. 在Android Studio中打开iosApp/simpleloginIOS/ContentView.swift并导入框架:

    import sharedKit
  2. 修改ContentView结构使用共享模块的greet()函数:

    struct ContentView: View { var body: some View { Text(Greeting().greet()) .padding() } }
  3. 通过Android Studio运行iOS应用查看结果:

    共享模块问候语
  4. 再次更新ContentView.swift使用共享模块业务逻辑渲染UI:

    import SwiftUI import sharedKit struct ContentView: View { @State private var username: String = "" @State private var password: String = "" @ObservedObject var viewModel: ContentView.ViewModel var body: some View { VStack(spacing: 15.0) { ValidatedTextField(titleKey: "Username", secured: false, text: $username, errorMessage: viewModel.formState.usernameError, onChange: { viewModel.loginDataChanged(username: username, password: password) }) ValidatedTextField(titleKey: "Password", secured: true, text: $password, errorMessage: viewModel.formState.passwordError, onChange: { viewModel.loginDataChanged(username: username, password: password) }) Button("Login") { viewModel.login(username: username, password: password) }.disabled(!viewModel.formState.isDataValid || (username.isEmpty && password.isEmpty)) } .padding(.all) } } struct ValidatedTextField: View { let titleKey: String let secured: Bool @Binding var text: String let errorMessage: String? let onChange: () -> () @ViewBuilder var textField: some View { if secured { SecureField(titleKey, text: $text) } else { TextField(titleKey, text: $text) } } var body: some View { ZStack { textField .textFieldStyle(RoundedBorderTextFieldStyle()) .autocapitalization(.none) .onChange(of: text) { _ in onChange() } if let errorMessage = errorMessage { HStack { Spacer() FieldTextErrorHint(error: errorMessage) }.padding(.horizontal, 5) } } } } struct FieldTextErrorHint: View { let error: String @State private var showingAlert = false var body: some View { Button(action: { self.showingAlert = true }) { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) } .alert(isPresented: $showingAlert) { Alert(title: Text("Error"), message: Text(error), dismissButton: .default(Text("Got it!"))) } } } extension ContentView { struct LoginFormState { let usernameError: String? let passwordError: String? var isDataValid: Bool { get { return usernameError == nil && passwordError == nil } } } class ViewModel: ObservableObject { @Published var formState = LoginFormState(usernameError: nil, passwordError: nil) let loginValidator: LoginDataValidator let loginRepository: LoginRepository init(loginRepository: LoginRepository, loginValidator: LoginDataValidator) { self.loginRepository = loginRepository self.loginValidator = loginValidator } func login(username: String, password: String) { if let result = loginRepository.login(username: username, password: password) as? ResultSuccess { print("Successful login. Welcome, \(result.data.displayName)") } else { print("Error while logging in") } } func loginDataChanged(username: String, password: String) { formState = LoginFormState( usernameError: (loginValidator.checkUsername(username: username) as? LoginDataValidator.ResultError)?.message, passwordError: (loginValidator.checkPassword(password: password) as? LoginDataValidator.ResultError)?.message) } } }

  5. simpleLoginIOSApp.swift中导入sharedKit并指定ContentView()参数:

    import SwiftUI import sharedKit @main struct SimpleLoginIOSApp: App { var body: some Scene { WindowGroup { ContentView(viewModel: .init(loginRepository: LoginRepository(dataSource: LoginDataSource()), loginValidator: LoginDataValidator())) } } }
  6. 再次运行iOS应用查看登录表单

  7. 输入用户名"Jane"和密码"password"

  8. 由于已设置集成 ,iOS应用使用共享代码验证输入:

    简单登录应用

享受成果——一次更新全局生效

现在你的应用已实现跨平台。更新shared模块的业务逻辑可同时影响Android和iOS。

  1. 修改密码验证逻辑:禁止使用"password"。更新LoginDataValidator类的checkPassword()函数:

    package com.jetbrains.simplelogin.shared.data class LoginDataValidator { //... fun checkPassword(password: String): Result { return when { password.length < 5 -> Result.Error("Password must be >5 characters") password.lowercase() == "password" -> Result.Error("Password shouldn't be \"password\"") else -> Result.Success } } //... }
  2. 从Android Studio运行iOS和Android应用查看变更:

    iOS密码错误
    Android密码错误

可查看教程最终代码

还能共享什么?

除业务逻辑外,还可以共享其他应用层。例如AndroidiOSViewModel类代码几乎相同,若移动应用需相同表示层可共享该代码。

后续步骤

完成Android应用跨平台改造后,可以:

也可查看社区资源:

21 四月 2025