赞
踩
HarmonyOS NEXT 不再支持 AOSP,仅支持鸿蒙内核和鸿蒙系统的应用,各大 App 也纷纷投入到了原生鸿蒙应用的开发中。在此之前,主要的客户端平台为 Android 和 iOS,现在鸿蒙的加入已经改变了这个局面,开发者需要考虑的平台已经从原来的双端演变为三端。这无疑将增加研发的复杂性和成本,由此可以预见的是未来对于跨端代码复用的诉求将越发强烈。本文将介绍 KMP 在鸿蒙上的接入,并探索 Compose 在鸿蒙上应用的可能性。
对于 Android 开发者来说最熟悉的技术栈莫过于 Kotlin, 如果可以基于 Kotlin 实现跨端开发,那么可以很大程度的降低学习成本并复用已有知识。Kotlin 本身是支持跨平台开发的,也就是 Kotlin Multiplatform(简称 KMP),本节将简单介绍 KMP 并探索在鸿蒙上的接入。
Kotlin Multiplatform 是 Kotlin 推出的跨平台开发方案,官方对它的介绍如下:
The Kotlin Multiplatform technology is designed to simplify the development of cross-platform projects. It reduces time spent writing and maintaining the same code for different platforms while retaining the flexibility and benefits of native programming.
从中可以看出 KMP 的主要优势在于跨平台复用代码的同时,可以保留 Native 开发的性能体验与灵活性,而之所以能够做到这一点主要依赖于 KMP 的实现原理。
目前大部分的跨端方案都是自建一套运行环境(虚拟机)并通过跨语言调用实现与 Native 的交互,如 JVM、Flutter 等。而 KMP 则是直接将 Kotlin 代码编译为目标平台的可执行代码,例如在 Android 平台上编译为 JVM 字节码、Web 上编译为 JS 等,目前 KMP 已经支持的平台如下图所示
得益于这种实现方式,基于 KMP 的代码复用粒度可以控制在非常小的范围,且与 Native 代码的交互也没有额外的开销,这使得我们可以渐进式的在现有项目中复用跨平台代码。
虽然鸿蒙的主要开发语言是 ArkTS ,但同时也支持使用 TS 和 JS 代码进行开发,并且 ArkTS 最终也会被编译为 TS 代码运行,所以从理论上讲我们可以基于 Kotlin/JS 在鸿蒙上实现 Kotlin 代码开发与复用。本节将借助一个简单的 Logger 工具类展示如何在鸿蒙上使用 KMP,Logger 功能定义如下:
首先创建一个 KMP 项目,由于在鸿蒙上我们是基于 Kotlin/JS 进行开发所以在项目中增加 JS target,整体项目结构如图所示。 其中 commonMain 中存放多平台复用的代码,jsMain 中存放鸿蒙独有的代码。
然后在 build.gradle.kts
中配置 JS target
由于 Looger 是支持多平台的工具类,所以首先我们在 commonMain 中定义对外提供的 Looger 类,该类对外提供了三个方法,功能分别为
enable
:打开 Logdisable
:关闭 Loglog
:打印 Log 而各平台输出 Log 的方式都不一样,所以这部分逻辑需要各平台单独实现,这里我们定义一个类 PlatformLogger
来表示各平台实现的 Logger 能力,并通过 expect
关键字来要求各平台单独提供实现。@OptIn(ExperimentalJsExport::class) @JsExport object Logger { private var enable = false private val realLogger = PlatformLogger() fun enable() { this.enable = true } fun disable() { this.enable = false } fun log(tag: String, msg: String) { if (enable) { realLogger.log(tag, msg) } } } expect class PlatformLogger { constructor() fun log(tag: String, msg: String) }
接下来就是实现鸿蒙上的 Looger 能力,鸿蒙上是通过 HiLog
打印日志的,所以第一步是定义 HiLog
声明以便在 Kotlin 代码中使用 HiLog
@JsModule("@ohos.hilog")
external class HiLog {
companion object {
fun debug(domain: Number, tag: String, format: String, args: Array<Any>)
}
}
然后我们实现鸿蒙上的 PlatformLogger
,通过 actual
关键字来声明 JS target 下 PlatformLogger
类的实现,而内部只是简单的调用了 HiLog 来打印日志
actual class PlatformLogger {
actual fun log(tag: String, msg: String) {
HiLog.debug(0, tag, msg, emptyArray())
}
}
通过执行 compileDevelopmentExecutableKotlinJs
任务可以将 Kotlin 代码编译为对应的 JS 代码,命令如下
./gradlew compileDevelopmentExecutableKotlinJs
编译的产物在 build/compileSync/js/main/developmentExecutable/kotlin
目录下
可以看到生成了 TS 类型声明文件 Kmp_Harmony.d.ts
,这个文件声明了对外提供的接口类型,也就是我们的 Logger 以及它的三个方法
但是对应的 js 代码是以 .mjs
作为后缀的,鸿蒙无法识别这个后缀,所以我们通过下面两步对这些 mjs 文件进行修改
.mjs
文件重命名为 .js
,将 .mjs.map
文件重命名为 .js.map
为了简化这个流程,我开发了一个 Gradle 插件自动对产物进行处理,接入方式如下所示
buildscript {
dependencies {
classpath "io.github.XDMrWu:harmony-plugin:1.0.0"
}
repositories {
mavenCentral()
google()
gradlePluginPortal()
}
}
plugins {
id("io.github.XDMrWu.harmony.js") // 引入插件
}
引入插件后会新增一个任务 compileDevelopmentExecutableHarmonyKotlinJs
,我们执行这个任务后即可在 build/harmony-js
目录下得到处理后的 js 代码
将上一步生成的 harmony-js
目录 copy 到鸿蒙项目中即可使用 Logger
,我们简单写一个界面来测试 Logger
的能力。
import { Logger } from '../harmony-js/Kmp_Harmony'; import promptAction from '@ohos.promptAction'; @Entry @Component struct Index { build() { Stack() { Column() { Row() { Button("Enable Log") .onClick(_ => { Logger.getInstance().enable() promptAction.showToast({ message: "Enable Log", duration: 200 }) }) Button("Disable Log") .onClick(_ => { Logger.getInstance().disable() promptAction.showToast({ message: "Disable Log", duration: 200 }) }) } Button("Print Log") .onClick(_ => { Logger.getInstance().log("HarmonyLogger", "Hello Kotlin Multiplatform") }) }、 } } }
对于客户端跨平台开发来说,仅支持逻辑代码复用还不足以满足需求,我们仍需要在 UI 层支持跨平台复用能力。Compose Multiplatform 是 Jetbrains 基于 KMP 和 Jetpack Compose 推出的跨平台响应式 UI 开发框架,支持 Android、iOS、Web 和 Desktop 等平台。它底层基于 Skia 实现跨平台 UI 渲染,而鸿蒙底层同样基于 Skia,所以理论上 Compose Multiplatform 是可以经过改造来支持鸿蒙系统的。
但受限于个人水平,改造 Compose Multiplatform 这条路暂未走通,那么是否还有其他方案可以实现呢?反观市面上的跨平台 UI 框架,从原理上可以分为自渲染和 Native UI 两类。Compose Multiplatform 属于前者也就是自渲染方案,如果自渲染的方式暂时无法实现,那么我们是否可以尝试一下 Compose + Native UI 的方式呢?
在 Compose 的架构设计中,只有上层的 UI 部分涉及到渲染、布局等逻辑,而 Compose Runtime 则与 UI 完全解耦,仅关注内部的状态管理等。得益于这种设计,我们完全可以基于 Compose Runtime 来定制上层的 UI 框架,实现 Compose 与 原生 UI 的结合。而 Redwood
就是这样一个库,下面会详细介绍一下这个库。
Redwood 是 跨平台 UI 库,它基于 Compose Compiler
和 Compose Runtime
实现了自定义 UI 树的构建与更新,并将这颗 UI 树在各个平台上映射为平台对应的原生 UI,整体的工作机制如下图所示
名词解释
- UI Schema:用于声明 UI 控件的一个 data class,包含控件的所有属性
- Widget:Redwood Compose 实际管理的 Node 类型
- Composable 方法:一个 @Composable 方法,用于使用方调用
Redwood 在编译期会基于提供的 UI Schema
生成对应的 Widget 类和 Composable 方法,以一个按钮控件为例,我们首先需要定义它的各个属性
data class Button(
val text: String,
val onClick: (() -> Unit)? = null,
)
基于上述定义的 UI Schema,Redwood 会生成以下的 Widget 和 Composable 方法
@Composable
fun Button(
text: String,
onClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) { … }
interface Button<W : Any> : Widget<W> {
fun text(text: String)
fun onClick(onClick: (() -> Unit)?)
}
运行时调用 Button
Composable 方法将在 Compose 的 Node Tree 上生成一个 Button
Widget,各平台的 Button
Widget 内部实现会桥接到平台对应的原生 UI,包含各个属性的变更。以 Android 平台为例,Button
实现如下
class AndroidButton(private val context: Context): Button<View> {
private val innerView = android.widget.Button(context)
override fun text(text: String?) {
innerView.text= text
}
override fun onClick(onClick: (() -> Unit)?) {
innerView.setOnClickListener {
onClick?.invoke()
}
}
}
通过上述对 Redwood 的介绍可以发现,我们只需要实现一套鸿蒙平台的 Widget
即可在鸿蒙上使用 Redwood。但 ArkUI 作为一套声明式 UI,本身并不存在类似 DOM 的 UI 节点,我们无法直接将 Widget
桥接到 ArkUI 上。所以我们首先需要定义一套鸿蒙的 DOM,并实现 DOM 到 ArkUI 的转换能力,如下图所示
HarmonyDom 已经在 Github 开源,基于 HarmonyDom 可以将 ArkUI 从响应式 UI 变为命令式 UI 项目地址:github.com/Compose-for…
我们采用类似 Android View 的设计,首先定义两个基础 DOM 类 BaseNode
和 BaseNodeGroup
,用于表示没有子节点的 UI 节点和带子节点的 UI 节点
export class BaseNode {...}
export class BaseNodeGroup extends BaseNode {...}
还是以一个按钮控件为例,在鸿蒙上定义为一个 ButtonNode
export class ButtonNode extends BaseNode {
text: string = ""
clickBlock?: () => void
setText(text: string): void {
this.text = text
}
onClick(onClick: () => void) {
this.clickBlock = onClick
}
}
为了将 ButtonNode
映射为一个 Component,我们实现一个 ButtonBridgeView
,该 Component 接收一个 ButtonNode
并将它的各个属性实现为 UI 属性
@Component
export struct ButtonBridgeView {
@ObjectLink buttonNode: ButtonNode
build() {
Button(this.buttonNode.text)
.onClick(_ => {
if (this.buttonNode.clickBlock) {
this.buttonNode.clickBlock()
}
})
}
}
为了在 ButtonNode
的属性变更时 ButtonBridgeView
可以及时响应,我们通过 @ObjectLink
的方式来接收 ButtonNode,并将 ButtonNode
通过 @Observed
装饰。需要注意的是,所有的方法调用都需要反应到属性变更上,否则无法更新 UI。
@Observed
export class ButtonNode extends BaseNode {...}
这时候我们还缺少一个将 ButtonNode
与 ButtonBridgeView
关联起来的地方,需要定义一个总的 Bridge 方法 createUIFromNode
用于为每个 Node 创建对应的 BridgeView,这样我们就可以通过调用 createNode
来创建 ArkUI,新增 Node 只需要在 createUIFromNode
方法中补充条件分支即可。
@Builder
export function createUIFromNode(node: BaseNode) {
if (node instanceof ButtonNode) {
ButtonBridgeView({buttonNode: node})
} else if (node instanceof XXXNode) {
XXXBridgeView({xxxNode: node})
} else if ....
}
完成 HarmonyDom 设计后,为了能够在 KMP 项目中使用,我们需要提供一份 HarmonyDom
的 Kotlin 代码声明
@file:JsModule(DOM_PACKAGE) package harmony.dom public open external class BaseNode { public var parentNode: BaseNodeGroup? public fun setLayoutWeight(weight: Number) public fun setWidthString(width: String) public fun setWidth(width: Number) public fun setHeightString(height: String) public fun setHeight(height: Number) public fun setPadding(left: Number?, top: Number?, right: Number?, bottom: Number?) } public open external class BaseNodeGroup: BaseNode { public fun insert(index: Number, node: BaseNode) public fun move(fromIndex: Number, toIndex: Number, count: Number) public fun remove(index: Number, count: Number) public fun clear() } public open external class ButtonNode: BaseNode { public var text: String public fun onClick(onClick: (() -> Unit)?) }
还是以 Button 为例子,我们基于 HarmonyDom
实现鸿蒙平台上的 Button 控件
import harmony.dom.BaseNode import harmony.dom.ButtonNode public class HarmonyButton: Button<BaseNode> { private val innerNode = ButtonNode() override fun text(text: String?) { innerNode.text = text ?: "" } override fun onClick(onClick: (() -> Unit)?) { innerNode.onClick { onClick?.invoke() } } }
其他的控件实现方式和 Button 类似,完成所有控件的定义与各平台实现后就基本上可以实现基于 Redwood 的跨平台 UI 开发。这部分代码已经在 Github 开源,目前支持 Android 和鸿蒙两个平台。
为了演示 compose-ez-ui 的效果,本节将会基于该库实现一个仿微博列表的组件,并在鸿蒙和 Android 平台上运行,具体的代码在项目
samples/weibo
目录下可以找到。
首先我们基于现有的控件实现 WeiboCard
,用于展示一条微博的样式
@Composable fun WeiboCard(weiboModel: WeiboModel) { Column { Row(padding = Padding(10.dp, 10.dp, 10.dp, 10.dp)) { Image(weiboModel.avatar_url, Length(40.dp), Length(40.dp), circle = true) Column(padding = Padding(10.dp), modifier = Modifier.weight(1f)) { Text(weiboModel.user_name, fontSize = 16.dp, fontColor = Color.Orange) Row { Text(weiboModel.created_at, fontSize = 10.dp, fontColor = Color.Grey) Text(" | ", fontSize = 10.dp, fontColor = Color.Grey) Text(weiboModel.source, fontSize = 10.dp, fontColor = Color.Grey) } } } Text(weiboModel.text, maxLines = 5, padding = Padding(start = 10.dp, end = 10.dp), spans = weiboModel.createTextSpans()) // TODO Grid Image(weiboModel.pics.split(",").first(), Length(200.dp), Length(200.dp), padding = Padding(10.dp, 10.dp, 10.dp, 10.dp)) Row(width = Length.Fill, padding = Padding(bottom = 5.dp)) { Text("转发 ${weiboModel.reposts_count}", fontSize = 15.dp, fontColor = Color.Grey, textCenter = true, modifier = Modifier.weight(1f)) Text("评论 ${weiboModel.comments_count}", fontSize = 15.dp, fontColor = Color.Grey, textCenter = true, modifier = Modifier.weight(1f)) Text("点赞 ${weiboModel.attitudes_count}", fontSize = 15.dp, fontColor = Color.Grey, textCenter = true, modifier = Modifier.weight(1f)) } Divider(Length.Fill, Length(5.dp), Color.LightGrey) } }
接下来实现一个 WeiboVM
负责数据的获取,这里我们手动延迟 1 秒模拟请求耗时
class WeiboVM { var isLoading by mutableStateOf(true) var weiboList = mutableStateListOf<WeiboModel>() private val json = Json { ignoreUnknownKeys = true } suspend fun fetchWeiboList() { isLoading = true delay(1000) val response = json.decodeFromString<WeiboResponse>(WeiboRepo.fakeData) response.weibo.forEach { it.user_name = response.user.screen_name it.avatar_url = response.user.profile_image_url } weiboList.addAll(response.weibo) isLoading = false } }
最后实现 WebiList
,基于 WebiVM
和 WeiboCard
展示 Loading UI 与微博列表
@Composable fun WeiboList() { val vm = remember { WeiboVM() } LaunchedEffect(Unit) { vm.fetchWeiboList() } if (vm.isLoading) { Text("Loading", width = Length.Fill, height = Length.Fill, textCenter = true) } else if (vm.weiboList.isEmpty()) { Text("Empty", width = Length.Fill, height = Length.Fill, textCenter = true) } else { com.compose.ez.ui.compose.List { vm.weiboList.forEach { WeiboCard(it) } } }
最后我们将 WeiboList
运行在 Android 和鸿蒙平台上就行了。
首先必学的是开发语言 ArkTS,这是重中之重,然后就是ArkUI声明式UI开发、Stage模型、网络/数据库管理、分布式应用开发、进程间通信与线程间通信技术、OpenHarmony多媒体技术……。中间还有许多的知识点,都整理成思维导图来分享给大家~
此外,小编精心准备了一份联合鸿蒙官方发布笔记整理收纳的《鸿蒙开发学习笔记》,内容包含ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。
有需要的小伙伴,可以扫描下方二维码免费领取!!!】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。