让你的Android应用兼容iOS——教程
将视图从Android切换至Project:
实现代码跨平台
要实现代码跨平台,需按以下步骤操作:
确定哪些代码需要跨平台
为跨平台代码创建共享模块
测试代码共享
为Android应用添加共享模块依赖
实现业务逻辑跨平台
在Android上运行跨平台应用
确定跨平台代码范围
决定Android应用中哪些代码适合与iOS共享,哪些应保持原生。简单原则是:尽可能复用高共享价值的代码。业务逻辑通常对Android和iOS都通用,因此是理想的复用候选。
在示例应用中,业务逻辑存储在com.jetbrains.simplelogin.androidapp.data
包中。未来的iOS应用将使用相同逻辑,故应使其跨平台。

创建跨平台代码共享模块
用于iOS和Android的跨平台代码将存储在共享模块中。从Meerkat版本开始,Android Studio提供了创建此类模块的向导。
创建共享模块并连接到现有Android应用和未来iOS应用:
在Android Studio主菜单中选择File | New | New Module
在模板列表中选择Kotlin Multiplatform Shared Module
保留库名称
shared
,输入包名com.jetbrains.simplelogin.shared
点击Finish 。向导将创建共享模块、修改构建脚本并启动Gradle同步
完成后,
shared
目录将显示如下结构:确保
shared/build.gradle.kts
文件中的kotlin.androidLibrary.minSdk
属性与app/build.gradle.kts
中的对应值一致
向共享模块添加代码
在commonMain/kotlin/com.jetbrains.simplelogin.shared
目录中添加共享代码:
创建包含以下代码的
Greeting
类:package com.jetbrains.simplelogin.shared class Greeting { private val platform = getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } }替换以下文件内容:
commonMain/Platform.kt
:package com.jetbrains.simplelogin.shared interface Platform { val name: String } expect fun getPlatform(): PlatformandroidMain/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应用中使用跨平台代码,需连接共享模块,转移业务逻辑代码并使其跨平台。
在
app/build.gradle.kts
中添加共享模块依赖:dependencies { // ... implementation(project(":shared")) }通过IDE提示或File | Sync Project with Gradle Files菜单同步Gradle文件
在
app/src/main/java/
目录中打开LoginActivity.kt
文件通过添加
Log.i()
调用验证共享模块连接:override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i("Login Activity", "Hello from shared module: " + (Greeting().greet())) // ... }按Android Studio提示导入缺失类
在工具栏点击
app
下拉菜单,选择调试图标:在Logcat工具窗口搜索"Hello",可找到共享模块的问候语:
实现业务逻辑跨平台
现在可将业务逻辑代码提取到Kotlin Multiplatform共享模块并使其平台无关,这对Android和iOS代码复用至关重要。
将业务逻辑代码从
app
目录的com.jetbrains.simplelogin.androidapp.data
移至shared/src/commonMain
的com.jetbrains.simplelogin.shared
包选择移动操作并确认重构
忽略所有平台相关代码警告并点击Continue
通过以下方式移除Android专用代码:
用跨平台代码替换Android专用代码
在
data
目录中尽可能用Kotlin依赖替换JVM依赖:在
LoginDataSource
类中将login()
函数的IOException
替换为RuntimeException
// 替换前 return Result.Error(IOException("Error logging in", e))// 替换后 return Result.Error(RuntimeException("Error logging in", e))移除
IOException
导入:import java.io.IOException用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() }移除
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。将
login()
函数中的调用改为randomUUID()
:val fakeUser = LoggedInUser(randomUUID(), "Jane Doe")在
shared/src/commonMain
创建Utils.kt
并提供expect
声明:package com.jetbrains.simplelogin.shared expect fun randomUUID(): String在
shared/src/androidMain
创建Utils.android.kt
提供Android实现:package com.jetbrains.simplelogin.shared import java.util.* actual fun randomUUID() = UUID.randomUUID().toString()在
shared/src/iosMain
创建Utils.ios.kt
提供iOS实现:package com.jetbrains.simplelogin.shared import platform.Foundation.NSUUID actual fun randomUUID(): String = NSUUID().UUIDString()在
LoginDataSource.kt
中导入randomUUID
函数:import com.jetbrains.simplelogin.shared.randomUUID
现在Kotlin将为Android和iOS使用平台特定的UUID实现。
在Android上运行跨平台应用
运行Android版跨平台应用验证功能。

使跨平台应用兼容iOS
完成Android应用跨平台改造后,可创建iOS应用并复用共享业务逻辑。
在Xcode中创建iOS项目
配置iOS项目使用KMP框架
在Android Studio设置iOS运行配置
在iOS项目使用共享模块
在Xcode中创建iOS项目
在Xcode中选择File | New | Project
选择iOS应用模板点击Next
输入产品名"simpleLoginIOS"点击Next
选择跨平台应用所在目录作为项目位置
在Android Studio中可见如下结构:

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

配置iOS项目使用KMP框架
直接设置iOS应用与Kotlin Multiplatform构建框架的集成。其他方法参见iOS集成方法概述。
在Xcode中双击项目名打开设置
在Targets中选择simpleLoginIOS ,点击Build Phases标签
点击**+添加New Run Script Phase**
展开新建项并粘贴脚本:
cd "$SRCROOT/.." ./gradlew :shared:embedAndSignAppleFrameworkForXcode将Run Script阶段移至Compile Sources之前
在Build Settings中禁用User Script Sandboxing
构建项目(Product | Build )。若配置正确将成功构建(可忽略"每次构建都会运行"的警告)
在Android Studio设置iOS运行配置
Xcode配置正确后,可在Android Studio设置iOS运行配置:
主菜单中选择Run | Edit configurations
点击加号选择iOS Application
命名配置为"SimpleLoginIOS"
在Xcode project file字段选择
simpleLoginIOS.xcodeproj
文件位置从Execution target列表选择模拟环境点击OK:
点击运行按钮测试新建配置:
在iOS项目使用共享模块
shared
模块的build.gradle.kts
为每个iOS目标定义了binaries.framework.baseName
属性为sharedKit
,这是Kotlin Multiplatform为iOS应用构建的框架名称。
测试集成:
在Android Studio中打开
iosApp/simpleloginIOS/ContentView.swift
并导入框架:import sharedKit修改
ContentView
结构使用共享模块的greet()
函数:struct ContentView: View { var body: some View { Text(Greeting().greet()) .padding() } }通过Android Studio运行iOS应用查看结果:
再次更新
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) } } }在
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())) } } }再次运行iOS应用查看登录表单
输入用户名"Jane"和密码"password"
由于已设置集成 ,iOS应用使用共享代码验证输入:
享受成果——一次更新全局生效
现在你的应用已实现跨平台。更新shared
模块的业务逻辑可同时影响Android和iOS。
修改密码验证逻辑:禁止使用"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 } } //... }从Android Studio运行iOS和Android应用查看变更:
可查看教程最终代码。
还能共享什么?
除业务逻辑外,还可以共享其他应用层。例如Android和iOS的ViewModel
类代码几乎相同,若移动应用需相同表示层可共享该代码。
后续步骤
完成Android应用跨平台改造后,可以:
也可查看社区资源: