Compose Multiplatform+kotlin Multiplatfrom

前言

现在Android原生需求日渐减少,多平台或车载,端侧大模型等我看是未来的主流,上一年做了新能源DBC协议的数据可视化显示,将数据实时保存到本地的csv或asc文件中,处理的难点高频数据的缓存和释放。端侧目前各系统不成熟,后面纯血鸿蒙处理应该底层就搭建大模型,应用层可直接调用模型的接口,下放更多能力,可是除了图片,音频我也不知道大模型能怎么改变我们普通的开发者,我们的应用没那么多AI,高手可留言讨论😁。今年把原生的转Compose Multiplatform+kotlin Multiplatfrom,开始搭建时kotlin版本1.9.22,compose1.6,过程解决各类问题,进入无数的无人区调试。

功能需求分析

源码涉及公司后续抽离公布
1.尽量一套代码开发Android、iOS的功能,ui或逻辑都尽量在commonMain实现,问题是ui预览时相当麻烦,都要androidApp内的MainActivity设置@Preview后查看,那么多页面不知道大家平时怎么用预览的,太麻烦了。

2.通过网络链接下载文件到设备本地,目前下载是ktor库可回调进度,下载目录是参考coil它内部用FileSystem来处理文件,在commonMain内部不包含java的File类、FileOutputStrem,这里确实坑很不熟悉。后续我尝试放到相册和外部存储。

3.webview用kevinnzou的实现,这里有个大坑是和导航库voyager-navigator一起用会导致每次点击输入框都重新刷新网页,后面没办法用precompose导航了,但这玩意不足是传递参数时放在path后面会有截取,字符长度我看就300多,如果直接把实体对象转字符放到path后面就截取了,最后我在viewmodel里建个单例对象管理😂,那么页面的路由路径就可不带参数,要取参数直接从viewModel里拿。

4.json解析坑Json { isLenient = true ignoreUnknownKeys = true}构造器要设置参数,不然基本接口回调数据基本报错,还有涉及手动解析属性,封装jsonArray,都跟java不同,addJsonObject{putJsonArray(){}}。我那个要处理成😉{"pageNum":1,"pageSize":20,"query":"空调","mainRecommend":false,"first":false,"productTagParam":{"productName":"家用空调","tags":{"hp":["小1匹","3匹"],"color":["白色"],"series":["冷静享","星宇"]},"othersTags":[]}}

5.权限问题,dev.icerock.moko:mvvm-compose,Git只找到这个,没有人做权限库,在安卓那ComponentActivity下用它库的viewmodel绑定写法不会蹦,用FragmentActivity不会报bindeffect,但是我们要compose肯定是ComponentActivity,然后要写SampleViewModel,但是案例只每次申请一个权限,后续有需要自己改。iOS的注意info.list文件加入节点,因为iOS不算太了解,xcode内部的调试感觉太简陋了。

6.视频播放没看到有功能库,所以做桥接接口,Android用media3-exoplayer,iOS用AvPlayer各自原生实现,注意原生构建view只有一次,那么更新用AndroidView{ update={}},还有个释放资源问题,BackHandler{}内只能处理Android的,iOS没有这个响应,所以用 DisposableEffect(Unit) { onDispose { player.pause() }},不然退出页面还在播放😅

7.声明式ui,目前还不太熟悉,在用paging3做自动加载列表时,每次从详情页返回都重新刷新列表,后面是在Pager{}.flow.cacheIn(scope)就可以,但是页面的刷新次数貌似无法控制,要触发一些功能要写很多state去修改它的值进而remember状态发生变化自动更新ui,这里跟命令式代码差别很大,还有LaunchedEffect{}也常用。

8.用coil3加载超长图会oom,注意不是coil2,它官网根本都没发compose multiplatform对应的版本库,我是自己拼地址猜出来了,但是在开发中也可以依赖,目前应该还有很多不足。Android的com.github.piasy:BigImageViewer性能非常好,推荐单独集成Android内。

9.Android端集成第三方腾讯文档浏览服务,这个是要付费,跟包名绑定,这里我是要把文件链接下载到本地然后调用sdk功能打开pdf/doc等格式文件,涉及TbsFileSdk_dwg_universal_release这个aar依赖,我在手机连线打debug包时没问题,但是在generate signed apk时生成真正的release模式下apk时会报错,就是aar依赖导致的。我的改好了,自提代码大伙😉。

10.编译问题,Compose Multiplatform+Kotlin Multiplatfrom这套开发架构是我是基于Android来开发的,会jetpack compose就容易入手,对iOS很多不熟悉,网上很多都是单纯跑下一两个页面,实际项目一大堆业务合并拆分,编译会遇到各种问题,在AndroidStudio写完Android也能跑了,那么跑iOS模拟器网上很多有改写run configurations跑iosApp,那么真机呢!网上都没人搞,我尝试了当成iOS项目原生跑,用xcode 打开iOSApp文件夹内的iosApp.xcodeproj,编译过程没有配置开发者签名的话就会报错,根据编译过程的日志提示解决报错,很多情况是改info.list,不同版本xcode有很多配置问题,我这iOS17.4模拟器,真机17.4.1 ipad。

好用的库依赖

这里贴了项目中用到的库,筛选了很多类似的最后选用的库,版本基本最新。

compose 的热门库github收集

如下shared的build.gradle

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.jetbrainsCompose) //加了后重新编译,可以快捷引入compose的resource
    alias(libs.plugins.kotlinxSerialization)
    alias(libs.plugins.kotlinCocoapods) // 如果 IOS 使用 cocoapods,引入此插件
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "11"
            }
        }
    }

    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
    }

    sourceSets {
        commonMain.dependencies {
            //所有公共逻辑、ui都写在common模块,那么就必须要引用这里以kotlin写的库
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
            implementation(compose.components.resources)

            //多平台公共依赖库 https://www.5axxw.com/wiki/content/znqhcn
            implementation(libs.kotlin.datetime)

            //ios库无法获取以下
            implementation(compose.components.uiToolingPreview)

            //支持多平台的网络库
            implementation(libs.ktor.client.core) //网络请求
            implementation(libs.ktor.client.content.negotiation)
            implementation(libs.ktor.serialization.kotlinx.json)
            implementation(libs.ktor.client.cio)
            implementation(libs.ktor.client.logging)

            //依赖注入
            implementation(libs.koin.core)
            //页面导航  https://github.com/adrielcafe/voyager/tree/main
            implementation(libs.voyager.navigator)
            implementation(libs.voyager.koin)
            implementation(libs.voyager.tab.navigator)
            implementation(libs.voyager.bottom.navigator)
            implementation(libs.voyager.transitions)

            //另一个导航
            implementation(libs.precompose.navigator)
            implementation(libs.precompose.koin)
            implementation(libs.precompose.viewmodel)


            //异步图片加载   //这个版本还缺其他?
            implementation(libs.coil3.core)
            implementation(libs.coil3.ktor)
            implementation(libs.coil3.compose) //这个地址太坑了,官网没更新出来

            //异步协程库
            implementation(libs.kotlinx.coroutines.core)

            //https://github.com/KevinnZou/compose-webview-multiplatform/tree/main https://blog.csdn.net/weixin_51235693/article/details/133277648
            api(libs.compose.webviews)

            //权限 compose multiplatform https://github.com/icerockdev/moko-permissions
            implementation(libs.mokopermission)
            implementation(libs.mokopermission.compose)
            implementation(libs.stately.common)
            implementation(libs.mokoMvvmCore)
            implementation(libs.mokoMvvmCompose)

            //多平台uuid https://github.com/benasher44/uuid/tree/master 不知道怎么用
            implementation(libs.uuid)

            //日志库
            implementation(libs.logger.kermit)

            //key-value存储
            implementation(libs.multiplatform.settings)

            //https://github.com/KoalaPlot/koalaplot-core 图表库

            //页面自适配判断库
            implementation(libs.windowSize)

            //https://github.com/skydoves/FlexibleBottomSheet 从底部弹窗
            implementation(libs.bottomSheet)

            //分页库
            implementation(libs.paging.compose)

            //网络流的数据可存FileSystem
            implementation(libs.okio.core)

            //文件选择器
            implementation(libs.file.picker)

            //第三方libs composeView https://github.com/ltttttttttttt/ComposeViews/blob/main/README_CN.md
            implementation(libs.view.libs1)

        }
        androidMain.dependencies {
            //引入本地Android aar库
            implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
            implementation(files("../androidApp/libs/TbsFileSdk_dwg_universal_release_1.0.5.6000030.20231109143411.aar"))

            //android平台引擎
            implementation(libs.ktor.client.android)

            //预览
            implementation(libs.compose.ui.tooling.preview)
            implementation(libs.androidx.activity.compose)
            implementation(libs.androidx.appcompat)

            implementation(libs.androidx.perference)

            //mmvm
            implementation(libs.androidx.lifecycle)
            implementation(libs.lifecycle.extension)
            implementation(libs.composeMaterial)
            implementation(libs.appCompat)

            //图片库加载,超长图moo
            implementation(libs.coil3.video)
            implementation(libs.coil3.gif)

            //远程日志上报
            implementation(libs.android.bugly)

            //视频播放器
            implementation(libs.medie3.core)
            implementation(libs.medie3.dash)
            implementation(libs.medie3.ui)

        }

        iosMain.dependencies {
            //网络库,提供iOS平台引擎
            implementation(libs.ktor.client.ios)
        }
        commonTest.dependencies {
            implementation(libs.kotlin.test)
        }
    }

    cocoapods {  //cocoapods类似gradle管理包构建依赖,这里集成日志库
        version = "1.0"
        summary = "Sample for Kmm"
        homepage = "https://www.touchlab.co"
        framework {
            baseName = "LiteLibs"
            isStatic = true

            // Only if you want to talk to Kermit from Swift
            export("co.touchlab:kermit-simple:2.0.3")
        }

    }
}

android {
    namespace = "com.hwj.cn"

    compileSdk = 34
    defaultConfig {
        minSdk = 24
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
}

下面是/gradle/libs.versions.toml

[versions]
agp = "8.2.0"
kotlin = "1.9.22"
compose = "1.6.2"
compose-compiler = "1.5.10"
compose-material3 = "1.1.2"
composeMaterialVersion = "1.4.1"
androidx-activityCompose = "1.8.2"
kotlinxDatetime = "0.4.0"
compose-plugin = "1.6.0"
appcompat = "1.6.1"
material = "1.11.0"
activity = "1.8.0"
constraintlayout = "2.1.4"
lifecycleRuntimeKtx = "2.7.0"
composeBom = "2023.08.00"
voyager = "1.1.0-alpha04"
androidMediaFilePicker = "1.9.1"
koin = "3.5.3"
mokoMvvmVersion = "0.16.1"
androidLifecycleVersion = "2.2.0"
stately = "2.0.6"
ktor = "2.3.9"
coroutines-core = "1.8.0-RC"
uuid = "0.8.4"
kermit = "2.0.3"
settings = "1.1.1"
mokopermission = "0.18.0"
webviews = "1.9.0"
perference = "1.2.0"
androidAppCompatVersion = "1.6.1"
coil3 = "3.0.0-alpha06"
windowSize = "0.5.0"
sheet = "0.1.2"
pagingCommonVersion = "3.3.0-alpha02-0.5.1"
precompose = "1.6.0"
okio = "3.9.0"
bugly = "4.1.9"
file = "0.4.0"
view1 = "1.6.0.3"
media3="1.1.0"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
composeMaterial = { module = "androidx.compose.material:material", version.ref = "composeMaterialVersion" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" }
compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" }
kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-lifecycle = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-perference = { module = "androidx.preference:preference-ktx", version.ref = "perference" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
voyager-bottom-navigator = { module = "cafe.adriel.voyager:voyager-bottom-sheet-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }
precompose-navigator = { module = "moe.tlaster:precompose", version.ref = "precompose" }
precompose-koin = { module = "moe.tlaster:precompose-koin", version.ref = "precompose" }
precompose-viewmodel = { module = "moe.tlaster:precompose-viewmodel", version.ref = "precompose" }

mokoMvvmCore = { module = "dev.icerock.moko:mvvm-core", version.ref = "mokoMvvmVersion" }
mokoMvvmCompose = { module = "dev.icerock.moko:mvvm-compose", version.ref = "mokoMvvmVersion" }
lifecycle-extension = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "androidLifecycleVersion" }
mediaFilePicker = { module = "com.github.icerockdev:MaterialFilePicker", version.ref = "androidMediaFilePicker" }
#mediaFilePicker = { module = "com.nbsp:materialfilepicker", version.ref = "androidMediaFilePicker" }
detektGradleLib = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.22.0" }
stately-common = { module = "co.touchlab:stately-common", version.ref = "stately" }
mokopermission = { module = "dev.icerock.moko:permissions", version.ref = "mokopermission" }
mokopermission-compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "mokopermission" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
ktor-client-ios = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines-core" }
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
logger-kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "settings" }
compose-webviews = { module = "io.github.kevinnzou:compose-webview-multiplatform", version.ref = "webviews" }
coil3-core = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
coil3-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" }
coil3-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil3" }
coil3-video = { module = "io.coil-kt.coil3:coil-video", version.ref = "coil3" }
coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
windowSize = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "windowSize" }
bottomSheet = { module = "com.github.skydoves:flexible-bottomsheet-material3", version.ref = "sheet" }
paging-compose = { module = "app.cash.paging:paging-compose-common", version.ref = "pagingCommonVersion" }
okio-core = { module = "com.squareup.okio:okio", version.ref = "okio" }
android-bugly = { module = "com.tencent.bugly:crashreport", version.ref = "bugly" }
file-picker = { module = "io.github.vinceglb:filekit-compose", version.ref = "file" }
view-libs1 = { module = "io.github.ltttttttttttt:ComposeViews", version.ref = "view1" }
medie3-core = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
medie3-dash = { module = "androidx.media3:media3-exoplayer-dash", version.ref = "media3" }
medie3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }


[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

下面是androidApp下的build.gradle

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

plugins {
    alias(libs.plugins.androidApplication)
    alias(libs.plugins.kotlinAndroid)
}

android {
    namespace = "com.hwj.cn"
    compileSdk = 34
    defaultConfig {
        applicationId = "com.hwj.cn"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0.0"

        ndk { //设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
            abiFilters += listOf("armeabi", "armeabi-v7a", "arm64-v8a")
        }
    }



    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
    }
    packaging {
        resources {
            excludes += "/META-INF/{AL2.0,LGPL2.1}"
        }
    }
    buildTypes {
        getByName("debug") {
            //签名
            signingConfig = signingConfigs.getByName("debug")
        }
        register("alpha") {
            //继承debug配置
            initWith(getByName("debug"))
            //混淆
//            isMinifyEnabled = true //有混淆无法编译?
//            proguardFiles(
//                getDefaultProguardFile("proguard-android-optimize.txt"),
//                "proguard-rules.pro"
//            )
            //移除无用的resource文件
//            isShrinkResources = true

//            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
        getByName("release") {
            //继承alpha配置
            initWith(getByName("alpha"))
            //关闭debug
//            isDebuggable = false
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }

    signingConfigs {
        getByName("debug") {
            enableV1Signing = true
            enableV2Signing = true
            enableV3Signing = true
//            enableV4Signing = true //增量安装,不需要
            storeFile = file("../hwj_pad_app.jks")
            storePassword = "111116"
            keyAlias = "gggg"
            keyPassword = "111116"
        }

        register("release") {
            enableV1Signing = true
            enableV2Signing = true
            enableV3Signing = true
            storeFile = file("../hwj_pad_app.jks")
            storePassword = "111116"
            keyAlias = "gggg"
            keyPassword = "111116"
        }
    }

    sourceSets {
        getByName("main") {
            jniLibs.srcDirs("libs")
        }
    }

    //自定义打包文件名
    afterEvaluate {
        tasks.named("assembleRelease") {
            finalizedBy("copyAndRenameApkTask")
        }
    }
}

val copyAndRenameApkTask by tasks.registering(Copy::class) {
    val config = project.android.defaultConfig
    val versionName = config.versionName
    val versionCode = config.versionCode
    val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmm")
    val createTime = LocalDateTime.now().format(formatter)
    val gitHash = providers.exec {
        commandLine("git", "rev-parse","--short","HEAD")
    }.standardOutput.asText.get()
    val destDir = File(rootDir, "apkBackup/compose_${versionName}")
    from("release/androidApp-release.apk")
    into(destDir)
    rename { _ -> "compose_ark_${versionName}_${versionCode}_${createTime}.apk" }
    doLast {
        File(destDir, "App上传配置.txt").outputStream().bufferedWriter().use {
            it.appendLine("版本号:${versionCode}")
                .appendLine("版本名称:${versionName}")
                .appendLine("软件名称:composeApp")
                .appendLine("软件包名:com.hwj.cn")
                .appendLine("版本说明:kotlin multiplatform、compose-multiplatform")
                .appendLine("发布时间:${createTime}")
                .appendLine("git记录:${gitHash}")
        }
    }

}

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
    implementation(projects.shared)
    implementation(libs.compose.ui)
    implementation(libs.compose.ui.tooling.preview)
    implementation(libs.compose.material3)
    implementation(libs.androidx.activity.compose)
    debugImplementation(libs.compose.ui.tooling)

    implementation(libs.appCompat)
}

结尾

整套架构下来,页面数据传递还是有点迷糊,例如列表点击跳转详情,一个实体对象内部如果有几个属性是网络链接,那么转字符长度就很大,如果是Screen(arg1:string)构建页时把参数独立传,如果有多个中间页很麻烦,我这里是利用viewModel的单例暂存传递的对象解决;图片的功能太少了;预览ui时非常麻烦,都在Activity内声明;目前性能问题也是没有怎么考虑,ipad2018下感觉卡顿。

Jason
2024/6/3

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值