赞
踩
本文原作者: 朱江,原文发布于: 掘金
https://juejin.cn/post/7176875120839884860
这篇文章带大家一起来玩下 Compose Desktop,从头到尾写一个桌面版的天气应用,并且打好包让别人也可以进行使用,接下来就开始吧!先来看下最终的实现效果吧!
效果是不是挺好?哈哈哈!
其实作为一个安卓开发来说,当运行起第一个桌面版程序的时候内心突然感觉回到了最开始学习编程的时候,那种感觉就好像一个多年未见的老友对您说: 久违了!特别是使用的技术还都是安卓开发的技术,只是有一些平台原因需要稍做修改的地方就能开发出一个完整的桌面版软件,内心还是非常激动,非常地有成就感,这种感觉太舒服了!
缘起 Compose
为什么会搞 Compose Desktop,这还得从 Jetpack Compose 说起: Google 从 2017 年开始立项开搞 Compose 到第一个正式版本用了四年的时间,那么久的时间,投入了那么多的人力,以及后面投入了大量经费宣传,无一不在告诉安卓开发者 Compose 很重要,这也是之后安卓开发的新方向!所以当第一个 alpha 版本的 Compose 出现的时候我就坐不住了,立马加上依赖尝试了下!刚开始写的时候感觉有点奇怪,毕竟从之前的开发模式变为了全新的声明式开发,但写了不到一周就感受到了 Compose 的优势,编写起来太快了,动画实现起来也太简单了,声明式编程也太方便了。。。。
其实 Compose Desktop 出现的也很早,Jetpack Compose 出来没多久它也就出来了,有很多同行在 Compose Desktop 出来第一个 alpha 版本的时候就开始研究,
对标 Flutter?
Flutter 现在已经比较成熟了,它最大的优势就是跨平台,Flutter 虽然宣称的是原生的性能,一套代码多端实现,但其实对于跨平台来说一套代码并不能完全实现需求,肯定需要各种适配,只不过看框架适配的好与坏,Compose Desktop 也是如此,但 Flutter 的性能也只是媲美原生,而 Compose 就是原生啊!Compose Desktop 其实并不是和 Flutter 抢饭碗,它只是告诉广大安卓开发: 并不需要学习安卓之外的东西就能开发各种设备上的应用!这也是 Kotlin 的辉煌,我个人认为这也是 Jetbrains 公司开发 Compose Desktop 的初衷。
基于上面的分析,打开了 Jetbrains 的 Compose Desktop 的官网: https://www.jetbrains.com/zh-cn/lp/compose-desktop/,也开始试着玩一玩桌面版的应用!
初探 Compose Desktop
我本来还想着使用 Android Studio 来使用 Compose Desktop 来着,结果打开 Android Studio 新建项目一看并没有找到创建 Compose Desktop 的入口,后来想想也对,Android Studio 嘛!本来就是为了构建 Android 项目的,并不是为了构建别的东西,对吧!(内心独白: 可能是我自己没找到)
那就使用 IntelliJ Idea 来看看吧,点击 New -> Project 就会出现下面的页面:
选择 Kotlin 之后就可以看到右边有 Compose Multiplatform 的选项,里面有三种,第一种就是这段时间要搞的 Compose Desktop,第二种就是多平台了,里面有 Android,也有 Compose Desktop,第三种是 Compose Web。不得不说太强了!桌面、Web、移动端,Compose 一套搞定!目前 IOS 也支持了,不过这不是咱们要看的重点,还是来看 Compose Desktop 吧!接下来点击 Next,之后选择配置项之后点击 Finish 后第一个 Compose Desktop 项目就创建好了!
项目结构
接下来看下初始项目的结构吧:
OK,有一个 Main.kt 文件,还有 build 和 settings 文件。下面咱们一个一个来看,先来看下 settings 文件吧:
- pluginManagement {
- repositories {
- google()
- gradlePluginPortal()
- maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
- }
-
- }
- rootProject.name = "Demo"
嗯,这个很简单,放了依赖的仓库地址,还有项目的名称。
接下来再来看下 build 文件:
- plugins {
- kotlin("jvm") version "1.5.31"
- id("org.jetbrains.compose") version "1.0.0"
- }
-
-
- dependencies {
- implementation(compose.desktop.currentOs)
- }
-
-
- tasks.withType<KotlinCompile> {
- kotlinOptions.jvmTarget = "11"
- }
-
-
- compose.desktop {
- application {
- mainClass = "MainKt"
- nativeDistributions {
- targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
- packageName = "Demo"
- packageVersion = "1.0.0"
- }
- }
- }
build 文件中稍微多点,分别是 plugins、dependencies、jvmTarget 和 application,前几个就不过多介绍,因为安卓项目中都有,最后的 application 是这里独有的,其实这块就是对桌面项目的一些属性的配置,可以看到有包名和版本号等信息,这块在这里先不进行过多介绍,因为这块的内容很多,Windows、Mac、Linux 各个系统的配置都不太相同,在之后的文章中会着重来介绍,这里先跳过。
初始代码
最后来看下 Main.kt 文件:
- @Composable
- @Preview
- fun App() {
- var text by remember { mutableStateOf("Hello, World!") }
- MaterialTheme {
- Button(onClick = {
- text = "Hello, Desktop!"
- }) {
- Text(text)
- }
- }
- }
-
-
- fun main() = application {
- Window(onCloseRequest = ::exitApplication) {
- App()
- }
- }
代码并不多,而且很熟悉,但也有不认识的地方。可以看到这里出现了 Java 中熟悉的 Main 方法,然后里面调用了一个 application 方法,在其中有一个可组合项 Window,在里面调用了 App 可组合项。
Application
可组合项咱们都是非常了解的,这块不太清楚的其实就是 application 和 Window,因为这两个在之前 Jetpack Compose 中都是没有的,下面咱们就先来看看 application:
- fun application(
- exitProcessOnExit: Boolean = true,
- content: @Composable ApplicationScope.() -> Unit
- ) {
- val configureSwingGlobals = System.getProperty("compose.application.configure.swing.globals") == "true"
- if (configureSwingGlobals) {
- configureSwingGlobalsForCompose()
- }
- runBlocking {
- awaitApplication {
- content()
- }
- }
- if (exitProcessOnExit) {
- exitProcess(0)
- }
- }
application 是 Compose Desktop 应用程序的入口点,这块需要注意的是: 不要在这个函数中使用任何动画 (例如,withframamanos 或 animatefloatasstate 等),因为底层的 MonotonicFrameClock 没有与任何显示同步,所以无法尽快地生成帧。
方法一共接收两个参数,来分别看下:
exitProcessOnExit: 结束进程,默认为 true,在应用程序关闭后调用 exitProcess(0),exitProcess 加速进程退出 (立即退出,而不是 1-4 秒)。如果为 false,函数的执行将在应用程序退出后被解除阻塞 (当最后一个窗口关闭,以及所有 LaunchedEffect 完成时)。
content: 放可组合项的,不做多介绍。
Window
下面再来看下可组合项 Window:
- @Composable
- fun Window(
- onCloseRequest: () -> Unit,
- state: WindowState = rememberWindowState(),
- visible: Boolean = true,
- title: String = "Untitled",
- icon: Painter? = null,
- undecorated: Boolean = false,
- transparent: Boolean = false,
- resizable: Boolean = true,
- enabled: Boolean = true,
- focusable: Boolean = true,
- alwaysOnTop: Boolean = false,
- onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
- onKeyEvent: (KeyEvent) -> Boolean = { false },
- content: @Composable FrameWindowScope.() -> Unit
- )
Window 的代码有点多,这块咱们先不关心里面的具体实现,先来看看都有哪些功能。在当前 Compose 中组合成平台窗口。当 Window 进入组合成时,将创建一个新的平台窗口并接收焦点。当 Window 离开合成时,Window 将被释放并关闭。Window 的参数有点多,咱们分别来看下:
onCloseRequest: 当用户关闭窗口时将被调用的回调函数
state: 用于控制或观察窗口状态的状态对象
visible: 是否对用户可见
title: 窗口的名称
icon: 窗口的图标 (和应用图标不同,完全两码事)
undecorated: 禁用或启用此窗口的装饰
transparent: 禁用或启用窗口透明度,需要注意: 只有在窗口未装饰时才应该设置透明度,否则将引发异常
resizable: 用户是否可以调整窗口的大小
enabled: 窗口是否能对输入事件作出反应
focusable: 窗口是否可以接收焦点
alwaysOnTop: 窗口是否在另一个窗口的顶部
onPreviewKeyEvent: 当用户与硬件键盘交互时调用此回调,它为聚焦组件的祖先提供了拦截 KeyEvent 的机会
onKeyEvent: 当用户与硬件键盘交互时调用此回调。在实现此回调时,返回 true 以停止此事件的传播。如果返回 false,KeyEvent 将被发送给这个 onKeyEvent 的父事件。
第一次运行
OK,到现在位置初始项目中的内容大概都过了一遍,接下来运行看下效果吧!
那么问题又来了,怎么运行呢。。。之前咱们运行安卓项目的时候都是点击 Android Studio 上方运行,但现在看下:
没有了,灰着的!不用担心,不还有 main 函数呢嘛!直接运行 main 函数!
点击运行按钮看下:
直接弹出了一个 Java 程序,里面放着一个按钮,刚才咱们看可组合项 Window 的时候不是可以修改名字嘛,下面修改下看看!
- fun main() = application {
- Window(onCloseRequest = ::exitApplication, title = "天青色等烟雨") {
- App()
- }
- }
改了下名字,第二次运行的时候可以点击 main 函数,也可以点击 Idea 的上方运行按钮了,因为刚才运行的记录已经被保存下来了!
OK,点击运行查看结果:
没错,和想的一样!
到现在为止已经可以使用咱们之前学习的 Jetpack Compose 知识来愉快的编程了!
显示图片
刚还想的可以好好使用 Compose 来编写桌面程序来着,可我刚想显示一张图片就发现了问题!怎么显示???
普通图片
在 Jetpack Compose 中显示图片不叫事,直接使用 painterResource 将图片资源传进去就可以了,但在 Compose Desktop 中该怎么办呢?
先来看下 Compose Desktop 中的 Image:
- @Composable
- fun Image(
- painter: Painter,
- contentDescription: String?,
- modifier: Modifier = Modifier,
- alignment: Alignment = Alignment.Center,
- contentScale: ContentScale = ContentScale.Fit,
- alpha: Float = DefaultAlpha,
- colorFilter: ColorFilter? = null
- )
可以看到和 Jetpack Compose 中是一致的,不同的就是如何在 Compose Desktop 中构建 Painter。
先来看一种构建的方式吧:
BitmapPainter(useResource(resourcePath, ::loadImageBitmap))
这块的 resourcePath 指的是图片的路径,这个路径是如何定义的呢?还记得上面创建完项目的初始结构么?里面有一个 resource 文件夹,这个文件夹就是根目录,比如 resource 文件夹中有一张图片 "icon.png",要构建这张图片的 Painter 就可以使用如下代码:
BitmapPainter(useResource("icon.png", ::loadImageBitmap))
当然可以在 resource 中创建不同的文件夹来存放不同的资源,图片也是一样的。
简单解释下这行代码吧,虽然看着就一行,其实使用到了好几个函数,首先说下 useResource,它的作用是将传入的文件路径打开为 InputStream,而 loadImageBitmap 函数是将 InputStream 转为 ImageBitmap,最后通过使用 BitmapPainter 才构建出一个 Painter。
光说不练假把式!来整一张图片试下吧!
Image(painter = BitmapPainter(useResource("image/icon.png", ::loadImageBitmap)),"Test")
由于我将图片放到 resource 中的 image 文件夹中,所以这块的路径做了一些修改,再来看下图片的目录吧:
下面来运行看下效果!
OK,没问题,图片展示出来了!又向成功迈进了一步!!!
SVG 图片?
咱们现在在安卓中使用的图片大多改为了 SVG 格式的,体积又小且清晰,接下来按照相同的方式试一下,先放一张 SVG 格式的图片到刚才创建的 image 文件夹中:
图片放好了,下面来修改下图片的路径:
Image(painter = BitmapPainter(useResource("image/ic_launcher.svg", ::loadImageBitmap)),"Test")
再运行下程序!
额。。。刚不是还好好的嘛!这改了个图片格式就不行了?来看下报错信息吧!
- Exception in thread "AWT-EventQueue-0" java.lang.IllegalArgumentException: Failed to Image::makeFromEncoded
- at org.jetbrains.skia.Image$Companion.makeFromEncoded(Image.kt:139)
- at androidx.compose.ui.res.ImageResources_desktopKt.loadImageBitmap(ImageResources.desktop.kt:33)
- at MainKt$App$1.invoke(Main.kt:31)
- at MainKt$App$1.invoke(Main.kt:24)
可以看到报了编码错误,这应该咋么搞???
Compose Desktop 早就为我们想到了:
useResource(resourcePath) { loadSvgPainter(it, Density(2f)) }
Compose Desktop 为我们提供了一个叫 loadSvgPainter 的函数,专门用来处理 SVG 图片,接下来使用下看看:
Image(painter = useResource("image/ic_launcher.svg") { loadSvgPainter(it, Density(2f)) },"Test")
使用也很简单,运行看下效果吧:
嗯,没问题,正常展示!为了方便大家在 Compose Desktop 中使用图片,我写了一个构建 Painter 的函数:
- /**
- * 构建Painter,为了图片使用
- *
- * @param resourcePath 图片路径
- */
- fun buildPainter(resourcePath: String): Painter {
- val painter: Painter = if (resourcePath.endsWith(".svg")) {
- useResource(resourcePath) {
- loadSvgPainter(it, Density(2f))
- }
- } else if (resourcePath.endsWith(".png") || resourcePath.endsWith(".jpg") ||
- resourcePath.endsWith(".jpeg") || resourcePath.endsWith(".webp") ||
- resourcePath.endsWith(".PNG") || resourcePath.endsWith(".JPG") ||
- resourcePath.endsWith(".JPEG") || resourcePath.endsWith(".WEBP") || resourcePath.endsWith(".ICO")
- ) {
- BitmapPainter(useResource(resourcePath, ::loadImageBitmap))
- } else {
- throw IllegalArgumentException("resource is illegal argument")
- }
- return painter
- }
这里并没有列举全所有的图片的后缀,但咱们一般使用到的都列举了出来,如果有特殊需求的话大家可以自己加上需要的后缀即可。
柳暗花明
上面的一堆都是自己犯傻。。。其实 Compose Desktop 中也可以直接使用 painterResource 来构建图片。
- Image(
- painter = painterResource(getWeatherIcon(dailyBean.iconDay)), "",
- )
哈哈哈,为什么要写上面的一大堆呢,是让大家体会下我当时写的时候的经历。。。 (内心:我是不是太坏了,哈哈哈
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。