• Kotlin & Compose Multiplatform 跨平台开发实践之加入 iOS 支持


    前言

    几个月前 Compose Multiplatform 的 iOS 支持就宣布进入了 Alpha 阶段,这意味着它已经具备了一定的可用性。

    在它发布 Alpha 的时候,我就第一时间尝鲜,但是只是浅尝辄止,没有做过多的探索,最近恰好有点时间,于是我又重新开始学习 Compose Multiplatform ,并且尝试移植我已有的项目使其支持 iOS,并且将移植过程整理记录了下来,即为本文。

    这次移植我选择的依旧是这个使用 Compose 写的计算器项目 calculator-Compose-MultiPlatform 。本来这次我想着移植一个涉及技术稍微多一点的项目的比如这个 githubAppByCompose,但是我仔细研究了一下,毕竟现在 Compose Multiplatform 还处于实验阶段,好多对应的功能和库都还没有,所以只能选择移植前者。

    对于这个计算器项目,最开始只是一个使用 Compose 实现的纯 Android 项目,后来移植到了支持 Android 和 桌面 端,所以其实现在再给它添加上 iOS 支持,也算是补齐了最后一个平台了,哈哈。

    在开始阅读本文之前,我会假设你已经了解并且知道 Compsoe 的基本使用方法。

    为了更好的理解本文,可能需要首先阅读这两篇前置文章:

    1. 【译】快速开始 Compose 跨平台项目
    2. Kotlin & Compose Multiplatform 跨平台(Android端、桌面端)开发实践之使用 SQLDelight 将数据储存至数据库

    前言的最后看一下运行效果:

    Android 端:

    2.png

    ios 端:

    4.png

    桌面端:

    3.png

    开始移植

    准备工作

    首当其冲,我们需要为 iOS 的支持更改编译配置文件和添加对应的平台特定代码。

    在我的这个项目中,我通过以下几个步骤为其添加了对 iOS 的支持:

    更改共享代码模块名称

    把公用代码模块由 common 改为 shared ,其实这里不用改也行,只是模板配置文件中写的 iOS 使用的公用代码路径是 shared ,但是直接改模块名比改配置文件简单多了,所以我们直接把模块名改了就好了。

    改完之后切记要检查一下其他模块引用的名字是否改了,以及注意检查一下包名是否正确。

    添加 native.cocoapods 插件

    shared 模块的 build.gradle.kts 文件的 plugins 增加 native.cocoapods 插件:

    plugins {
        kotlin("native.cocoapods")
        // ……
    }
    
    • 1
    • 2
    • 3
    • 4
    添加 cocoapods 配置

    shared 模块的 build.gradle.kts 文件的 kotlin 下增加 cocoapods 相应的配置:

    kotlin {
    	// ……
    
    	iosX64()
        iosArm64()
        iosSimulatorArm64()
    
        cocoapods {
            version = "1.0.0"
            summary = "Some description for the Shared Module"
            homepage = "Link to the Shared Module homepage"
            ios.deploymentTarget = "14.1"
            podfile = project.file("../iosApp/Podfile")
            framework {
                baseName = "shared"
                isStatic = true
            }
            extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
        }
    
        // ……
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    配置 iOS 源集

    shared 模块的 build.gradle.kts 文件的 kotlin 中的 sourceSets 下增加 iOS 的源集配置:

    kotlin {
    	// ……
    
    	sourceSets {
    		// ……
    		val iosX64Main by getting
            val iosArm64Main by getting
            val iosSimulatorArm64Main by getting
            val iosMain by creating {
                dependsOn(commonMain)
                iosX64Main.dependsOn(this)
                iosArm64Main.dependsOn(this)
                iosSimulatorArm64Main.dependsOn(this)
            }
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    添加其他插件

    在项目根目录下的 settings.gradle.kts 文件的 pluginManagement 中的 plugins 增加插件配置:

    pluginManagement {
    	//……
    
    	plugins {
    		kotlin("jvm").version(extra["kotlin.version"] as String)
    
    		// ……
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    添加 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: Channel<StandardAction>? = null,
        programmerChannelTop: Channel<ProgrammerAction>? = null,
    ) {
        val homeChannel = remember { Channel<HomeAction>() }
        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.background
    
            Surface(
                modifier = Modifier.fillMaxSize(),
                color = backgroundColor
            ) {
                HomeScreen(
                    homeChannel,
                    homeState,
                    standardChannel,
                    standardState,
                    programmerChannel,
                    programmerState
                )
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    并且,因为不同平台需要差异化实现部分功能,以及目前我还没找到一个好使的支持跨平台的依赖注入库,所以我索性将所有 控制(channel) 和 状态(state) 都提升到了最顶层,作为参数传递给下面的 Compose 函数。

    然后,更改三端各自的入口函数:

    Android (android 模块下的 MainActivity.kt 文件)
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            setContent {
                APP()
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    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 { Channel<StandardAction>() }
        val programmerChannel = remember { Channel<ProgrammerAction>() }
    
    
        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()
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    iOS ( shared模块 下的 main.ios.kt 文件)
    fun MainViewController() = ComposeUIViewController {
        APP()
    }
    
    • 1
    • 2
    • 3

    注意,不同于其他平台,iOS 的入口函数在 shared模块 中。

    当然,你要是想直接改 iosAPP 目录中的代码,那也不是不行,只是对于我们安卓开发来说,还是直接改 shared 更方便点。

    实现 iOS 的 平台代码

    之前我们的项目中有几个地方的实现依赖于平台,所以写了一些 expect 函数,现在我们需要给 iOS 实现对应的 actual 函数。

    首先在 shared 模块的 iosMain 包中创建一个包路径,保持和 commonMainexpect 函数包一致:

    1.png

    注意: 包路径一定要一致,不然会编译失败,我就在这里踩了坑,没注意到包名不一样, debug 了好久。

    这个项目中的平台差异函数主要有四个:

    控制振动

    因为我对 iOS 一窍不通,所以不知道怎么写,索性直接留空了:

    actual fun vibrateOnClick() {
    
    }
    
    actual fun vibrateOnError() {
    
    }
    
    actual fun vibrateOnClear() {
    
    }
    
    actual fun vibrateOnEqual() {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    控制屏幕旋转和显示小窗

    这里同上,不知道怎么写,直接留空:

    actual fun showFloatWindows() {
    
    }
    
    actual fun changeKeyBoardType(changeTo: Int) {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    数据库(sqldelight)

    actual fun createDriver(): SqlDriver {
        return NativeSqliteDriver(HistoryDatabase.Schema, "history.db")
    }
    
    • 1
    • 2
    • 3

    关于使用 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")
    	        }
            }
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此时如果你直接 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    这是因为 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    添加完记得需要 sync 一下 gradle。

    此时再编译运行,大概率还是会报错:

    ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a
    
    • 1

    不用担心,再在刚才的配置文件中加上这么一段:

    # iosApp's 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.1'
            end
        end
    end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    此时应该就不会有任何问题了。

    适配 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.utils
    
    import kotlin.math.max
    
    object 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` = `val`
            var charPos = offset + len
            val radix = 1 shl shift
            val mask = radix - 1
            do {
                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 0's
            var i = i
            if (i <= 0) return if (i == 0) 32 else 0
            var n = 31
            if (i >= 1 shl 16) {
                n -= 16
                i = i ushr 16
            }
            if (i >= 1 shl 8) {
                n -= 8
                i = i ushr 8
            }
            if (i >= 1 shl 4) {
                n -= 4
                i = i ushr 4
            }
            if (i >= 1 shl 2) {
                n -= 2
                i = i ushr 2
            }
            return n - (i ushr 1)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    然后更改我们的代码中使用到的地方即可,例如:

    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")
    			}
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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

  • 相关阅读:
    java版工程管理系统Spring Cloud+Spring Boot+Mybatis实现工程管理系统源码
    模拟实现ATM系统——Java
    29.5.4 恢复数据
    STM32学习历程(day6)
    WebKit策略:<foreignObject>可用于绘制svg中的html标签,但与<use>搭配不生效
    nginx主机黑白名单[geoip]
    Docker的数据卷
    java技术专家面试指南80问【java学习+面试宝典】(三)
    MyBatis进阶版
    java:java.util.StringTokenizer实现字符串切割
  • 原文地址:https://blog.csdn.net/sinat_17133389/article/details/133894370