将 Android 应用程序迁移到 iOS – 教程
本教程使用 Android Studio,但你也可以在 IntelliJ IDEA 中学习。如果
本教程展示了如何将现有 Android 应用程序改造为跨平台应用程序,使其既能在 Android 上运行,也能在 iOS 上运行。 你将能够一次性在同一个地方为 Android 和 iOS 编写代码。
本教程使用一个 Android 示例应用程序,该应用程序有一个用于输入用户名和密码的单个屏幕。凭据将被检测并保存到内存数据库中。
为了使你的应用程序在 iOS 和 Android 上都能工作, 你首先需要将部分代码迁移到共享模块,使其成为跨平台代码。 之后,你将在 Android 应用程序中使用你的跨平台代码,然后在新创建的 iOS 应用程序中重用相同的代码。
如果你不熟悉 Kotlin Multiplatform,请先学习如何从头创建跨平台应用程序。
准备开发环境
在快速入门中,完成为 Kotlin Multiplatform 开发设置环境的说明。
你需要一台装有 macOS 的 Mac 电脑才能完成本教程中的某些步骤,例如运行 iOS 应用程序。 这是 Apple 的要求。
在 Android Studio 中,从版本控制创建新项目:
texthttps://github.com/Kotlin/kmp-integration-sample
master
分支包含项目的初始状态 – 一个简单的 Android 应用程序。 要查看包含 iOS 应用程序和共享模块的最终状态,请切换到final
分支。切换到 Project 视图:
使你的代码跨平台
要使你的代码跨平台,你需要遵循以下步骤:
决定哪些代码要跨平台
决定 Android 应用程序的哪些代码更适合与 iOS 共享,哪些代码应保留为 native。一个简单的规则是:尽可能多地重用你想要重用的部分。业务逻辑通常在 Android 和 iOS 上是相同的, 因此它是重用的绝佳候选。
在你的 Android 示例应用程序中,业务逻辑存储在 com.jetbrains.simplelogin.androidapp.data
包中。 你未来的 iOS 应用程序将使用相同的逻辑,因此你也应该使其跨平台。
为跨平台代码创建共享模块
用于 iOS 和 Android 的跨平台代码将存储在共享模块中。 Android Studio 和 IntelliJ IDEA 都提供了用于创建 Kotlin Multiplatform 共享模块的向导。
创建一个共享模块,以连接到现有的 Android 应用程序和你未来的 iOS 应用程序:
在 Android Studio 中,从主菜单中选择 File | New | New Module。
在模板列表中,选择 Kotlin Multiplatform Shared Module。 将库名称保留为
shared
并输入包名:textcom.jetbrains.simplelogin.shared
点击 Finish。向导会创建一个共享模块,相应地更改构建脚本,并开始 Gradle 同步。
设置完成后,你将在
shared
目录中看到以下文件结构:确保
shared/build.gradle.kts
文件中的kotlin.androidLibrary.minSdk
属性与app/build.gradle.kts
文件中同名属性的值匹配。
将代码添加到共享模块
现在你已经有了一个共享模块, 将一些通用代码添加到 commonMain/kotlin/com.jetbrains.simplelogin.shared
目录中以供共享:
创建一个包含以下代码的新
Greeting
类:kotlinpackage com.jetbrains.simplelogin.shared class Greeting { private val platform = getPlatform() fun greet(): String { return "Hello, ${platform.name}!" } }
用以下代码替换已创建文件中的代码:
在
commonMain/Platform.kt
中:kotlinpackage com.jetbrains.simplelogin.shared interface Platform { val name: String } expect fun getPlatform(): Platform
在
androidMain/Platform.android.kt
中:kotlinpackage 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
中:kotlinpackage 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
文件中:kotlindependencies { // ... implementation(project(":shared")) }
按照 IDE 的建议或使用 File | Sync Project with Gradle Files 菜单项同步 Gradle 文件。
在
app/src/main/java/
目录中,打开com.jetbrains.simplelogin.androidapp.ui.login
包中的LoginActivity.kt
文件。为了确保共享模块已成功连接到你的应用程序,通过向
onCreate()
方法添加Log.i()
调用,将greet()
函数的结果转储到日志中:kotlinoverride fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.i("Login Activity", "Hello from shared module: " + (Greeting().greet())) // ... }
按照 IDE 的建议导入缺失的类。
在工具栏中,点击
app
下拉菜单,然后点击调试图标:在 Logcat 工具窗口中,在日志中搜索 "Hello",你将找到来自共享模块的问候语:
使业务逻辑跨平台
你现在可以将业务逻辑代码提取到 Kotlin Multiplatform 共享模块中,并使其独立于平台。 这对于在 Android 和 iOS 上重用代码是必要的。
将
com.jetbrains.simplelogin.androidapp.data
业务逻辑代码从app
目录移动到shared/src/commonMain
目录中的com.jetbrains.simplelogin.shared
包。当 Android Studio 询问你想要做什么时,选择移动包,然后批准重构。
忽略所有关于平台相关代码的警告,然后点击 Refactor Anyway。
通过将其替换为跨平台 Kotlin 代码或使用 expect 和 actual 声明连接到 Android 特有的 API,来移除 Android 特有的代码。有关详细信息,请参阅以下部分:
用跨平台代码替换 Android 特有的代码
为了使你的代码在 Android 和 iOS 上都能很好地工作,请尽可能在移动的
data
目录中用 Kotlin 依赖项替换所有 JVM 依赖项。在
LoginDataValidator
类中,用匹配电子邮件验证模式的 Kotlin 正则表达式替换android.utils
包中的Patterns
类:kotlin// Before private fun isEmailValid(email: String) = Patterns.EMAIL_ADDRESS.matcher(email).matches()
kotlin// After 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
类的导入指令:kotlinimport android.util.Patterns
在
LoginDataSource
类中,将login()
函数中的IOException
替换为RuntimeException
。IOException
在 Kotlin/JVM 中不可用。```kotlin // Before return Result.Error(IOException("Error logging in", e)) ``` ```kotlin // After return Result.Error(RuntimeException("Error logging in", e)) ```
同时移除
IOException
的导入指令:kotlinimport java.io.IOException
从跨平台代码连接到平台特有的 API
在
LoginDataSource
类中,fakeUser
的通用唯一标识符 (UUID) 是使用java.util.UUID
类生成的,该类在 iOS 上不可用。kotlinval fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe")
尽管 Kotlin 标准库提供了用于 UUID 生成的实验性类, 但为了练习,让我们在这种情况下使用平台特有的功能。
在共享代码中为
randomUUID()
函数提供expect
声明,并在相应的源代码集中为每个平台(Android 和 iOS)提供其actual
实现。 你可以了解更多关于连接到平台特有的 API 的信息。将
login()
函数中的java.util.UUID.randomUUID()
调用更改为randomUUID()
调用,你将为每个平台实现该调用:kotlinval fakeUser = LoggedInUser(randomUUID(), "Jane Doe")
在
shared/src/commonMain
目录的com.jetbrains.simplelogin.shared
包中创建Utils.kt
文件,并提供expect
声明:kotlinpackage com.jetbrains.simplelogin.shared expect fun randomUUID(): String
在
shared/src/androidMain
目录的com.jetbrains.simplelogin.shared
包中创建Utils.android.kt
文件,并提供randomUUID()
在 Android 中的actual
实现:kotlinpackage com.jetbrains.simplelogin.shared import java.util.* actual fun randomUUID() = UUID.randomUUID().toString()
在
shared/src/iosMain
目录的com.jetbrains.simplelogin.shared
包中创建Utils.ios.kt
文件,并提供randomUUID()
在 iOS 中的actual
实现:kotlinpackage com.jetbrains.simplelogin.shared import platform.Foundation.NSUUID actual fun randomUUID(): String = NSUUID().UUIDString()
在
shared/src/commonMain
目录的LoginDataSource.kt
文件中导入randomUUID
函数:kotlinimport com.jetbrains.simplelogin.shared.randomUUID
现在,Kotlin 将为 Android 和 iOS 使用平台特有的 UUID 实现。
在 Android 上运行你的跨平台应用程序
在 Android 上运行你的跨平台应用程序,以确保它像以前一样正常工作。
使你的跨平台应用程序在 iOS 上工作
一旦你的 Android 应用程序实现跨平台,你就可以创建 iOS 应用程序并重用其中的共享业务逻辑。
在 Xcode 中创建 iOS 项目
在 Xcode 中,点击 File | New | Project。
选择 iOS app 模板并点击 Next。
将产品名称指定为 "simpleLoginIOS" 并点击 Next。
选择存储你的跨平台应用程序的目录作为项目位置,例如
kmp-integration-sample
。
在 Android Studio 中,你将获得以下结构:
你可以将 simpleLoginIOS
目录重命名为 iosApp
,以与跨平台项目的其他顶层目录保持一致。 为此,请关闭 Xcode,然后将 simpleLoginIOS
目录重命名为 iosApp
。 如果在 Xcode 打开的情况下重命名文件夹,你将收到警告并可能损坏你的项目。
配置 iOS 项目以使用 KMP framework
你可以直接设置 iOS 应用与 Kotlin Multiplatform 构建的 framework 之间的集成。 本教程不包括除此方法之外的其他方法,这些方法在 iOS 集成方法概述中有所介绍。
在 Android Studio 中,右键点击
iosApp/simpleLoginIOS.xcodeproj
目录并选择 Open In | Open In Associated Application 以在 Xcode 中打开 iOS 项目。在 Xcode 中,通过双击 Project 导航器中的项目名称来打开 iOS 项目设置。
在左侧的 Targets 部分中,选择 simpleLoginIOS,然后点击 Build Phases 标签页。
点击 + 图标并选择 New Run Script Phase。
将以下脚本粘贴到运行脚本字段中:
textcd "$SRCROOT/.." ./gradlew :shared:embedAndSignAppleFrameworkForXcode
禁用 Based on dependency analysis 选项。
这可以确保 Xcode 在每次构建期间都运行该脚本,并且每次都不会警告缺少输出依赖项。
将 Run Script 阶段向上移动,将其放置在 Compile Sources 阶段之前:
在 Build Settings 标签页上,禁用 Build Options 下的 User Script Sandboxing 选项:
如果你的构建配置与默认的
Debug
或Release
不同,请在 Build Settings 标签页上,在 User-Defined 下添加KOTLIN_FRAMEWORK_BUILD_TYPE
设置,并将其设置为Debug
或Release
。在 Xcode 中构建项目(主菜单中的 Product | Build)。 如果一切配置正确,项目应该能成功构建 (你可以安全地忽略“build phase will be run during every build”警告)
如果你在禁用 User Script Sandboxing 选项之前构建了项目,构建可能会失败: Gradle daemon 进程可能已被沙盒化,需要重新启动。 在再次构建项目之前,通过在项目目录(本例中为
kmp-integration-sample
)中运行此命令来停止它:shell./gradlew --stop
在 Android Studio 中设置 iOS 运行配置
确认 Xcode 设置正确后,返回 Android Studio:
在主菜单中选择 File | Sync Project with Gradle Files。Android Studio 会自动生成一个名为 simpleLoginIOS 的运行配置。
Android Studio 会自动生成一个名为 simpleLoginIOS 的运行配置,并将
iosApp
目录标记为链接的 Xcode project。在运行配置列表中,选择 simpleLoginIOS。 选择一个 iOS 模拟器,然后点击 Run 以检测 iOS 应用是否正常运行。
在 iOS 项目中使用共享模块
shared
模块的 build.gradle.kts
文件为每个 iOS target 定义了 binaries.framework.baseName
属性为 sharedKit
。 这是 Kotlin Multiplatform 为 iOS 应用生成并使用的 framework 的名称。
要测试集成,在 Swift 代码中添加对通用代码的调用:
在 Android Studio 中,打开
iosApp/simpleloginIOS/ContentView.swift
文件并导入 framework:swiftimport sharedKit
为了检测它是否正确连接,将
ContentView
结构更改为使用来自跨平台应用程序共享模块的greet()
函数:swiftstruct ContentView: View { var body: some View { Text(Greeting().greet()) .padding() } }
使用 Android Studio iOS 运行配置运行应用程序,查看结果:
再次更新
ContentView.swift
文件中的代码,以使用共享模块中的业务逻辑来渲染应用程序 UI:kotlin在
simpleLoginIOSApp.swift
文件中,导入sharedKit
模块并为ContentView()
函数指定实参:swiftimport SwiftUI import sharedKit @main struct SimpleLoginIOSApp: App { var body: some Scene { WindowGroup { ContentView(viewModel: .init(loginRepository: LoginRepository(dataSource: LoginDataSource()), loginValidator: LoginDataValidator())) } } }
再次运行 iOS 运行配置,查看 iOS 应用显示登录表单。
输入 "Jane" 作为用户名,"password" 作为密码。
由于你已经提前设置了集成, iOS 应用会使用通用代码验证输入:
享受成果 – 只需更新一次逻辑
现在你的应用程序是跨平台的了。你可以在 shared
模块中更新业务逻辑,并在 Android 和 iOS 上看到结果。
更改用户密码的验证逻辑:"password" 不应是有效选项。 为此,更新
LoginDataValidator
类的checkPassword()
函数 (要快速找到它,按两次 ,粘贴类名,然后切换到 Classes 标签页):kotlinpackage 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 应用程序,查看更改:
你可以查看本教程的最终代码。
还有什么可以共享?
你已经共享了应用程序的业务逻辑,但你也可以决定共享应用程序的其他层。 例如,ViewModel
类的代码对于 Android 和 iOS 应用程序几乎相同, 如果你的移动应用程序应该具有相同的展示层,你可以共享它。
接下来是什么?
一旦你将 Android 应用程序改造为跨平台,你可以继续:
你可以使用 Compose Multiplatform 在所有平台上创建统一的 UI:
你还可以查看社区资源: