网站会员注册系统源码,企业网络营销策划方案设计,网络科技公司起名大全参考,门户网站建设汇报材料前言
几个月前 Compose Multiplatform 的 iOS 支持就宣布进入了 Alpha 阶段#xff0c;这意味着它已经具备了一定的可用性。
在它发布 Alpha 的时候#xff0c;我就第一时间尝鲜#xff0c;但是只是浅尝辄止#xff0c;没有做过多的探索#xff0c;最近恰好有点时间这意味着它已经具备了一定的可用性。
在它发布 Alpha 的时候我就第一时间尝鲜但是只是浅尝辄止没有做过多的探索最近恰好有点时间于是我又重新开始学习 Compose Multiplatform 并且尝试移植我已有的项目使其支持 iOS并且将移植过程整理记录了下来即为本文。
这次移植我选择的依旧是这个使用 Compose 写的计算器项目 calculator-Compose-MultiPlatform 。本来这次我想着移植一个涉及技术稍微多一点的项目的比如这个 githubAppByCompose但是我仔细研究了一下毕竟现在 Compose Multiplatform 还处于实验阶段好多对应的功能和库都还没有所以只能选择移植前者。
对于这个计算器项目最开始只是一个使用 Compose 实现的纯 Android 项目后来移植到了支持 Android 和 桌面 端所以其实现在再给它添加上 iOS 支持也算是补齐了最后一个平台了哈哈。
在开始阅读本文之前我会假设你已经了解并且知道 Compsoe 的基本使用方法。
为了更好的理解本文可能需要首先阅读这两篇前置文章
【译】快速开始 Compose 跨平台项目Kotlin Compose Multiplatform 跨平台Android端、桌面端开发实践之使用 SQLDelight 将数据储存至数据库
前言的最后看一下运行效果
Android 端 ios 端 桌面端 开始移植
准备工作
首当其冲我们需要为 iOS 的支持更改编译配置文件和添加对应的平台特定代码。
在我的这个项目中我通过以下几个步骤为其添加了对 iOS 的支持
更改共享代码模块名称
把公用代码模块由 common 改为 shared 其实这里不用改也行只是模板配置文件中写的 iOS 使用的公用代码路径是 shared 但是直接改模块名比改配置文件简单多了所以我们直接把模块名改了就好了。
改完之后切记要检查一下其他模块引用的名字是否改了以及注意检查一下包名是否正确。
添加 native.cocoapods 插件
在 shared 模块的 build.gradle.kts 文件的 plugins 增加 native.cocoapods 插件
plugins {kotlin(native.cocoapods)// ……
}添加 cocoapods 配置
在 shared 模块的 build.gradle.kts 文件的 kotlin 下增加 cocoapods 相应的配置
kotlin {// ……iosX64()iosArm64()iosSimulatorArm64()cocoapods {version 1.0.0summary Some description for the Shared Modulehomepage Link to the Shared Module homepageios.deploymentTarget 14.1podfile project.file(../iosApp/Podfile)framework {baseName sharedisStatic true}extraSpecAttributes[resources] [src/commonMain/resources/**, src/iosMain/resources/**]}// ……
}配置 iOS 源集
在 shared 模块的 build.gradle.kts 文件的 kotlin 中的 sourceSets 下增加 iOS 的源集配置
kotlin {// ……sourceSets {// ……val iosX64Main by gettingval iosArm64Main by gettingval iosSimulatorArm64Main by gettingval iosMain by creating {dependsOn(commonMain)iosX64Main.dependsOn(this)iosArm64Main.dependsOn(this)iosSimulatorArm64Main.dependsOn(this)}}
}添加其他插件
在项目根目录下的 settings.gradle.kts 文件的 pluginManagement 中的 plugins 增加插件配置
pluginManagement {//……plugins {kotlin(jvm).version(extra[kotlin.version] as String)// ……}
}添加 iOS 项目文件
直接把官方模板中的 iosAPP 模块整个目录复制到项目根目录来。
需要注意的是其实这个 iosAPP 目录并不是一个 idea 模块而是一个 Xcode 项目。但是目前暂时不需要知道这是什么只需要把相应的文件整个复制到自己项目中就行了。
然后把官方模版中的 sahred - iosMain 文件夹整个复制到 我们项目的 sahred 模块根目录中。
适配代码
在这一节中主要需要适配的有两种类型的代码
一是之前就已经在项目中声明了的 expect 函数需要为 iOS 也加上对应的 actual 函数。
二是需要将原本使用到的 jvm 相关或者说所有使用 java 实现的库和相关代码都需要重新编写或适配。
因为不同于 Android 和 桌面端kotlin 最终会被编译成 jvm 代码在 iOS 端kotlin 会编译成 native 代码所以所有使用 java 写的代码将无法再使用。
这也就是我前言中说的为啥不选择移植更复杂的项目的原因就是因为我在其中引用了大量的使用 java 编写的第三方库而这些第三方库又暂时没有使用纯 kotlin 实现的可用替代品。
下面我们就开始适配代码。
更改入口
为了保证三端界面一致我们将原本的UI界面再额外的抽出一个统一的入口函数 APP()将其放到 shared 模块的 common 包下
Composable
fun APP(standardChannelTop: ChannelStandardAction? null,programmerChannelTop: ChannelProgrammerAction? null,
) {val homeChannel remember { ChannelHomeAction() }val homeFlow remember(homeChannel) { homeChannel.consumeAsFlow() }val homeState homePresenter(homeFlow)val standardChannel standardChannelTop ?: remember { Channel() }val standardFlow remember(standardChannel) { standardChannel.consumeAsFlow() }val standardState standardPresenter(standardFlow)val programmerChannel programmerChannelTop ?: remember { Channel() }val programmerFlow remember(programmerChannel) { programmerChannel.consumeAsFlow() }val programmerState programmerPresenter(programmerFlow)CalculatorComposeTheme {val backgroundColor MaterialTheme.colors.backgroundSurface(modifier Modifier.fillMaxSize(),color backgroundColor) {HomeScreen(homeChannel,homeState,standardChannel,standardState,programmerChannel,programmerState)}}
}并且因为不同平台需要差异化实现部分功能以及目前我还没找到一个好使的支持跨平台的依赖注入库所以我索性将所有 控制(channel) 和 状态(state) 都提升到了最顶层作为参数传递给下面的 Compose 函数。
然后更改三端各自的入口函数
Android (android 模块下的 MainActivity.kt 文件)
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {APP()}}
}desktop (dektop 模块下的 Main.kt 文件)
fun main() application {val state if (Config.boardType.value KeyboardTypeStandard) {rememberWindowState(size defaultWindowSize, position defaultWindowPosition)} else {rememberWindowState(size landWindowSize, position defaultWindowPosition)}val standardChannel remember { ChannelStandardAction() }val programmerChannel remember { ChannelProgrammerAction() }Window(onCloseRequest ::exitApplication,state state,title Text.AppName,icon painterResource(icon.png),alwaysOnTop Config.isFloat.value,onKeyEvent {if (isKeyTyped(it)) {val btnIndex asciiCode2BtnIndex(it.utf16CodePoint)if (btnIndex ! -1) {if (Config.boardType.value KeyboardTypeStandard) {standardChannel.trySend(StandardAction.ClickBtn(btnIndex))}else {programmerChannel.trySend(ProgrammerAction.ClickBtn(btnIndex))}}}true}) {APP()}
}
iOS ( shared模块 下的 main.ios.kt 文件)
fun MainViewController() ComposeUIViewController {APP()
}注意不同于其他平台iOS 的入口函数在 shared模块 中。
当然你要是想直接改 iosAPP 目录中的代码那也不是不行只是对于我们安卓开发来说还是直接改 shared 更方便点。
实现 iOS 的 平台代码
之前我们的项目中有几个地方的实现依赖于平台所以写了一些 expect 函数现在我们需要给 iOS 实现对应的 actual 函数。
首先在 shared 模块的 iosMain 包中创建一个包路径保持和 commonMain 的 expect 函数包一致 注意 包路径一定要一致不然会编译失败我就在这里踩了坑没注意到包名不一样 debug 了好久。
这个项目中的平台差异函数主要有四个
控制振动
因为我对 iOS 一窍不通所以不知道怎么写索性直接留空了
actual fun vibrateOnClick() {}actual fun vibrateOnError() {}actual fun vibrateOnClear() {}actual fun vibrateOnEqual() {}控制屏幕旋转和显示小窗
这里同上不知道怎么写直接留空
actual fun showFloatWindows() {}actual fun changeKeyBoardType(changeTo: Int) {}数据库(sqldelight)
actual fun createDriver(): SqlDriver {return NativeSqliteDriver(HistoryDatabase.Schema, history.db)
}关于使用 sqldelight 的详细介绍可以看前言中的前置文章了解。
其实这里这样写是编译不通过的因为还没加 sqldelight 依赖下面介绍一下怎么加依赖这里又是一个大坑。
给 iOS 添加 sqldelight 支持
首先在 shared 模块下的 build.gradle.kts 文件中的 kotlin - sourceSets - iosMain 添加 sqldelight 的 驱动依赖
kotlin {// ……sourceSets {// ……val iosMain by creating {// ……dependencies {implementation(app.cash.sqldelight:native-driver:2.0.0)}}}
}此时如果你直接 sync gradle 后编译运行大概率会报错
Undefined symbols for architecture arm64:
_sqlite3_bind_text16, referenced from:
_SQLiter_SQLiteStatement_nativeBindString in app(combined.o)
_sqlite3_bind_int64, referenced from:
_SQLiter_SQLiteStatement_nativeBindLong in app(combined.o)
_sqlite3_last_insert_rowid, referenced from:
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
_sqlite3_reset, referenced from:
_SQLiter_SQLiteConnection_nativeResetStatement in app(combined.o)
_sqlite3_changes, referenced from:
_SQLiter_SQLiteStatement_nativeExecuteForChangedRowCount in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
_sqlite3_open_v2, referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
_sqlite3_db_config, referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
_sqlite3_busy_timeout, referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
_sqlite3_trace, referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
_sqlite3_bind_parameter_index, referenced from:
_SQLiter_SQLiteConnection_nativeBindParameterIndex in app(combined.o)
_sqlite3_column_bytes, referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetString in app(combined.o)
_SQLiter_SQLiteConnection_nativeColumnGetBlob in app(combined.o)
_sqlite3_finalize, referenced from:
_SQLiter_SQLiteStatement_nativeFinalizeStatement in app(combined.o)
_sqlite3_column_text, referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetString in app(combined.o)
_sqlite3_column_name, referenced from:
_SQLiter_SQLiteConnection_nativeColumnName in app(combined.o)
_sqlite3_bind_double, referenced from:
_SQLiter_SQLiteStatement_nativeBindDouble in app(combined.o)
_sqlite3_profile, referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
_sqlite3_close, referenced from:
_SQLiter_SQLiteConnection_nativeClose in app(combined.o)
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
_sqlite3_prepare16_v2, referenced from:
_SQLiter_SQLiteConnection_nativePrepareStatement in app(combined.o)
_sqlite3_column_type, referenced from:
_SQLiter_SQLiteConnection_nativeColumnIsNull in app(combined.o)
_SQLiter_SQLiteConnection_nativeColumnType in app(combined.o)
_sqlite3_column_count, referenced from:
_SQLiter_SQLiteConnection_nativeColumnCount in app(combined.o)
_sqlite3_bind_blob, referenced from:
_SQLiter_SQLiteStatement_nativeBindBlob in app(combined.o)
_sqlite3_db_readonly, referenced from:
_SQLiter_SQLiteConnection_nativeOpen in app(combined.o)
_sqlite3_column_int64, referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetLong in app(combined.o)
_sqlite3_bind_null, referenced from:
_SQLiter_SQLiteStatement_nativeBindNull in app(combined.o)
_sqlite3_extended_errcode, referenced from:
android::throw_sqlite3_exception(sqlite3*) in app(combined.o)
android::throw_sqlite3_exception(sqlite3*, char const*) in app(combined.o)
_sqlite3_column_double, referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetDouble in app(combined.o)
_sqlite3_column_blob, referenced from:
_SQLiter_SQLiteConnection_nativeColumnGetBlob in app(combined.o)
_sqlite3_step, referenced from:
_SQLiter_SQLiteConnection_nativeStep in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecute in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForChangedRowCount in app(combined.o)
_SQLiter_SQLiteStatement_nativeExecuteForLastInsertedRowId in app(combined.o)
_sqlite3_clear_bindings, referenced from:
_SQLiter_SQLiteConnection_nativeClearBindings in app(combined.o)
_sqlite3_errmsg, referenced from:
android::throw_sqlite3_exception(sqlite3*) in app(combined.o)
android::throw_sqlite3_exception(sqlite3*, char const*) in app(combined.o)
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)这是因为 ios 的 Xcode 项目没有添加 sqlite 依赖我们还需要为 ios 单独添加 sqlite 依赖。
ios 使用的是 cocoapods 进行依赖管理我们需要使用 pod 添加依赖。
我们有两种选择
一是在 shared 模块的 build.gradle.kts 中相应的位置添加 pod 依赖配置。
二是直接在 pod 配置文件中添加。
这里我们就选择直接改 pod 的配置文件。
打开项目根目录下的 iosAPP 目录中的 Podfile 文件在其中添加 sqlite3 依赖:
target iosApp do# ……pod sqlite3, ~ 3.42.0# ……end添加完记得需要 sync 一下 gradle。
此时再编译运行大概率还是会报错
ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a不用担心再在刚才的配置文件中加上这么一段
# iosApps podfile
post_install do |installer|installer.pods_project.targets.each do |target|target.build_configurations.each do |config|config.build_settings[IPHONEOS_DEPLOYMENT_TARGET] 14.1endend
end此时应该就不会有任何问题了。
适配 jvm 相关代码
正如我们在上一节所说由于 iOS 使用 native 代码所以项目中就不能再使用 java 代码包括引用的第三方库也是。
在我这个项目中涉及到需要适配的主要有两个地方。一个是进制转换时使用到了 java 的 Long 类的方法另一个就是运算时使用的是 BigInteger 和 BigDecimal 。
进制转换
之前的代码使用的是 java 中的 java.lang.Long.toXXXString 。
这里适配起来其实很简单要么自己使用 kotlin 实现一个进制转换工具类要么就像我一样直接把 Long.java 中需要的部分 CV 一下然后使用 Android studio 的 java 转 kotlin 一键转换就行了。
下面就是我转好的工具类
package com.equationl.common.utilsimport kotlin.math.maxobject LongUtil {val digits charArrayOf(0, 1, 2, 3, 4, 5,6, 7, 8, 9, a, b,c, d, e, f, g, h,i, j, k, l, m, n,o, p, q, r, s, t,u, v, w, x, y, z)fun toBinaryString(i: Long): String {return toUnsignedString0(i, 1)}fun toHexString(i: Long): String {return toUnsignedString0(i, 4)}fun toOctalString(i: Long): String {return toUnsignedString0(i, 3)}fun toUnsignedString0(val: Long, shift: Int): String {// assert shift 0 shift 5 : Illegal shift value;val mag: Int Long.SIZE_BITS - numberOfLeadingZeros(val)val chars: Int max((mag (shift - 1)) / shift, 1)//if (COMPACT_STRINGS) {val buf ByteArray(chars)formatUnsignedLong0(val, shift, buf, 0, chars)return buf.map { it.toInt().toChar() }.toCharArray().concatToString()
// } else {
// val buf ByteArray(chars * 2)
// java.lang.Long.formatUnsignedLong0UTF16(val, shift, buf, 0, chars)
// return String(buf, UTF16)
// }}private fun formatUnsignedLong0(val: Long,shift: Int,buf: ByteArray,offset: Int,len: Int) {var val valvar charPos offset lenval radix 1 shl shiftval mask radix - 1do {buf[--charPos] digits[val.toInt() and mask].code.toByte()val val ushr shift} while (charPos offset)}fun numberOfLeadingZeros(i: Long): Int {val x (i ushr 32).toInt()return if (x 0) 32 numberOfLeadingZeros(i.toInt()) else numberOfLeadingZeros(x)}fun numberOfLeadingZeros(i: Int): Int {// HD, Count leading 0svar i iif (i 0) return if (i 0) 32 else 0var n 31if (i 1 shl 16) {n - 16i i ushr 16}if (i 1 shl 8) {n - 8i i ushr 8}if (i 1 shl 4) {n - 4i i ushr 4}if (i 1 shl 2) {n - 2i i ushr 2}return n - (i ushr 1)}
}然后更改我们的代码中使用到的地方即可例如
Long.toBinaryString 改为 LongUtil.toBinaryString(long) 。
记得把导入的包也改了
import java.lang.Long 改为 import com.equationl.common.utils.LongUtil
当然如果你的工具类直接取名叫 Long 的话那么调用代码就不用改了改导入包就行了。
BigInteger 和 BigDecimal
接下来就是 BigInteger 和 BigInteger同样的思路我们可以选择自己使用 kotlin 写一个功能相同的工具类但是显然这两个类可不同于进制转换它涉及到的代码量可要大多了。
好在已经有大神写好了纯 kotlin 的支持跨平台的 BigInteger 和 BigDecimal kotlin-multiplatform-bignum 。我们只需要简单的引用它就可以了。
在 shared 模块下的 build.gradle.kts 文件中的 kotlin - sourceSets - commonMain - dependencies 添加依赖
kotlin {sourceSets {val commonMain by getting {dependencies {implementation(com.ionspin.kotlin:bignum:0.3.8)}}}
}sync gradle 后依次修改项目中使用到 BigInteger 和 BigDecimal 地方的代码即可。
需要注意的是这个库的 API 和 java 的 BigInteger 以及 BigDecimal 并非完全一致因此需要我们逐个检查并修改。
例如在 java 的 BigDecimal 中除法的 API 是 divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
而在这个库中则变为了 divide(other: BigDecimal, decimalMode: DecimalMode? null)
除此之外还有一些小地方的代码可能引用的是 java 代码这里就不再赘述了按照上述两种思路逐个适配即可。
总结
自此我们的项目就完全移植到了完整形态的 Compose Multiplatform 中了现在它已经完全支持 Android、iOS 和 desktop 了
不知道你们有没有发现在全文中我几乎都是在说怎么适配和移植逻辑代码并没有说到有关 UI 的代码。
哈哈不是因为我忘记说了而是因为 Compose Multiplatform 代码真的做到了一套代码多平台通用。新增加 iOS 支持完全不用动 UI 部分的代码。
完整项目代码 calculator-Compose-MultiPlatform