当前位置:   article > 正文

鸿蒙 KMP & Compose 探索_compose redwood

compose redwood

前言

HarmonyOS NEXT 不再支持 AOSP,仅支持鸿蒙内核和鸿蒙系统的应用,各大 App 也纷纷投入到了原生鸿蒙应用的开发中。在此之前,主要的客户端平台为 Android 和 iOS,现在鸿蒙的加入已经改变了这个局面,开发者需要考虑的平台已经从原来的双端演变为三端。这无疑将增加研发的复杂性和成本,由此可以预见的是未来对于跨端代码复用的诉求将越发强烈。本文将介绍 KMP 在鸿蒙上的接入,并探索 Compose 在鸿蒙上应用的可能性。

KMP 初探

对于 Android 开发者来说最熟悉的技术栈莫过于 Kotlin, 如果可以基于 Kotlin 实现跨端开发,那么可以很大程度的降低学习成本并复用已有知识。Kotlin 本身是支持跨平台开发的,也就是 Kotlin Multiplatform(简称 KMP),本节将简单介绍 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 代码的交互也没有额外的开销,这使得我们可以渐进式的在现有项目中复用跨平台代码。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

KMP 在鸿蒙上接入

虽然鸿蒙的主要开发语言是 ArkTS ,但同时也支持使用 TS 和 JS 代码进行开发,并且 ArkTS 最终也会被编译为 TS 代码运行,所以从理论上讲我们可以基于 Kotlin/JS 在鸿蒙上实现 Kotlin 代码开发与复用。本节将借助一个简单的 Logger 工具类展示如何在鸿蒙上使用 KMP,Logger 功能定义如下:

  • 提供一个用于打印日志的接口
  • 提供一个开关用来控制是否输出日志
  • 支持多平台,包括 Android、iOS、鸿蒙等

创建 KMP 项目

首先创建一个 KMP 项目,由于在鸿蒙上我们是基于 Kotlin/JS 进行开发所以在项目中增加 JS target,整体项目结构如图所示。 其中 commonMain 中存放多平台复用的代码,jsMain 中存放鸿蒙独有的代码。

在这里插入图片描述

然后在 build.gradle.kts 中配置 JS target

在这里插入图片描述

编写 Kotlin 代码

由于 Looger 是支持多平台的工具类,所以首先我们在 commonMain 中定义对外提供的 Looger 类,该类对外提供了三个方法,功能分别为

  • enable:打开 Log
  • disable:关闭 Log
  • log:打印 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)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

接下来就是实现鸿蒙上的 Looger 能力,鸿蒙上是通过 HiLog 打印日志的,所以第一步是定义 HiLog 声明以便在 Kotlin 代码中使用 HiLog

@JsModule("@ohos.hilog")
external class HiLog {
    companion object {
        fun debug(domain: Number, tag: String, format: String, args: Array<Any>)
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然后我们实现鸿蒙上的 PlatformLogger,通过 actual 关键字来声明 JS target 下 PlatformLogger 类的实现,而内部只是简单的调用了 HiLog 来打印日志

actual class PlatformLogger {
    actual fun log(tag: String, msg: String) {
        HiLog.debug(0, tag, msg, emptyArray())
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

编译成 JS 代码

通过执行 compileDevelopmentExecutableKotlinJs 任务可以将 Kotlin 代码编译为对应的 JS 代码,命令如下

./gradlew compileDevelopmentExecutableKotlinJs

  • 1
  • 2

编译的产物在 build/compileSync/js/main/developmentExecutable/kotlin 目录下

在这里插入图片描述

可以看到生成了 TS 类型声明文件 Kmp_Harmony.d.ts,这个文件声明了对外提供的接口类型,也就是我们的 Logger 以及它的三个方法

在这里插入图片描述

但是对应的 js 代码是以 .mjs 作为后缀的,鸿蒙无法识别这个后缀,所以我们通过下面两步对这些 mjs 文件进行修改

  • .mjs 文件重命名为 .js,将 .mjs.map 文件重命名为 .js.map
  • 将 js 代码中 import 语句包含的 mjs 路径替换为对应的 js 路径

为了简化这个流程,我开发了一个 Gradle 插件自动对产物进行处理,接入方式如下所示

buildscript {
  dependencies {
    classpath "io.github.XDMrWu:harmony-plugin:1.0.0"
  }
  repositories {
    mavenCentral()
    google()
    gradlePluginPortal()
  }
}

plugins {
  id("io.github.XDMrWu.harmony.js") // 引入插件
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

引入插件后会新增一个任务 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")
          })
      }}
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

Compose 适配

方案选型

对于客户端跨平台开发来说,仅支持逻辑代码复用还不足以满足需求,我们仍需要在 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 介绍

Redwood 是 跨平台 UI 库,它基于 Compose CompilerCompose 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,
)

  • 1
  • 2
  • 3
  • 4
  • 5

基于上述定义的 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)?)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

运行时调用 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()
    }
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Redwood 适配

思路

通过上述对 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 类 BaseNodeBaseNodeGroup,用于表示没有子节点的 UI 节点和带子节点的 UI 节点

export class BaseNode {...}
export class BaseNodeGroup extends BaseNode {...}

  • 1
  • 2
  • 3

还是以一个按钮控件为例,在鸿蒙上定义为一个 ButtonNode

export class ButtonNode extends BaseNode {
  text: string = ""
  clickBlock?: () => void

  setText(text: string): void {
    this.text = text
  }

  onClick(onClick: () => void) {
    this.clickBlock = onClick
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

为了将 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()
        }
      })
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

为了在 ButtonNode 的属性变更时 ButtonBridgeView 可以及时响应,我们通过 @ObjectLink 的方式来接收 ButtonNode,并将 ButtonNode 通过 @Observed 装饰。需要注意的是,所有的方法调用都需要反应到属性变更上,否则无法更新 UI。

@Observed
export class ButtonNode extends BaseNode {...}

  • 1
  • 2
  • 3

这时候我们还缺少一个将 ButtonNodeButtonBridgeView 关联起来的地方,需要定义一个总的 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 ....
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Redwood Schema 实现

完成 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)?)
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

还是以 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()
    }
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

其他的控件实现方式和 Button 类似,完成所有控件的定义与各平台实现后就基本上可以实现基于 Redwood 的跨平台 UI 开发。这部分代码已经在 Github 开源,目前支持 Android 和鸿蒙两个平台。

Demo 演示

为了演示 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)
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

接下来实现一个 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
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

最后实现 WebiList ,基于 WebiVMWeiboCard 展示 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)
      }
    }
  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

最后我们将 WeiboList 运行在 Android 和鸿蒙平台上就行了。

怎样学习鸿蒙?

首先必学的是开发语言 ArkTS,这是重中之重,然后就是ArkUI声明式UI开发、Stage模型、网络/数据库管理、分布式应用开发、进程间通信与线程间通信技术、OpenHarmony多媒体技术……。中间还有许多的知识点,都整理成思维导图来分享给大家~
在这里插入图片描述
此外,小编精心准备了一份联合鸿蒙官方发布笔记整理收纳的《鸿蒙开发学习笔记》,内容包含ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

有需要的小伙伴,可以扫描下方二维码免费领取!!!】

快速入门

  • 开发准备
  • 构建第一个ArkTS应用(Stage模型)
  • 构建第一个ArkTS应用(FA模型)
  • 构建第一个JS应用(FA模型)
    在这里插入图片描述

开发基础知识

  • 应用程序包基础知识
  • 应用配置文件(Stage模型)
  • 应用配置文件概述(FA模型)
    在这里插入图片描述

资源分类与访问

  • 资源分类与访问
  • 创建资源目录和资源文件
  • 资源访问
    在这里插入图片描述

学习ArkTs语言

  • 初识ArkTS语言
  • 基本语法
  • 状态管理
  • 其他状态管理
  • 渲染控制
    在这里插入图片描述

基于ArkTS声明式开发范式

  • UI开发(ArkTS声明式开发范式)概述
  • 开发布局
  • 添加组件
  • 显示图片
  • 使用动画
  • 支持交互事件
  • 性能提升的推荐方法

在这里插入图片描述

兼容JS的类Web开发范式

  • 概述
  • 框架说明
  • 构建用户界面
  • 常见组件开发指导
  • 动效开发指导
  • 自定义组件
    在这里插入图片描述

Web组件

  • 概述
  • 设置基本属性和事件
  • 并发
  • 窗口管理
  • WebGL
  • 媒体
  • 安全
  • 网络与连接
  • 电话服务
  • 数据管理

  • 在这里插入图片描述

应用模型

  • 概述
  • Stage模型开发指导
  • FA模型开发指导
    在这里插入图片描述
2024完整鸿蒙学习资料领取方式:扫描下方二维码即可
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/624065
推荐阅读
相关标签
  

闽ICP备14008679号