快速入门KMM和Compose Multiplatform

一、前言

最近才有空,想起来Compose-jb和kmm这2个框架,就来个快速入门指南吧。

什么是KMM (Kotlin Multiplatform Mobile)

KMM用于简化跨平台开发,可以在Android和IOS之间共享通用的代码。
仅在使用各自平台能力的时候,才会去编写各自平台特定的代码。

Compose Multiplatform, by JetBrains 缩写名称:compose-jb

Compose Multiplatform, by JetBrains

JetBrains开源的compose-jb官方的介绍内容:

桌面和Web UI框架,基于Google的JetpackCompose工具包
Compose Multiplatform 简化并加速了桌面和Web应用程序的UI开发,并且允许Android和桌面之间大部分的UI代码共享。

二、Window平台-开发工具

1、compose-jb环境安装

1、下载IntelliJ IDEA Community Edition

2、下载JAVA JDK

2、KMM环境安装

1、下载AndroidStudio

2、下载JAVA JDK

3、下载Kotlin多平台移动插件

在Android Studio中,在市场中搜索:Kotlin Multiplatform Mobile,然后安装

在这里插入图片描述

4、更新Kotlin插件

Kotlin插件与AndroidStudio版本是捆绑在一起的,我们需要更新Kotlin到最新版本,来避免兼容性问题。
在这里插入图片描述

三、MacOS平台-开发工具

1、compose-jb环境安装

1、下载IntelliJ IDEA Community Edition

2、下载JAVA JDK

2、KMM环境安装

1、下载XCode

2、在终端或命令行工具中,运行以下命令

brew install kdoctor

如果你还没有Homebrew,请安装它或查看KDoctor README以获取其他安装方法。

3、安装完成后,在控制台调用 KDoctor

kdoctor

四、KMM工程

1、创建工程

打开AndroidStudio ,点击 New Project,然后找到 Kotlin Multiplatform App,然后点击 Next

在这里插入图片描述

配置应用程序的名称、应用包名、项目的位置、最小SDK版本,配置完成之后,点击 Next

在这里插入图片描述

iOS framework distribution 我们选择Regular framework, 因为此选项不需要第三方工具,并且安装问题较少。

cocoapods dependency manager 是什么呢?

CocoaPods是 Swift 和 Objective-C Cocoa项目的依赖管理器。

对于更复杂的项目,可能需要CocoaPods依赖项管理器来帮助处理库依赖项。

在这里插入图片描述

点击Finish,首次执行此操作时,下载和设置所需的组件可能需要一些时间。

2、工介绍

KMM工程包含三个模块:

  • androidApp 是Android应用程序项目,依赖于shared模块,并将shared模块用作常规的Android库,UI就是使用Jetpack Compose那一套

  • shared 包含Android和iOS应用程序在平台之间共享的通用代码逻辑

  • iosApp 是iOS应用程序项目,它依赖于并使用shared模块作为iOS框架

在这里插入图片描述
androidApp和iosApp模块都是各自平台原来的开发方式,shared模块它是平台之间共享的通用代码逻辑,那么它如何实现共享的呢?我们看一下下面这张图片:
在这里插入图片描述

连接到各自的平台:

expect和actual文档

我们可以看到它在公共模块中使用expect关键字,expect修饰类、成员变量或方法时,表示类、成员变量、方法,可以跨平台实现。

注意:expect声明不包含任何实现代码。
expect和actual所修饰的类/变量/方法,名称都需要完全一样,并且位于同一包中(具有相同的完整包名)。

在各自的平台Android/IOS,中使用actual修饰,实现同名的类、方法、成员变量。

我们再来看Hello World示例,commonMain目录下面创建了一个Platform接口,并使用expect关键字修饰了getPlatform()方法
在这里插入图片描述

那么Android/IOS平台,需要去使用actual修饰,实现同名的类、方法、成员变量。
在这里插入图片描述

我们在commonMain目录下面,可以定义逻辑类,共享通用的代码逻辑,同样以Hello World为例:
在这里插入图片描述

那么在Android/IOS的工程中就可以使用这个Greeting类来获取通用的代码逻辑,如下:

在这里插入图片描述


在这里插入图片描述

3、如何添加依赖项

切换到Project视图下,找到shared模块,点击build.gradle.kts,在sourceSets里面,我们可以给
commonMain、androidMain、iosMain 分别添加依赖。

我们这里给commonMain添加依赖项,这样Android、IOS平台就可以在获取通用代码逻辑的时候,使用到这个依赖项的能力了。

在这里插入图片描述

添加kotlinx-datetime的依赖项:

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
            }
        }
    }
}

在这里插入图片描述

commonMain目录下面创建一个KotlinNewYear.kt的File:

import kotlinx.datetime.*

fun daysUntilNewYear(): Int {
    val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
    val closestNewYear = LocalDate(today.year + 1, 1, 1)
    return today.daysUntil(closestNewYear)
}

我们在Greeting里面使用这个方法:
在这里插入图片描述

我们看看运行之后的结果:

4、网络请求

我们需要准备以下三个多平台库

  • kotlinx.coroutines,用于使用协程编写异步代码,允许同时操作
  • kotlinx.serialization,用于将 JSON 响应反序列化为用于处理网络操作的实体类的对象。
  • Ktor,一个作为HTTP客户端的框架,用于通过互联网检索数据。

1、添加Kotlinx.coroutines依赖库:

在shared模块的build.gradle.kts中,添加kotlinx.coroutines依赖

sourceSets {
    val commonMain by getting {
        dependencies {
            // ...
           implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
        }
    }
}

我们打开项目根目录下面的build.gradle.kts文件,查看kotlin版本是否小于1.7.20

在这里插入图片描述

使用 Kotlin 1.7.20 及更高版本,则默认情况下已经启用了新的 Kotlin/Native 内存管理器。

Kotlin版本小于1.7.20版本,请将以下内容添加到build.gradle.kts文件末尾:

kotlin.targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget::class.java) {
    binaries.all {
        binaryOptions["memoryModel"] = "experimental"
    }
}

2、添加Kotlinx.serialization依赖库:

我们打开shared模块,打开build.gradle.kts文件,在文件开头的plugins块中增加 序列化插件 内容:

在这里插入图片描述

plugins {
    kotlin("plugin.serialization") version "1.8.0"
}

3、添加Ktor依赖库

我们打开shared模块,打开build.gradle.kts文件,增加下面内容:

val ktorVersion = "2.2.1"

sourceSets {
    val commonMain by getting {
        dependencies {
            // ...
            implementation("io.ktor:ktor-client-core:$ktorVersion")
            implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
            implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
        }
    }
    val androidMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-android:$ktorVersion")
        }
    }
    val iosMain by creating {
        // ...
        dependencies {
            implementation("io.ktor:ktor-client-darwin:$ktorVersion")
        }
    }
}

ktor-client-core:核心依赖项。

ktor-client-content-negotiation:负责序列化/反序列化特定格式的内容。

ktor-serialization-kotlinx-json:使用JSON格式用作序列化库,在接收响应时将其反序列化为数据类。

ktor-client-android:提供Android平台引擎

ktor-client-darwin:提供IOS平台引擎

点击Sync Now同步Gradle之后,我们创建一个RocketLaunch.kt

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class RocketLaunch (
    @SerialName("flight_number")
    val flightNumber: Int,
    @SerialName("name")
    val missionName: String,
    @SerialName("date_utc")
    val launchDateUTC: String,
    @SerialName("success")
    val launchSuccess: Boolean?,
)

点击查看Ktor官方文档

4、创建一个Ktor实例来执行网络请求并解析生成的JSON

import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

class Greeting {
    private val platform: Platform = getPlatform()

    private val httpClient = HttpClient {
        // 安装ContentNegotiation插件
        install(ContentNegotiation) {
            // 注册 JSON 序列化程序
            json(Json {
                // true:表示打印并生成漂亮的JSON。 默认情况下:false
                prettyPrint = true
                // true:删除 JSON 规范限制,允许使用带引号的布尔文本和不带引号的字符串文字
                isLenient = true
                // true:表示可以忽略JSON中遇到的未知属性,防止引发序列化异常。 默认情况下:false
                ignoreUnknownKeys = true
            })
        }
    }
}

点击查看ContentNegotiation Ktor插件文档

修改greeting方法,添加suspend修饰,并使用httpClient去获取网络请求的数据:

import io.ktor.client.call.*
import io.ktor.client.request.*

class Greeting {
    // ...
    @Throws(Exception::class)
    suspend fun greeting(): String {
        // 获取数据
        val rockets: List<RocketLaunch> =
            httpClient.get("https://api.spacexdata.com/v4/launches").body()
        // 判断最近一次火箭是否发射成功
        val lastSuccessLaunch = rockets.last { it.launchSuccess == true }
        // 返回结果
        return "Guess what it is! > ${platform.name.reversed()}!" +
                "\nThere are only ${daysUntilNewYear()} left until New Year! 🎆" +
                "\nThe last successful launch was ${lastSuccessLaunch.launchDateUTC} 🚀"
    }
}

到这里还没结束,这里只是获取数据的代码,Android/IOS项目目录下面,仍然要配置内容,请往下看:

5、Android相关配置

  • 打开androidApp/src/main/AndroidManifest.xml 配置网络权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.jetbrains.simplelogin.kotlinmultiplatformsandbox" >
    <uses-permission android:name="android.permission.INTERNET"/>
</manifest>
  • 打开androidApp/build.gradle.kts 添加Android协程库,处理commMain模块的挂起方法:

在这里插入图片描述

dependencies {
    // ..
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
}

然后点击同步Gradle,打开MainActivity.kt文件,使用协程调用suspend fun greeting()

import androidx.compose.runtime.*
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    val scope = rememberCoroutineScope()
                    var text by remember { mutableStateOf("Loading") }
                    LaunchedEffect(true) {
                        scope.launch {
                            text = try {
                                Greeting().greeting()
                            } catch (e: Exception) {
                                e.localizedMessage ?: "error"
                            }
                        }
                    }
                    GreetingView(text)
                }
            }
        }
    }
}

6、IOS相关配置

IOS这里使用SwiftUI构建UI界面,使用Model-view-ViewModel,将 UI 连接到包含所有业务逻辑的shared模块

ContentView.swift中创建viewModel,并获取shared模块中的网络请求数据:

看到这里可能大家都会感觉很熟悉,鸿蒙的ArkUI和SwiftUI真的好像,感觉就是表兄弟。
Compose和SwitfUI还差点样子,样貌算是外甥的那种哈哈。

在这里插入图片描述

点击查看DispatchQueue.main.async解释

运行之后AndroidIOS的界面显示如下:

5、小结

KMM属于Android和IOS各自写各自平台的UI,通用的业务逻辑数据处理需要从shared模块去获取。
优点:重复的业务逻辑数据处理部分,统一处理,在业务需求发生变更,也只需要更新shared模块即可,Android/IOS各自平台只需要关心各自的UI和平台的细节处,分工合作。
缺点:不能统一平台UI,各自平台仍然要每个平台各自写一份,但是总提上来说还是减少了一定的工作量。

点击观看官方KMM技术演进的计划-视频源自youtube

点击观看官方KMM技术演进的计划-视频源自Bilibili

五、Compose-jb工程

在这里插入图片描述

我们从上面可以看到创建单平台的项目,目前可以选择DesktopWeb

1、单平台Desktop目录介绍

创建一个单平台Desktop项目,项目目录如下:
在这里插入图片描述

jvmMain目录下面编写我们的窗口代码,build.gradle.kts文件中,我们需要在sourceSetsjvmMain中添加我们的三方库依赖:

    sourceSets {
        val jvmMain by getting {
            dependencies {
                // 增加其他依赖库
                implementation(compose.desktop.currentOs)
            }
        }
        //.....
    }

build.gradle.kts文件中最后,这段代码,里面的内容有啥含义呢?


// 包名
group = "com.example"
// 应用程序的版本名称
version = "0.1-SNAPSHOT"

compose.desktop {
    application {
        // 程序入口类
        mainClass = "MainKt"
        nativeDistributions {
            // 目标格式
            targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
            // 应用程序的名称
            packageName = "demo"
            // 安装包版本名称
            packageVersion = "1.0.0"

            // 下面是我用来介绍元数据增加的,仅供参考
            // 应用程序描述(默认值:无)
            description = "Compose Example App"
            // 应用程序的版权(默认值:无)
            copyright = "© 2023 My Name. All rights reserved."
            // 应用程序的供应商(默认值:无)
            vendor = "Example vendor"
            // 应用程序的许可证(默认值:无)
            licenseFile.set(project.file("LICENSE.txt"))
        }
    }
}

更多使用细节,可参考github这里的README.md文档

打开gradle.properties文件,可修改kotlin、agp、compose版本号:

在这里插入图片描述

打开settings.gradle.kts文件,可修改配置maven仓库镜像地址,以及插件版本号

在这里插入图片描述


这里注意一点,如果你不看参数注释直接去修改 Window 窗口透明的话:

// 错误的用法
fun main() = application {
    Window(onCloseRequest = ::exitApplication, transparent = true) {
        App()
    }
}

运行的时候,会提示如下错误:

Exception in thread "main" java.lang.IllegalStateException: Transparent window should be undecorated!

............

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':run'.
> Process 'command '************\bin\java.exe'' finished with non-zero exit value 1

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.

* Get more help at https://help.gradle.org

修改后的窗口透明代码如下:

fun main() = application {
    // undecorated - 禁用或启用此窗口的装饰。
    // transparent - 禁用或启用窗口透明度。只有在窗口【没有启用装饰】时才应设置透明度,否则将抛出异常。
    Window(onCloseRequest = ::exitApplication, undecorated = true, transparent = true) {
        App()
    }
}

运行起来之后,整个窗口背景色都是透明的,但窗口实际占的位置还是原来那么大(占位的部分,不能点击穿透),我用红色方框画了窗口的实际大小,如下:
在这里插入图片描述

2、单平台Web目录介绍

在这里插入图片描述

先看一下index.html文件的内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sample</title>
</head>
<body>
<div id="root"></div>
<script src="替换成你的module名称.js"></script>
</body>
</html>

很普通,body最后一行,需要引入以ModuleName为名称的js文件,还定义了一个div root用来插入Compose管理的DOM树内容。

我们再打开Main.kt文件,在main()方法里面,入口是这样使用的:

renderComposable(rootElementId = "root") {
    // 内容在这里
}

Compose需要一个根节点,用来管理自己的DOM树:

fun renderComposable(
    rootElementId: String,
    content: @Composable DOMScope<Element>.() -> Unit
): Composition = renderComposable(
    root = document.getElementById(rootElementId)!!,
    content = content
)

这里的root,是通过document.getElementById(rootElementId)获取的,这个方法的作用是:

返回一个Element对象,用于快速访问:id为root的div元素。

通过Compose DOM DSL给我们提供的常用的HTML可组合项,我们可以在renderComposable里面使用它们,
TextSpanDivInputATextArea 等等可组合项。

查看可组合项和HTML标签代码的异同:

Span(
    attrs = { style { color(Color.red) } } // inline style
) {
    Text("Red text")
}

对应的HTML代码:

<span style="color: red;">Red text</span>

可运行下面示例查看简单的界面:

import androidx.compose.runtime.*
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable

fun main() {
    var count: Int by mutableStateOf(0)

    renderComposable(rootElementId = "root") {
        val text = remember { mutableStateOf("") }
        TextArea(
            value = text.value,
            attrs = {
                onInput {
                    text.value = it.value
                }
            }
        )
        Div({ style { padding(25.px) } }) {
            Button(attrs = {
                onClick { count -= 1 }
            }) {
                Text("-")
            }

            Span({ style { padding(15.px) } }) {
                Text("$count")
            }

            Button(attrs = {
                onClick { count += 1 }
            }) {
                Text("+")
            }
        }

        A(
            attrs = {
                href("https://www.baidu.com")
                target(ATarget.Blank)
                hreflang("en")
                download("https://...")
            }
        ) { Text("打开百度的超链接") }
    }
}


那么如何运行Web项目到浏览器中呢?

我们可通过命令,或者通过工具栏菜单去运行:

./gradlew jsBrowserRun

如果不想每次变更内容都去重新编译和运行,可通过如下命令连续编译模式:

./gradlew jsBrowserRun --continuous

或者通过IDEA的工具栏去双击运行它:

在这里插入图片描述

运行之后,浏览器将打开:localhost:8080 界面如下:

点击查看Compose-Web详细使用的文档指南

3、多平台目录介绍

在这里插入图片描述

切换到多平台去创建一个工程,创建完工程,我们看一下目录图片(可保存到电脑上查看长图):

我们发现这个多平台的目录,只有AndroidDesktop平台,那么接着往下看吧:

androiddesktop,把可组合项代码放到了commonMain目录下面,意味着AndroidDesktop 能共用可组合项代码了,一模一样肯定是不能够的,我们要根据platform区分,因为电脑桌面的UI和手机UI排列和样式这些还是会不同的。

1、运行桌面应用

./gradlew :desktop:run

2、构建桌面应用发布包(JDK>=15)

./gradlew :desktop:packageDistributionForCurrentOS

或者可以通过菜单栏去双击构建运行:

3、运行Android应用

点击IDE的Edit Configurations,去配置一个Android app

然后在工具栏上面,菜单栏下面,有如下入口,可点击按钮运行:

4、打包Android APP

命令打包:

./gradlew build release

或者通过菜单项,手动点击去打包,Build -> Generate Signed Bundle/Apk

4、小结

Compose Multiplatform 加速了桌面和Web应用程序的UI开发,创建多平台的项目工程的时候,Android和桌面之间大部分的UI代码可直接共享。

目前看还有很长的路要走,期待:IOS工程能像Android和Desktop一样共享大部分UI代码。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Compose返回的图片格式可以是Painter类型的对象。在使用Compose展示图片时,可以使用Image函数,其中一个函数的第一个入参是Painter类型的对象。可以通过使用rememberImagePainter函数来创建Painter对象,该函数接受一个图片的URL作为参数。例如: ``` Image(rememberImagePainter(url), "") ``` 此外,还可以使用Coil库来加载网络图片或其他本地路径下的文件。Coil提供了rememberAsyncImagePainter函数来创建异步加载图片的Painter对象。可以通过指定图片的URL来加载网络图片。例如: ``` Image( painter = rememberAsyncImagePainter("https://picsum.photos/300/300"), contentDescription = null ) ``` 总之,Compose返回的图片格式是Painter类型的对象,可以通过不同的函数来创建和加载不同类型的图片。 #### 引用[.reference_title] - *1* [Compose-jb图片加载库load-the-image,适用于KMM Compose desktop桌面端(Windows,Linux,MacOs)](https://blog.csdn.net/qq_33505109/article/details/125194044)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Jetpack Compose 中的基础组件](https://blog.csdn.net/lyabc123456/article/details/131059925)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Halifax ‎

交个朋友

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值