一、前言

最近刚从鸿蒙体系产品研制中抽开身闲下来,想起来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,然后装置

快速入门KMM和Compose Multiplatform

4、更新Kotlin插件

Kotlin插件与AndroidStudio版别是捆绑在一起的,咱们需求更新Kotlin到最新版别,来避免兼容性问题。

快速入门KMM和Compose Multiplatform

三、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

快速入门KMM和Compose Multiplatform

装备使用程序的称号、使用包名、项目的位置、最小SDK版别,装备完成之后,点击 Next

快速入门KMM和Compose Multiplatform

iOS framework distribution 咱们选择Regular framework, 由于此选项不需求第三方工具,而且装置问题较少。

cocoapods dependency manager 是什么呢?

CocoaPods是 Swift 和 Objective-C Cocoa项目的依靠办理器。

对于更杂乱的项目,或许需求CocoaPods依靠项办理器来协助处理库依靠项。

快速入门KMM和Compose Multiplatform

点击Finish,首次执行此操作时,下载和设置所需的组件或许需求一些时刻。

2、工介绍

快速入门KMM和Compose Multiplatform

KMM工程包括三个模块:

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

  • shared 包括Android和iOS使用程序在渠道之间同享的通用代码逻辑

  • iosApp 是iOS使用程序项目,它依靠于并运用shared模块作为iOS结构

快速入门KMM和Compose Multiplatform

androidApp和iosApp模块都是各自渠道本来的开发方式,shared模块它是渠道之间同享的通用代码逻辑,那么它怎么完成同享的呢?咱们看一下下面这张图片:

快速入门KMM和Compose Multiplatform

连接到各自的渠道:

expect和actual文档

咱们能够看到它在公共模块中运用expect关键字,expect润饰类、成员变量或办法时,表明类、成员变量、办法,能够跨渠道完成。

留意:expect声明不包括任何完成代码。 expect和actual所润饰的类/变量/办法,称号都需求完全一样,而且位于同一包中(具有相同的完整包名)。

在各自的渠道Android/IOS,中运用actual润饰,完成同名的类、办法、成员变量。

咱们再来看Hello World示例,commonMain目录下面创立了一个Platform接口,并运用expect关键字润饰了getPlatform()办法

快速入门KMM和Compose Multiplatform

那么Android/IOS渠道,需求去运用actual润饰,完成同名的类、办法、成员变量。

快速入门KMM和Compose Multiplatform

咱们在commonMain目录下面,能够界说逻辑类,同享通用的代码逻辑,相同以Hello World为例:

快速入门KMM和Compose Multiplatform

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

快速入门KMM和Compose Multiplatform


快速入门KMM和Compose Multiplatform

3、怎么添加依靠项

切换到Project视图下,找到shared模块,点击build.gradle.kts,在sourceSets里边,咱们能够给 commonMain、androidMain、iosMain 分别添加依靠。

咱们这儿给commonMain添加依靠项,这样Android、IOS渠道就能够在获取通用代码逻辑的时分,运用到这个依靠项的才能了。

快速入门KMM和Compose Multiplatform

添加kotlinx-datetime的依靠项:

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

快速入门KMM和Compose Multiplatform

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里边运用这个办法:

快速入门KMM和Compose Multiplatform

咱们看看运转之后的成果:

快速入门KMM和Compose Multiplatform

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

快速入门KMM和Compose Multiplatform

运用 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块中添加 序列化插件 内容:

快速入门KMM和Compose Multiplatform

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

快速入门KMM和Compose Multiplatform

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模块的挂起办法:

快速入门KMM和Compose Multiplatform

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还差点姿态,样貌算是外甥的那种哈哈。

快速入门KMM和Compose Multiplatform

点击检查DispatchQueue.main.async解说

运转之后AndroidIOS的界面显现如下:

快速入门KMM和Compose Multiplatform

5、小结

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

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

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

五、Compose-jb工程

快速入门KMM和Compose Multiplatform

咱们从上面能够看到创立单渠道的项目,现在能够选择DesktopWeb

1、单渠道Desktop目录介绍

创立一个单渠道Desktop项目,项目目录如下:

快速入门KMM和Compose Multiplatform

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版别号:

快速入门KMM和Compose Multiplatform

翻开settings.gradle.kts文件,可修正装备maven库房镜像地址,以及插件版别号

快速入门KMM和Compose Multiplatform

快速入门KMM和Compose Multiplatform

这儿留意一点,假如你不看参数注释直接去修正 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()
    }
}

运转起来之后,整个窗口背景色都是通明的,但窗口实践占的位置仍是本来那么大(占位的部分,不能点击穿透),我用红色方框画了窗口的实践大小,如下:

快速入门KMM和Compose Multiplatform

2、单渠道Web目录介绍

快速入门KMM和Compose Multiplatform

先看一下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的工具栏去双击运转它:

快速入门KMM和Compose Multiplatform

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

快速入门KMM和Compose Multiplatform

点击检查Compose-Web详细运用的文档攻略

3、多渠道目录介绍

快速入门KMM和Compose Multiplatform

切换到多渠道去创立一个工程,创立完工程,咱们看一下目录图片(可保存到电脑上检查长图):

快速入门KMM和Compose Multiplatform

咱们发现这个多渠道的目录,只要AndroidDesktop渠道,那么接着往下看吧:

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

1、运转桌面使用

./gradlew :desktop:run

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

./gradlew :desktop:packageDistributionForCurrentOS

或许能够经过菜单栏去双击构建运转:

快速入门KMM和Compose Multiplatform

3、运转Android使用

点击IDE的Edit Configurations,去装备一个Android app

快速入门KMM和Compose Multiplatform

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

快速入门KMM和Compose Multiplatform

4、打包Android APP

指令打包:

./gradlew build release

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

快速入门KMM和Compose Multiplatform

4、小结

Compose Multiplatform 加速了桌面和Web使用程序的UI开发,创立多渠道的项目工程的时分,Android和桌面之间大部分的UI代码可直接同享。

现在看还有很长的路要走,等待:IOS工程能像Android和Desktop一样同享大部分UI代码。