当前位置: 首页 > news >正文

Compose Multiplatform+Kotlin Multiplatfrom 第四弹跨平台

文章目录

    • 引言
    • 功能效果
    • 开发准备
    • 依赖使用
      • gradle依赖库
      • MVI+Flow设计
      • 富文本显示
    • 总结

引言

  Compose Multiplatform+kotlin Multiplatfrom 今天已经到compose v1.7.3,从界面UI框架上实战开发看,很多api都去掉实验性注解,表示稳定使用了!然后继续这套框架做技术预研,上马目前所有系统,即Android、iOS、macOs、Windows、Linux(没有系统可验证)、Browser。
  现在AI大模型是前沿技术广泛引用的排头兵,这次引入deepseek的多轮会话v3大模型,使用调用api的方式完成数据的显示。
  *** 基于保密不会将重要的算法、工厂引擎代码透露。

功能效果

  目前成功在Android真机,Macbook pro2022,Windows10,iOS 17模拟器运行,本地构建要依赖的库实在是太多了,稍微一个错编译就是很漫长,还有相当多技术在预研,后面后把完整代码公布出来。

运行效果图gif预览,点进去看不到要登录下gitee
gif图太大了,没法压缩到10mb,给出链接iOS/macOs
在这里插入图片描述

Windows版电脑有水印不好截图,放下载包.exe

运行截图1 截图2 截图3

开发准备

  • 依托Android的compose框架为界面开发,Android studio最新版Android Studio Koala | 2024.1.1 Patch 1 Build #AI-241.18034.62.2411.12071903, built on July 11, 2024,本地的JDK为17或21,我把第三方库comShot引进来就要21,
    XCode是编译iOS的,现在只运行过在模拟器,所有系统配置好环境变量。
    开发电脑两台Windows 10专业版,MacBook2022,两边都可自主同步编译,主要问题是gradle.properties文件中指定了项目的JDK路径,
    两台电脑是不一致的,必须清楚自己的JDK版本和路径,$java -version 。
  • 去deepseek获取免费一个月的api key,其实换openAI的key也可以,但是注意api的路径和域名,确定当前compose plugin最新版本,目前我的所有依赖都是最新版本,已经踏平了很多坑。
  • 明确需求,部分实现:
  • 1.多轮机器对话 ,支持多个大模型切换
  • 2.本地会话记录,sqlDelight数据库
  • 3.黑暗模式切换,支持所有系统,支持代码框包裹
  • 4.富文本对代码支持,对公式函数支持
  • 5.系统图册选择后模型问答
  • 6.截图后的模型问答,desktop端自由裁剪很难,移动端还行
  • 7.多端编译,一套代码开发,适配不同机型和系统,磨平差异性
  • 8.desktop端可随处弹出一个小功能窗快捷询问,移动端则可以是语音和浮窗长驻
  • 9.系统粘贴版监听,本地存储粘贴记录
  • 10.本地知识库构建,离线小模型处理,多场景搜索算法智能调用
  • 10.MVI+Flow流+ViewModel+Koin3开发架构设计,我并没有引入第三方的框架来做,我去研读了下FlowMVI的设计,它引入了太多其他库,而且把逻辑块的设计完全剥离,感觉自己没完全理解不敢用,最终目的就是异步的流式编程,在声明式开发中用户意图单向流动到界面,数据源唯一可信,界面就能按我们的意图渲染,再结合副作用effect,觉得比原生好用好多,就是现在不太会画canvas
  • 完全自动运行,类似手机AI应用帮我们点饭,解决隐私权限,模拟点击,持续日志信息队列分析

依赖使用

  在multiplatform compose 开发建议还是多依赖第三方的团队sdk,实在没有的再搞接口expect去实现,因为多端差异性开发工作量实在是大,整个UI框架现在迭代速度也挺快的,运行的效果差异巨大,不同系统差异,当同系统下还有机型差异,系统版本插件,就键盘弹起的坑、状态栏样式等都不及预期。

  • 通用类commonMain

  • openai-client,这个库用kotlin把整个大模型请求都包装好了,我在java时用okhttp3是成功实现SSE数据流的结果,但在kotlin multiplatform时每次接口数据都是等一会然后所有data包一次性返回来,我对比过openai-client的请求源码分析也没太大逻辑差别,暂时解决不了原生kotlin用ktor3请求SSE效果,有实现可以告知>-<

  • ktor3.0 + kotlinx-json ,这是网络请求库和序列化,在大模型的请求参数中,每个实体类的值都会转为Json参数,然后post/json的方式获取data 包,正常的SSE返回应该每个包都是data :{},里面的chioe[]又可能为空,所以记得善用序列化的注解,不同的大模型对参数要求有小差异,不出现也要声明把值至为null,不然没有时偶尔报错导致程序解析中断。

  • multiplatform-settings,本地数据持久化,之前的文章也提过,现在理解透彻后重新实现了依赖注入的单例,改为suspend的协程异步执行,虽然优化了线程,当异步时小心业务使用的逻辑错误,取值时不在同一个线程。

  • napier,日志库,Android的日志Logcat,iOS在console,Desktop在terminal,都要初始化后才能使用。

  • precompose,路由页面跳转库,这个自带viewModel,内部也使用koin注入,所以每个composable页面使用的viewModel都继承precompose库下的viewModel,我里面封装了NavigateRoute整个路由表。

  • sqlDelight,数据库,会话记录历史存放工具,要自己写sql语句

  • comShot,Android和windows的composalbe组件截图,就是对UI控件的截图,我尝试整个屏幕构建一个透明的composable组件然后触发截图,但是在自由拖拽矩形时没法跳出应用外的界面,后面再研究下。

  • vinceglb/FileKit,文件选择器,以前不知道有这库本地文件都是用okio expect保存的

  • coil3,图片异步加载,gif/video只支持Android;可选sketch库

  • webview,io.github.kevinnzou:compose-webview-multiplatform修复很及时,注意和路由库使用的bug,用voyager和它不兼容

  • 系统特性类

  • 富文本显示,https://github.com/halilozercan/compose-richtext,只支持Android和Desktop,而且调试bug不多,

  • 富文本编辑或显示,https://github.com/MohamedRejeb/compose-rich-editor,全平台,但是挺多问题的,iOS不同写法会内容闪烁,只支持行内代码‘’,不支持块代码包裹如‘’‘ java’‘’。

  • 权限库,dev.icerock.moko:permissions,其他可选真不多。

gradle依赖库

[versions]
agp = "8.5.0"
#ksp = "2.0.20-1.0.24"
kotlin = "2.1.0"
ksp = "2.1.0-1.0.29"
compose-compiler = "1.5.4"
java = "21"
compose = "1.7.6"
kotlinxDatetime = "0.6.0"
compose-material3 = "1.3.1"
activityCompose = "1.9.2"
compose-plugin = "1.7.3"
androidAppCompatVersion = "1.7.0"
sqlDelight = "2.0.2"
buildkonfigGradlePlugin = "0.15.1"
koin = "4.0.0"
mokoMvvmVersion = "0.16.1"
androidLifecycleVersion = "2.2.0"
stately = "2.0.6"
ktor = "3.1.0"
uuid = "0.8.4"
settings = "1.3.0"
mokopermission = "0.18.1"
perference = "1.2.0"
permissionX = "1.8.0"
coil3 = "3.1.0"
windowSize = "0.5.0"
sheet = "0.1.2"
pagingCommonVersion = "3.3.0-alpha02-0.5.1"
#precompose = "1.6.2"
precompose = "1.7.0-alpha01" #预发布
okio = "3.9.0"
bugly = "4.1.9"
file = "0.8.8"
coroutines-core = "1.10.1"
coreSplashscreen = "1.0.1"
datastorePreferences = "1.1.1"
lifecycleRuntimeKtx = "2.7.0"
#coreKtx = "1.10.1"
coreKtx="1.15.0"
jna = "5.15.0"
toast4j = "0.2.0"
kotlinxCoroutinesSwing = "1.10.1"
kotlinxSerializationJson = "1.7.3"
desugar_jdk_libs = "2.1.3"
accompanist-systemUIController = "0.36.0"
androidx-lifecycle = "2.8.4"
androidx-navigation = "2.8.0-alpha10"
napier = "2.7.1"
richtext = "1.0.0-alpha02"
richedit = "1.0.0-rc11"
reveal = "3.2.0"
capture = "0.3.0"
ktoken = "0.3.0"
openai-client = "4.0.1"
webview="1.9.40"[libraries]
#UI框架相关
buildkonfig-gradle-plugin = { module = "com.codingfeline.buildkonfig:buildkonfig-gradle-plugin", version.ref = "buildkonfigGradlePlugin" }
core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
androidx-core = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines-core" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesSwing" }
kotlinX-serializationJson = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationJson" }uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
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" }
kotlin-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
toast4j = { module = "de.mobanisto:toast4j", version.ref = "toast4j" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
accompanist-systemUIController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist-systemUIController" }
napier = { module = "io.github.aakira:napier", version.ref = "napier" }
stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
compose-webviews = { module = "io.github.kevinnzou:compose-webview-multiplatform", version.ref = "webview" }#没有iOS的富文本处理
richtext-core = { module = "com.halilibo.compose-richtext:richtext-ui", version.ref = "richtext" }
richtext-mark = { module = "com.halilibo.compose-richtext:richtext-commonmark", version.ref = "richtext" }
richtext-markdown = { module = "com.halilibo.compose-richtext:richtext-markdown", version.ref = "richtext" }
richtext-material = { module = "com.halilibo.compose-richtext:richtext-ui-material", version.ref = "richtext" }
richtext-material3 = { module = "com.halilibo.compose-richtext:richtext-ui-material3", version.ref = "richtext" }
rich-editor = { module = "com.mohamedrejeb.richeditor:richeditor-compose", version.ref = "richedit" }reveal = { module = "com.svenjacobs.reveal:reveal-core", version.ref = "reveal" }
#capture-shot = { module = "ir.mahozad.multiplatform:comshot", version.ref = "capture" }#Android特有
androidx-perference = { module = "androidx.preference:preference-ktx", version.ref = "perference" }
androidx-lifecycle = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" }
lifecycle-extension = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "androidLifecycleVersion" }
android-bugly = { module = "com.tencent.bugly:crashreport", version.ref = "bugly" }
permissionX-android = { module = "com.guolindev.permissionx:permissionx", version.ref = "permissionX" }#数据处理
file-picker = { module = "io.github.vinceglb:filekit-compose", version.ref = "file" }
okio-core = { module = "com.squareup.okio:okio", version.ref = "okio" }
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "settings" }
multiplatform-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "settings" }
multiplatform-datastore = { module = "com.russhwolf:multiplatform-settings-datastore", version.ref = "settings" }
multiplatform-serialization = { module = "com.russhwolf:multiplatform-settings-serialization", version.ref = "settings" }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-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" }#依赖注入
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
stately-common = { module = "co.touchlab:stately-common", version.ref = "stately" }#权限申请,兼容Android
mokopermission = { module = "dev.icerock.moko:permissions", version.ref = "mokopermission" }
mokopermission-compose = { module = "dev.icerock.moko:permissions-compose", version.ref = "mokopermission" }
mokoMvvmCompose = { module = "dev.icerock.moko:mvvm-compose", version.ref = "mokoMvvmVersion" }
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" }#数据库
android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" }
native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" }
sqldelight-extensions = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "sqlDelight" }
sqlDelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" }
primitive-adapters = { module = "app.cash.sqldelight:primitive-adapters", version.ref = "sqlDelight" }
sqlite-driver = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqlDelight" }#ktor 3.0网络连接
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
#ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" }
#ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" }
#ktor engines
#ktor-client-apache = { module = "io.ktor:ktor-client-apache", version.ref = "ktor" }
#ktor-client-jetty = { module = "io.ktor:ktor-client-jetty", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", 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-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" }
ktoken = { group = "com.aallam.ktoken", name = "ktoken", version.ref = "ktoken" }
openai-client = { module = "com.aallam.openai:openai-client", version.ref = "openai-client" }#https://coil-kt.github.io/coil/getting_started/
#coil3-core = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" }
coil3-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" }
coil3-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil3" }
coil3-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil3" }
#coil3-ktor = { module = "io.coil-kt.coil3:coil-network-ktor2", version.ref = "coil3" }
#video/gif只有Android版
#coil3-video = { module = "io.coil-kt.coil3:coil-video", version.ref = "coil3" }
#coil3-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil3" }[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" }
jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfigGradlePlugin" }
sqlDelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

MVI+Flow设计

使用//主动获取数据 chatViewModel.processConfig(ModelConfigIntent.LoadData)

package com.hwj.ai.ui.viewmodelimport com.hwj.ai.data.repository.GlobalRepository
import com.hwj.ai.global.CODE_IS_DARK
import com.hwj.ai.global.getCacheBoolean
import com.hwj.ai.global.getMills
import com.hwj.ai.models.LLMModel
import com.hwj.ai.ui.global.GlobalIntent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import moe.tlaster.precompose.viewmodel.ViewModel
import moe.tlaster.precompose.viewmodel.viewModelScopeclass ChatViewModel(private val globalRepo: GlobalRepository) : ViewModel() {private val _drawerShouldBeOpened = MutableStateFlow(false)val drawerShouldBeOpened = _drawerShouldBeOpened.asStateFlow()fun openDrawer() {_drawerShouldBeOpened.value = true}fun resetOpenDrawerAction() {_drawerShouldBeOpened.value = false}//全局参数状态private val _configObs = MutableStateFlow(ModelConfigState())val configState = _configObs.asStateFlow()private var lastTime = getMills()//本地主题值修改private val _darkObs = MutableStateFlow(false)val darkState = _darkObs.asStateFlow()fun processConfig(intent: ModelConfigIntent) {when (intent) {is ModelConfigIntent.LoadData -> {fetchModelConfig()}is ModelConfigIntent.UpdateData -> {if (getMills() - lastTime > 30000) {fetchModelConfig()}}}}fun processGlobal(intent: GlobalIntent) {when (intent) {is GlobalIntent.CheckDarkTheme -> {fetchDarkStatus()}}}private fun fetchModelConfig() {_configObs.update { it.copy(isLoading = true) }viewModelScope.launch(Dispatchers.IO) {try {val result = globalRepo.fetchModelConfig()_configObs.update { it.copy(isLoading = false, data = result) }} catch (e: Exception) {_configObs.update { it.copy(error = e.toString()) }}}}private fun fetchDarkStatus() {viewModelScope.launch {val isDark = getCacheBoolean(CODE_IS_DARK)_darkObs.value = isDark}}
}data class ModelConfigState(val isLoading: Boolean = false,val data: List<LLMModel>? = null,val error: String? = null
)
//解决不了基类的写法
//data class ModelConfigState(val json: String? = null) : BaseUiState<String>(data = json)sealed class ModelConfigIntent {//获取所有的大模型数据,解析后保存到本地object LoadData : ModelConfigIntent()//判断时间间隔是否需要更新数据,主动拉取,data class UpdateData(val time: Long) : ModelConfigIntent()
} MyState 继承自 UiState
//data class MyState(
//    val items: List<String> = emptyList() // 你可以根据具体需要定制数据类型
//) : BaseUiState<List<String>>(data = items)

富文本显示

//android + windows@Composable
private fun TestBotMsgCard1(message: MessageModel) {
//    val chatViewModel = koinViewModel(ChatViewModel::class)
//    val isDark = chatViewModel.darkState.collectAsState().valueval richTextStyle = RichTextStyle(codeBlockStyle = CodeBlockStyle(textStyle = TextStyle(fontFamily = FontFamily.Default,fontWeight = FontWeight.Normal,fontSize = 13.sp,color = BackCodeTxtColor,),wordWrap = true,modifier = Modifier.background(color = BackCodeGroundColor,shape = RoundedCornerShape(6.dp))),stringStyle = RichTextStringStyle())//第一种
//    com.halilibo.richtext.ui.material.RichText(
//        modifier = Modifier.padding(
//            horizontal = 18.dp,
//            vertical = 12.dp
//        ).background(MaterialTheme.colorScheme.onPrimary),
//        style = richTextStyle,
//
//        ) {
//        //字体颜色对了,但是没能解析富文本的符合
            Text(message.answer.trimIndent(), color = MaterialTheme.colorScheme.onTertiary)
//
//        //没能改字体颜色
//        Markdown(message.answer.trimIndent())
//    }//第二
//    val richTextState = rememberRichTextState()
//    richTextState.setMarkdown(message.answer.trimIndent())
//    richTextState.config.codeSpanBackgroundColor= BackCodeGroundColor
//    richTextState.config.codeSpanColor= BackCodeTxtColor
//    ThemeChatLite {
//        RichTextEditor(
//            modifier = Modifier.padding(
//                horizontal = 18.dp,
//                vertical = 12.dp
//            ).background(MaterialTheme.colorScheme.onPrimary), state = richTextState,
//            textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onTertiary)
//        )//第三
//    val parser =CommonmarkAstNodeParser()
//    RichText( modifier = Modifier.padding(
//        horizontal = 18.dp,
//        vertical = 12.dp
//    ).background(MaterialTheme.colorScheme.onPrimary),
//    style = richTextStyle,
//        ){
//        BasicMarkdown(astNode = parser.parse(message.answer.trimIndent()))
//    }//第四  追踪源码查看 RichTextMaterialTheme-》contentColorProvider 修改内部字体颜色,自定义代码颜色RichTextThemeProvider(contentColorProvider = { MaterialTheme.colorScheme.onTertiary }) {BasicRichText(modifier = Modifier.padding(horizontal = 18.dp,vertical = 12.dp).background(MaterialTheme.colorScheme.onPrimary),style = richTextStyle,) {Markdown(message.answer.trimIndent())}}}
//ios端
//不会屏闪,也可现实代码,但是没有代码框,iOS端只要遇到代码就有线程报错日志
@Composable
fun BotCommonCardApp(message: MessageModel) {val chatViewModel = koinViewModel(ChatViewModel::class)val isDark = chatViewModel.darkState.collectAsState().valueval subScope = rememberCoroutineScope()val state = rememberRichTextState()LaunchedEffect(Unit) {state.removeLink()state.config.codeSpanBackgroundColor = BackCodeGroundColorstate.config.codeSpanColor = BackCodeTxtColorchatViewModel.processGlobal(GlobalIntent.CheckDarkTheme)if (!state.isCodeSpan) {state.toggleCodeSpan()}
//        ```java //无法解析这个 只有  `Code span example` ,但是3点是代码块,一点是行内代码,}val answerState = remember { mutableStateOf("") }LaunchedEffect(message.answer.trimIndent()) {subScope.launch(Dispatchers.IO) { //貌似频繁IOval newMsg = message.answer.trimIndent().replace("```java", "`").replace("```", "`")answerState.value = newMsg}}RichText(state = state.apply {state.setMarkdown(answerState.value)},modifier = Modifier.padding(horizontal = 18.dp, vertical = 12.dp).background(MaterialTheme.colorScheme.onPrimary),color = MaterialTheme.colorScheme.onTertiary,style = TextStyle(fontFamily = FontFamily.Default,fontWeight = FontWeight.Normal,fontSize = 13.sp,color = MaterialTheme.colorScheme.onTertiary))
}

总结

  大模型的多轮对话实现总体不难,只是很多小问题,现在我只是实现了主体对话功能,把多端都跑起来了,进而熟悉整个MVI的开发设计,以前对依赖注入Koin不够重视觉得没啥应用,在声明式开发中现在非常好用,绝大数工具实体都不用new对象,而且借助remember和flow做到状态随处感知,界面渲染代码 少很多,后续引入各种库的高级用法优化代码结构。现在是AI大火,跨端开发AI应用探索下这技术路线的可行性,过于先进确实很多问题只能自己解决,多用chatgpt给点思路也行。
  kotlin multiplatform 开发框架最大好处是直接用kotlin实现跨端逻辑代码,再结合compose multiplatform框架把UI框架也补全了,不需要像Futter要引入两个渲染引擎,一个引擎使用 C/C++ 开发,直接调用 OpenGL/Skia 的 API 进行绘制,从而摆脱 iOS 的 UIKit 以及 Android 的 View 组件直接渲染成需要的样式,保证样式高度统一,另一个是 Dart 语言的 Runtime,用于解析并运行 Dart 语言编译的 Bundle,导致apk包很大。今年的编译器IDEA也支持运行开发kotlin multiplatform项目,Fleet编译器不整了,目前生态库也不断加入比上一年开始完善很多,前期配置好后面的编译就很省事,前期的拉库是个漫长等待。

相关文章:

Compose Multiplatform+Kotlin Multiplatfrom 第四弹跨平台

文章目录 引言功能效果开发准备依赖使用gradle依赖库MVIFlow设计富文本显示 总结 引言 Compose Multiplatformkotlin Multiplatfrom 今天已经到compose v1.7.3&#xff0c;从界面UI框架上实战开发看&#xff0c;很多api都去掉实验性注解&#xff0c;表示稳定使用了&#xff01;…...

【Proteus仿真】【STM32单片机】全自动养护智能生态雨林缸

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真STM32单片机控制器&#xff0c;使用按键、LCD1602液晶、DS18B20模块、PCF8591 ADC、浑浊传感器、PH传感器、液位传感器、继电器、水泵、酸碱调节剂、加热降温装置等。 主要功能&am…...

GBT32960 协议编解码器的设计与实现

GBT32960 协议编解码器的设计与实现 引言 在车联网领域&#xff0c;GBT32960 是一个重要的国家标准协议&#xff0c;用于新能源汽车与监控平台之间的数据交互。本文将详细介绍如何使用 Rust 实现一个高效可靠的 GBT32960 协议编解码器。 整体架构 编解码器的核心由三个主要组…...

SolidWorks 转 PDF3D 技术详解

在现代工程设计与制造流程中&#xff0c;不同软件间的数据交互与格式转换至关重要。将 SolidWorks 模型转换为 PDF3D 格式&#xff0c;能有效解决模型展示、数据共享以及跨平台协作等问题。本文将深入探讨 SolidWorks 转 PDF3D 的技术原理、操作流程及相关注意事项&#xff0c;…...

OpenMCU(二):GD32E23xx FreeRTOS移植

概述 本文主要描述了GD32E230移植FreeRTOS的简要步骤。移植描述过程中&#xff0c;忽略了Keil软件的部分使用技巧。默认读者熟练使用Keil软件。本文的描述是基于OpenMCU_FreeRTOS这个工程&#xff0c;该工程已经下载放好了移植GD32E230 FreeRTOS的所有文件 OpenMCU_FreeRTOS工程…...

Codeforces Round 835 (Div. 4)题解ABCDEFG

Problem - A - Codeforces 题意&#xff1a;你有 t 组数据&#xff0c;每组有两两不同的三个数 a,b,c&#xff0c;现在需要你求出他们的中位数。 思路&#xff1a;模拟即可 // Code Start Here int t;cin >> t;while(t--){vector<int> a(3);for(int i 0;i<3…...

NO1.C++语言基础|四种智能指针|内存分配情况|指针传擦和引用传参|const和static|c和c++的区别

1. 说⼀下你理解的 C 中的四种智能指针 智能指针的作用是管理指针&#xff0c;可以避免内存泄漏的发生。 智能指针就是一个类&#xff0c;当超出了类的作用域时&#xff0c;就会调用析构函数&#xff0c;这时就会自动释放资源。 所以智能指针作用的原理就是在函数结束时自动释…...

SQLite Having 子句详解

SQLite Having 子句详解 引言 SQLite 是一款轻量级的数据库管理系统,广泛应用于移动设备、嵌入式系统和各种桌面应用程序。在 SQL 查询中,HAVING 子句是用于过滤结果集的关键部分,尤其是在使用 GROUP BY 子句进行分组操作时。本文将详细解析 SQLite 中的 HAVING 子句,包括…...

Python数据分析面试题及参考答案

目录 处理 DataFrame 中多列缺失值的 5 种方法 批量替换指定列中的异常值为中位数 使用正则表达式清洗电话号码格式 合并两个存在部分重叠列的 DataFrame 将非结构化 JSON 日志转换为结构化表格 处理日期列中的多种非标准格式(如 "2023 年 12 月 / 05 日") 识…...

Spring Boot 3 整合 MinIO 实现分布式文件存储

引言 文件存储已成为一个做任何应用都不可回避的需求。传统的单机文件存储方案在面对大规模数据和高并发访问时往往力不从心&#xff0c;而分布式文件存储系统则提供了更好的解决方案。本篇文章我将基于Spring Boot 3 为大家讲解如何基于MinIO来实现分布式文件存储。 分布式存…...

ubuntu20 安装python2

1. 确保启用了 Universe 仓库 在某些情况下&#xff0c;python2-minimal 包可能位于 Universe 仓库中。你可以通过以下命令启用 Universe 仓库并更新软件包列表&#xff1a; bash复制 sudo add-apt-repository universe sudo apt update 然后尝试安装&#xff1a; bash复制…...

2025.3.3总结

周一这天&#xff0c;我约了绩效教练&#xff0c;主要想了解专业类绩效的考核方式以及想知道如何拿到一个更好的绩效。其他的岗位并不是很清楚&#xff0c;但是专业类的岗位&#xff0c;目前采取绝对考核&#xff0c;管理层和专家岗采取相对考核&#xff0c;有末尾淘汰。 通过…...

多线程-JUC源码

简介 JUC的核心是AQS&#xff0c;大部分锁都是基于AQS扩展出来的&#xff0c;这里先结合可重入锁和AQS&#xff0c;做一个讲解&#xff0c;其它的锁的实现方式也几乎类似 ReentrantLock和AQS AQS的基本结构 AQS&#xff0c;AbstractQueuedSynchronizer&#xff0c;抽象队列…...

ICLR 2025|香港浸会大学可信机器学习和推理课题组专场

点击蓝字 关注我们 AI TIME欢迎每一位AI爱好者的加入&#xff01; AITIME 01 ICLR 2025预讲会团队专场 AITIME 02 专场信息 01 Noisy Test-Time Adaptation in Vision-Language Models 讲者&#xff1a;曹晨涛&#xff0c;HKBU TMLR Group一年级博士生&#xff0c;目前关注基础…...

docker引擎备份及解决拉取失败的问题

总结一下本文&#xff0c;docker引擎不是越多越好&#xff0c;此外阿里云的容器引擎加速可适用大多数情况。 docker引擎备份 仅使用阿里云 docker引擎备份&#xff0c;唯一使用的镜像地址是我的阿里云docker镜像加速地址&#xff0c;效果好&#xff08;注意下面的阿里云镜像加…...

Django项目实战

1、安装django 查看包安装的位置 pip镜像源 镜像源名称镜像地址​清华源​https://pypi.tuna.tsinghua.edu.cn/simple​阿里云​https://mirrors.aliyun.com/pypi/simple​腾讯云​https://mirrors.cloud.tencent.com/pypi/simple​华为云​https://repo.huaweicloud.co…...

【ThreeJS Basics 1-6】Camera

文章目录 Camera 相机PerspectiveCamera 透视相机正交相机用鼠标控制相机大幅度转动&#xff08;可以看到后面&#xff09; 控制组件FlyControls 飞行组件控制FirstPersonControls 第一人称控制PointerLockControls 指针锁定控制OrbitControls 轨道控制TrackballControls 轨迹球…...

SpringBoot-模拟SSE对话交互

SpringBoot-模拟SSE对话交互 后端使用SSE进行会话&#xff0c;前端使用Html模拟大模型的问答交互->【前端】【后端】 1-学习目的 本项目代码仓库&#xff1a;https://gitee.com/enzoism/springboot_sse 1-核心知识点 1&#xff09;什么是SSE协议->客户端发起一次请求&am…...

删除链表的倒数第N个节点 力扣19

一、题目 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5]示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;[]示例 3&a…...

IvorySQL v4 逻辑复制槽同步功能解析:高可用场景下的数据连续性保障

功能简介 IvorySQL v4 基于 PostgreSQL 17&#xff0c;引入了逻辑复制槽同步至热备份数据库的功能。这一改进有效解决了旧版本中主数据库与备份数据库切换后逻辑复制中断的问题。对于那些追求数据高可用性和业务连续性的数据库来说&#xff0c;这无疑是一个重大的利好消息。它…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

.Net框架,除了EF还有很多很多......

文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

Linux-07 ubuntu 的 chrome 启动不了

文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了&#xff0c;报错如下四、启动不了&#xff0c;解决如下 总结 问题原因 在应用中可以看到chrome&#xff0c;但是打不开(说明&#xff1a;原来的ubuntu系统出问题了&#xff0c;这个是备用的硬盘&a…...

DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”

目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...

算法岗面试经验分享-大模型篇

文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer &#xff08;1&#xff09;资源 论文&a…...

深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用

文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么&#xff1f;1.1.2 感知机的工作原理 1.2 感知机的简单应用&#xff1a;基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化

缓存架构 代码结构 代码详情 功能点&#xff1a; 多级缓存&#xff0c;先查本地缓存&#xff0c;再查Redis&#xff0c;最后才查数据库热点数据重建逻辑使用分布式锁&#xff0c;二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...

Qt 事件处理中 return 的深入解析

Qt 事件处理中 return 的深入解析 在 Qt 事件处理中&#xff0c;return 语句的使用是另一个关键概念&#xff0c;它与 event->accept()/event->ignore() 密切相关但作用不同。让我们详细分析一下它们之间的关系和工作原理。 核心区别&#xff1a;不同层级的事件处理 方…...

Ubuntu系统复制(U盘-电脑硬盘)

所需环境 电脑自带硬盘&#xff1a;1块 (1T) U盘1&#xff1a;Ubuntu系统引导盘&#xff08;用于“U盘2”复制到“电脑自带硬盘”&#xff09; U盘2&#xff1a;Ubuntu系统盘&#xff08;1T&#xff0c;用于被复制&#xff09; &#xff01;&#xff01;&#xff01;建议“电脑…...

十九、【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建

【用户管理与权限 - 篇一】后端基础:用户列表与角色模型的初步构建 前言准备工作第一部分:回顾 Django 内置的 `User` 模型第二部分:设计并创建 `Role` 和 `UserProfile` 模型第三部分:创建 Serializers第四部分:创建 ViewSets第五部分:注册 API 路由第六部分:后端初步测…...