diff --git a/Learning/复杂逻辑与多平台不同逻辑.md b/Learning/复杂逻辑与多平台不同逻辑.md new file mode 100644 index 0000000..21c7701 --- /dev/null +++ b/Learning/复杂逻辑与多平台不同逻辑.md @@ -0,0 +1 @@ +# 复杂逻辑与多平台不同逻辑 \ No newline at end of file diff --git a/Learning/深入Kotlin.md b/Learning/深入Kotlin.md index ac816e8..254d9d2 100644 --- a/Learning/深入Kotlin.md +++ b/Learning/深入Kotlin.md @@ -79,6 +79,8 @@ class MyClass( ## 特殊函数 +### 嵌套 lambda 与注解 + ### infix 函数 ### 扩展函数 diff --git a/Learning/第一个KMP应用.md b/Learning/第一个KMP应用.md index 9924db1..f13b46f 100644 --- a/Learning/第一个KMP应用.md +++ b/Learning/第一个KMP应用.md @@ -1 +1,215 @@ -# 第一个KMP应用 \ No newline at end of file +# 第一个 KMP 应用 + +这里,我推荐刚开始上手 KMP 的朋友们先写仅有 JVM 平台的 KMP 应用。 + +## 项目结构 + +- 项目根目录/ + - desktopApp/ + - src/ + - build.gradle.kts + - shared/ + - src/ + - commonMain/ + - composeResources/ + - kotlin/ + - commonTest/ + - jvmMain/ + - build.gradle.kts + - build.gradle.kts + - settings.gradle.kts + +## 跑起来看看效果 + +Gradle 默认会注册启动项 `hotRun`,它会被 IDEA / Android Studio 识别为 `desktopApp [hot] 🔥`。这是开发阶段的主要的测试方法。点击,即可启动编译,并运行热重载程序,它可以在你修改源代码并编译后,直接在原有程序窗口同步你的修改。 + +## commonMain 编写设计 + +可以看到 commonMain 的 App.kt 已经包含了一些内容了: + +```kotlin +@Composable +@Preview +fun App() { + MaterialTheme { + var showContent by remember { mutableStateOf(false) } + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.primaryContainer) + .safeContentPadding() + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Button(onClick = { showContent = !showContent }) { + Text("Click me!") + } + AnimatedVisibility(showContent) { + val greeting = remember { Greeting().greet() } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image(painterResource(Res.drawable.compose_multiplatform), null) + Text("Compose: $greeting") + } + } + } + } +} +``` + +接下来将对其中的细节进行讲解。 + +### 基本的 UI 设计思路 + +#### 结构 + +KMP 的 UI 是基于一个又一个“盒子”的,在这里,最外层是一个主题 `MaterialTheme`,在主题内部是一个 `Column`,看它的 `modifier` 参数,这里有一个 `fillMaxSize`,表明这个盒子会占据整个窗口。在一个 Column 中,内部的所有组件都会纵向排列。 + +Column 内部,有一个 `Button`,还有一个被 `AnimatedVisibility` 框起来的 Column,这里我们先不管 AnimatedVisibility 所实现的动画效果,我们看排版结构。前文提到,在 Column 内部,也就是它的 lambda 里面,所有的组件会纵向排列,也就是说 Button 和内层 Column 会在窗口中一个在上一个在下。 + +#### 图片资源 + +内层的 Column 又包含了两个组件,一个是图片,一个是文本。这里有一个东西叫图片资源,它们的源文件放在 `commonMain/composeResources/drawable` 中,放在这里的图片会自动被 gradle 注册在一个 object `Res.drawable` 之中,变量名就是它们的文件名。这是 KMP 中最好的调用图片方式。 + +### 基本的逻辑处理思路 + +我们来看它的 showContent 变量如何被定义,然后在 Button 被点击时被改变 + +```kotlin + // 将 showContent 委托给一个 Compose state + var showContent by remember { mutableStateOf(false) } + + // 一些排版 + + // 这里使用一个 lambda 将 showContent 的值反转 + Button(onClick = { showContent = !showContent }) +``` + +因为变量 showContent 是函数 AnimatedVisibility 的参数之一,因此,当它的值发生变化时,该函数会被重载,随后触发动画效果,其中的 Column 就会出现 / 消失。 + +## 更多的排版工具与资源文档 + +[排版 / 组件](https://m3.material.io/components) + +[Google 提供的矢量图标](https://fonts.google.com/icons) + +你可以这样使用它们: + +```kotlin +Icon( + imageVector = Icons.Default.Favorite, // 在网站上可以直接查到 + contentDescription = null, // 图片的文字标记,一般用于无障碍 + tint = Color(0xFFE53935) // 注意,alpha 值写在第一位 +) +``` + +## main 函数 + +刚刚生成的新项目,它的 main 函数一般是这样的: + +```kotlin +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "YourProjectName", + ) { + App() + } +} +``` + +在这个 `Window` 函数中,可以用 `state = rememberWindowState(width = 900.dp, height = 800.dp)` 来设定窗口的初始大小。用 `icon = painterResource()` 设定窗口图标。例如: + +```kotlin +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = Const.FULL_APP_NAME, // 自己定义一个 Const 对象,很方便的东西 + state = rememberWindowState(width = 900.dp, height = 800.dp), + icon = painterResource(Res.drawable.kmp) + ) { + App() + } +} +``` + +## 设置 desktop 编译选项 + +刚刚生成的新项目,它的 `desktopApp/build.gradle.kts` 一般是这样的: + +```kotlin +compose.desktop { + application { + mainClass = "your.package.name.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "your.package.name" + packageVersion = "1.0.0" + } + } +} +``` + +其实这样就可以编译对应三大桌面平台的安装包了。不过如果你想做的更细致点也是可以的: + +```kotlin +compose.desktop { + application { + mainClass = "your.package.name.MainKt" + + nativeDistributions { + targetFormats( + // Windows + TargetFormat.Msi, + TargetFormat.Exe, + // macOS + TargetFormat.Dmg, + // Linux + TargetFormat.Deb, + TargetFormat.Rpm, + TargetFormat.AppImage + ) + // 你可以在 gradle 文件中用 Kotlin 一样的方法定义变量,这很方便 + packageName = appName + packageVersion = appVersion + + // 如果可以直接运行,那么就不要写 proguard 这一段 + buildTypes.release.proguard { + configurationFiles.from(files("proguard-rules.pro")) + } + + windows { + iconFile.set(project.file("icons/icon.ico")) + menuGroup = appName + shortcut = true + dirChooser = true + } + + macOS { + iconFile.set(project.file("icons/icon.icns")) + bundleID = packageName + appStore = false + signing { + sign.set(false) + } + } + + linux { + iconFile.set(project.file("icons/icon.png")) + menuGroup = appName + shortcut = true + } + } + } +} +``` + +## 接下来可以看 + +[目录](../README.md) + +[我曾经写的一个简单的小应用](https://github.com/SOR2171/Dices) + +[下一篇:复杂逻辑与多平台不同逻辑](./复杂逻辑与多平台不同逻辑.md) \ No newline at end of file diff --git a/Learning/进入KMP.md b/Learning/进入KMP.md index 0c08a75..9fc07b3 100644 --- a/Learning/进入KMP.md +++ b/Learning/进入KMP.md @@ -25,7 +25,7 @@ - build.gradle.kts - .gitignore - README.md - - gradlew (二进制脚本,一个bat,一个sh) + - gradlew (二进制脚本,一个 bat,一个 sh) - gradle.proporties - local.proporties (默认被 ignore) - build.gradle.kts @@ -50,6 +50,8 @@ KMP 的 UI 以 `@Composable` 函数为核心,各个平台的 `main()` 函数入口都会默认调用 `shared/commonMain/kotlin` 中的 `App()` 函数。基本的 UI 也都会在 commonMain 中编写。 +在 Composable 函数中,通过各种 UI 函数的相互组合,便可以设计出无限可能的 GUI。 + ### @Composable 函数 我们先讲 `@Composable`,它是一个注解,用于函数或 lambda 变量: @@ -61,15 +63,15 @@ fun MyButton(a: Int) { } val myCard = @Composable { a: Int -> - TODO() // lambda 不推荐 + TODO() // lambda 不推荐大写首字母 } // 变量类型是 @Composable (Int) -> Unit ``` 它有几个重要的特点: -- Composable 函数只能在 main 函数或 Composable 函数中被调用。 +- Composable 函数只能在 main 函数(描述不严谨)或 Composable 函数中被调用。 - 当 Composable 函数的参数发生变化时,它会被立刻重组(重新触发),受影响的 UI 立刻重新绘制。 -- 如果 Composable 函数的 lambda 参数的函数体中出现了 `Compose state` 对象,那么,当它的状态变化时,函数也会被重组。 +- 如果 Composable 函数的 lambda 参数的函数体中使用了 `Compose state` 对象的 `value`,那么,当它的状态变化时,lambda 所在函数也会被重组。 ### 保护变量值