引言
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预览
gif图太大了,没法压缩到10mb,给出链接iOS/macOs
Windows版电脑有水印不好截图,放下载包.exe
开发准备
- 依托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.viewmodel
import 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.viewModelScope
class 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().value
val 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().value
val subScope = rememberCoroutineScope()
val state = rememberRichTextState()
LaunchedEffect(Unit) {
state.removeLink()
state.config.codeSpanBackgroundColor = BackCodeGroundColor
state.config.codeSpanColor = BackCodeTxtColor
chatViewModel.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) { //貌似频繁IO
val 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编译器不整了,目前生态库也不断加入比上一年开始完善很多,前期配置好后面的编译就很省事,前期的拉库是个漫长等待。