最近学习之余注意到了Compose MultiPlatform,然后就想试试水,正好最近越来越依靠ChatGPT,这东西是真香啊,可是总觉得每次都要找套壳网站,想用还得翻开浏览器,我很懒 ̄へ ̄,然后我大概找了一下,网上如同也没有人做私家桌面版的小工具(虽然这玩意完全不难做吧,但便是如同没看到有),正好又想玩玩Compose Desktop,于是就花了两天写了这个ChatPTQ。
由于这个项目比较简单,没啥特别好提的,所以这篇文章会更多地偏谈天式、口语化。
GitHub传送门
1 运用
- Windows运用,无需装置,开箱即用
- 运用前先在Setting里装备
- 个人私用,安全、高效、便利
详细的介绍和运用部分就直接看GitHub的文档吧,不想再写一遍了,下面简单说说项目的完成和Compose Desktop的试水体验。
2 完成
由于是个很小的项目,许多当地都很粗糙,能用就行,没有细心想了。
2.1 UI
就弄了俩页面,Chat和Setting,组件也基本上都是Material的组件。
写起来感触和Compose Android差不多 (除了有些东西不支持,比较简陋)。
可是捏,我如同没找到Toast怎样弄出来,报错的时分我想弹Toast提示用户,那行,只能自己完成一个了。
2.2 Toast完成
界说Toaster接口,随意写几个常用的重载方法。
interface Toaster {
fun toast(message: String) = toast(message, 2500L, true)
fun toast(message: String, duration: Long) = toast(message, duration, true)
fun toast(message: String, duration: Long, success: Boolean)
fun toastFailure(message: String) = toast(message, 2500L, false)
}
创立LocalAppToaster。
val LocalAppToaster = compositionLocalOf { Toaster.Default }
封装Toast效果域。在这个Composable里,界说toast的显现,然后把App这个Composable扔进CompositionLocalProvider里,让它给整个App域都供给Toaster接口,这样,任意的子Composable都能经过LocalAppToaster.current获取到当前的toaster,然后触发toast回调,然后设置个小动画(淡入淡出),Toast就完成了。
package view
@Composable
fun Toast(App: @Composable () -> Unit) {
var showToast by remember { mutableStateOf(false) }
var toastColor by remember { mutableStateOf(toastColors[0]) }
val coroutineScope = rememberCoroutineScope()
var toastText by remember { mutableStateOf("") }
val toaster by remember {
mutableStateOf(object : Toaster {
override fun toast(message: String, duration: Long, success: Boolean) {
if (showToast) {
return
}
coroutineScope.launch {
toastText = message
toastColor = toastColors[if (success) 0 else 1]
showToast = true
delay(duration)
showToast = false
}
}
})
}
CompositionLocalProvider(LocalAppToaster provides toaster) {
Box(modifier = Modifier.fillMaxSize()) {
App()
AnimatedVisibility(showToast, modifier = Modifier.align(Alignment.Center)) {
Box(modifier = Modifier.wrapContentSize().clip(shape = RoundedCornerShape(6.dp)).background(toastColor)) {
Text(toastText, modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp))
}
}
}
}
}
这儿我偷了懒,没有处理多条Toast的状况,仅仅简单的阻拦了一下,当前正在显现的话,就不显现。
Main.kt中,用Toast把RoutePage(真正的页面内容)包一层,就行了:
@Composable
fun App() {
MaterialTheme {
Toast {
RoutePage()
}
}
}
2.3 Config
讲到Toast就顺便说一下Config全局装备,由于它的完成思路和Toast差不多。
全局装备包含这么几项:
data class AppConfig(
val enableSystemProxy: Boolean = false,
val userProxy: UserProxy = UserProxy("0.0.0.0", 0),
val apiKey: String = "",
val autoStart: Boolean = false,
val gptName: String = "小彭"
)
需求做到:
- 在Setting界面能修改设置,并自动保存到本地且生效
- 任意页面能获取到想要的装备项
完成方式也和Toast差不多。
界说一个回调OnConfigChange,参数代表新的AppConfig,再界说一个AppConfigContext类封装一下AppConifg,给它加个回调。然后把AppConfigContext界说成CompositionLocal的。
typealias OnConfigChange = (AppConfig) -> Unit
class AppConfigContext(val appConfig: AppConfig = AppConfig(), val onConfigChange: OnConfigChange)
val LocalAppConfig = compositionLocalOf { AppConfigContext { } }
同样地,界说一个Config的封装Composable块。设一个LaunchedEffect,一启动就读一次装备,然后运用装备,然后把装备暂记下来。然后,CompositionLocalProvider套住App块,这样就能给全局供给AppConfigContext,其它页面能经过LocalAppConfig.current获取。若接收到页面发来的更改装备事情,统一在AppConfigContext的回调中处理,然后更新本地文件和appConfig变量,appConfig的改动会导致UI刷新。
@Composable
fun AppConfig(App: @Composable () -> Unit) {
var appConfig by remember { mutableStateOf(AppConfig()) }
val coroutineScope = rememberCoroutineScope()
val toast = LocalAppToaster.current
LaunchedEffect(Unit) {
val jsonConfig = readConfig() ?: run {
toast.toastFailure("装备文件未找到")
return@LaunchedEffect
}
applyChanges(null, jsonConfig, onSuccess = {
}, onFailure = {
toast.toastFailure(it)
})
appConfig = jsonConfig
}
CompositionLocalProvider(LocalAppConfig provides AppConfigContext(appConfig) { new ->
coroutineScope.launch {
writeConfig(appConfig, new, onSuccess = {
}, onFailure = {
toast.toastFailure(it)
})
appConfig = new
}
}) {
App()
}
}
Main.Kt中:
@Composable
@Preview
fun App() {
MaterialTheme {
Toast {
AppConfig {
RoutePage()
}
}
}
}
Setting页面更改装备示例:
@Composable
fun ApiKey(appConfig: AppConfig, onChange: OnConfigChange) {
SettingPanel("恳求设置") {
OutlinedTextField(appConfig.apiKey,
label = {
Text("APIKey")
},
onValueChange = {
//onChnage便是LocalAppConfig.current.onConfigChange
onChange(appConfig.copy(apiKey = it))
}
)
}
}
2.4 网络恳求和数据存储
Compose Desktop的网络恳求能够用Retrofit,小数据的存储能够用DataStore,详细地就不多说了,项目里仅仅很简陋地封装了一下,基本上Android怎样用,Desktop就怎样用。
源码
数据的存储为了偷懒我都存项目根途径了(即File(“.”)),如果是打包的exe便是exe所在的途径。
2.5 方便Enter键发送
键盘的监听能够用Modifier.onPreviewKeyEvent或者onKeyEvent,仅仅感觉键盘事情处理起来很扎手,由于搜狗输入法中文已经有字的时分敲Enter、普通的敲Enter换行、方便Enter发送,这三种场景都要Enter,得想方法处理好方便键事情,这儿我想的是长按就触发方便发送,可是代码写起来很不顺手,不知道还有没有更好的处理方式。(直接在输入框敲Enter,会触发一个awt无法辨认的事情,详细地能够自己println出来试一试。)
2.6 打包
关于打包的参数什么的,能够看官方文档。 (官方文档的教程还有许多别的功能,我没细心看,有相对应的需求能够自己去找找)
可是呢,官方打出来的包如同只能是一个装置包,便是有装置程序,然后装置到自己电脑上用,可是呢,我不知道是什么原因,一运转exe装置程序,他就自动变成了后台进程,然后什么反响都没有了,也便是无法装置。
那我就换一种思路吧,横竖整个运用也不大,能不能直接生成一个可履行的exe呢,直接双击就运转,不搞什么装置。这样也不会往c盘乱塞东西。
最终找到了方法,履行compose desktop下的CreateDistributable Task就能够了,会在build/compose/binaries底下输出直接可履行的exe。
3 结语
这次经过这个小项目试水了Compose Desktop,然后也做出了一个确实能够给自己工效果的ChatGPT桌面端运用。Compose MultiPlatform感觉还是很香的,基本上便是零学习成本写UI,直接就能上手写,而且kotlin是真的好用,爽爽,可是感觉现在有些当地还是不完善,究竟还没生长起来嘛,社区也没怎样起来,可是确实是感觉未来可期!
最终贴一个我完成过程中参考了的项目:从0到1搞一个 Compose Desktop 版本的气候运用
就写到这儿吧~