开源鸿蒙平台 CMP 三方库「ui-text-compose」适配全流程
本文记录
ui-text-compose接入 OpenHarmony 的完整过程,覆盖文本蓝图类库的选型定位、Compose 层停编决策、纯 Kotlin 算法内核移植、上游占位符规则的逐条对齐、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
ui-text-compose是一个 Compose Multiplatform 文本蓝图(text blueprint) 库:它解决的是 AndroidViewModel 反模式——ViewModel 里提前解析好的本地化字符串,在用户切换语言后全部过期。库的答案是让 ViewModel 持有未解析的蓝图(raw纯文本 /res资源模板 + 参数 /pluralRes复数资源 + 数量 / 多蓝图 compound 拼接,参数本身还可以嵌套另一张蓝图),渲染时才在 Composable 语境下解析成AnnotatedString,语言一变自动重新解析。本次适配与 KStore 的"真编译整库"路线有本质区别:@Composable、AnnotatedString、SpanStyle全部长在androidx.compose里,社区鸿蒙工具链(CMP 1.9.2 / HarmonyOS Kotlin 2.2.21)编不了上游 CMP 1.8 / Kotlin 2.1.20 的源码集——因此走算法内核移植路线:上游UITextUtil的两条占位符正则、转义规则、注解栈语义被逐条移植成不依赖 Compose 的纯 KotlincommonMain,上游 Compose 层四个文件git mv到src/composeMain停编保留;一个能正确处理两套占位符规则、嵌套蓝图、样式注解与链接的文本引擎运行在真机上,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/ui-text-compose.git
上游仓库: radusalagean/ui-text-compose
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
ui-text-compose 为 Compose 应用提供"文本怎么描述、何时解析"这一件事的完整答案:UIText { raw() / res() / pluralRes() } DSL 一行构造蓝图,arg(...) 支持纯字符串与嵌套蓝图两种参数,+SpanStyle / +ParagraphStyle / +LinkAnnotation.Url 往蓝图上挂样式注解,渲染时 buildStringComposable() 出纯文本、buildAnnotatedStringComposable() 出带样式的 AnnotatedString。资源占位符有两套规则:Android String Resources 的 %1$s + %s(含 %%s 转义还原),以及 Compose Multiplatform String Resources 的仅 %1$s——两条正则与转义剥离逻辑全部在上游 UITextUtil 里。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面、Web 共享。OpenHarmony 应用同样需要这套"渲染期解析 + 语言热切换"的文本能力,但上游构建不会生成 OpenHarmony 变体。
如果只在 ArkTS 里手写一套"模板 + 参数 + 样式"拼串逻辑,页面可以显示文字,却等于把上游经过测试的算法翻译一遍——两条占位符规则的差异、%% 转义、嵌套参数的注解语义,每一处都是翻译走样的机会。适配需要同时验证算法内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| Compose 制品缺失 | AnnotatedString / SpanStyle / LinkAnnotation 在 androidx.compose.ui.text,@Composable 在 androidx.compose.runtime——社区鸿蒙栈没有能对上上游 CMP 1.8 / Kotlin 2.1.20 的制品线;强行对齐等于再打一套 Compose 发行版。 |
| 接口形状强绑定 Compose | 上游 UITextBase 的两个方法都是 @Composable,UITextAnnotation 三个 value class 直接包装 Compose 类型——不是加一个 actual 就能过的 expect/actual 形态。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 文本语义跨不了桥 | AnnotatedString 是 Compose 类型,C ABI 只能同步传数据——分段样式必须翻译成纯数据。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:占位符/注解算法的纯 Kotlin 移植与测试锁定、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。蓝图语义完全由移植自上游的算法负责,ArkTS 只负责提供已本地化模板、发起解析和渲染分段样式。
1.2 库提供的能力
Demo 提供一个真实的文本蓝图解析会话,对应桥接的全部入口:
- 四种蓝图:
raw(纯文本)、res(模板 + 参数)、plural(复数模板 + 数量,无显式参数时自动把数量注册为%1$s——上游pluralRes(resource, quantity)的默认行为)、compound(多蓝图拼接); - 两套占位符规则:
android规则替换%1$s与%s并把%%s/%%1$s还原为%s/%1$s;cmp规则只替换%1$s,%%保持字面量——同一模板走两套规则的差异在 Demo 里并排展示; - 样式注解:
span(颜色 / 背景 / 字号 / 字重 / 斜体 / 下划线 / 删除线)、paragraph(对齐 / 行高)、link(URL + 链接色 + 下划线),注解栈按字段合并、内层覆盖外层; - 嵌套参数两种语义:无自身注解的嵌套蓝图继承外层注解栈(上游
getString返回 String、withStyle重刷);带自身注解的嵌套蓝图保留自身样式不被重刷(上游 appendAnnotatedString的行为)——两种都与上游对齐; - 双输出:每次解析同时返回
plain(等价上游buildString())与分段样式数组segments(等价上游buildAnnotatedString()的 span 切片); - 版本握手:
op:"spec"返回 fork 版本、上游版本与能力清单,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 解析是否成功,失败时 error 带原文。 |
op / locale | 回显本次操作与语言(信息性字段,模板进桥前已在 ArkTS 侧本地化)。 |
plain | 纯文本结果(上游 buildString() 的等价物)。 |
segments | {text, span, link} 分段数组;相邻同样式分段自动合并,ArkTS 侧直接映射成 Span。 |
paragraph | 整段级 {align, lineHeight},无段落注解时为 null。 |
上游算法的全部语义(两条正则、编号/未编号映射、%% 转义剥离、注解栈合并、嵌套两种情形、越界抛错)由 34 项 JVM 测试锁死(21 项算法交叉验证 + 12 项桥协议端到端冒烟 + 1 项文档示例契约),证明 ohos .so 里执行的解析逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 算法语义逐条移植自上游 UITextUtil,不改行为(连 %%1$s 在 cmp 规则下拆出字面量 % 这种上游怪癖都原样保留);上游 Compose 层 4 文件 git mv 到 composeMain 停编保留,等制品对齐后接回。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);uitextcompose-multiplatform / uitextcompose-android / 两个 sample 留盘不进构建。 |
| 桥接稳定 | 1 个 C ABI 入口(op = resolve / spec)和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 移植内核零第三方依赖(连 JSON 解析都是手写递归下降),.so 里只有 Kotlin/Native 运行时。 |
| UI 完整 | 单页覆盖上游 sample 全部 9 个场景卡 + 占位符规则对照卡 + en/ro 语言切换 + quantity 切换。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。文本的"渲染"由 ArkTS
Text+Span完成,桥只负责把蓝图画成带样式的分段数据——这正是"文本引擎出数据、ArkUI 出像素"的分工。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、确认 compose.ui 无 ohos 制品、确定交付物
第 2 阶段:源码集拆分 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Compose 层 git mv 停编
第 3 阶段:算法内核移植 ── 蓝图树/注解模型/占位符扫描器纯 Kotlin 化、上游正则逐条对齐、JVM 测试锁定
第 4 阶段:桥协议建模 ── 蓝图 JSON 契约、resolve/spec 双 op、错误通道、MiniJson 递归解析器
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主 ── 九场景卡 + 规则对照卡、en/ro 与 quantity 热切换、HAP、签名、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
uitextcompose-core/ 上游核心库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
uitextcompose-core/src/commonMain/ 新增:纯 Kotlin 算法内核(5 个文件)
uitextcompose-core/src/composeMain/ 上游 Compose 层 4 文件(git mv 停编,保留历史)
uitextcompose-multiplatform/ CMP 资源版 UIText,留盘不进 settings
uitextcompose-android/ Android R 资源版 UIText,留盘不进 settings
uitextcompose-multiplatform-sample/ 上游 KMP 示例,留盘不进构建
uitextcompose-android-sample/ 上游 Android 示例,留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Dispatch + MiniJson + Bridge)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :uitextcompose-core 与 :example:nativeApp,rootProject.name = "ohos_ui_text_compose"。上游的 binary-compatibility-validator / vanniktech 发布插件 / AGP classpath / compose 插件全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin,org.gradle.java.home 钉 C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 钉版;2.2.21 线的社区标准) |
| 依赖 | 零第三方依赖——算法内核与桥都不引 kotlinx 库 |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.uitextcompose |
export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
java -version # openjdk 21
注意 Windows 上 gradlew.bat 的启动器本身要求 JAVA_HOME——gradle.properties 里的 org.gradle.java.home 只作用于 daemon,启动器层面照样报 JAVA_HOME is not set,所以命令行构建必须先 export。
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
uitextcompose-core | 5 | UITextBase / UITextBuilderBase / UITextAnnotation / UITextUtil 强依赖 compose.runtime + compose.ui.text;InternalApi 纯 Kotlin | 4 个 git mv 到 composeMain 停编;算法移植为 Ui* 纯 Kotlin 5 文件,InternalApi 原样留在 commonMain |
uitextcompose-multiplatform | 2 | CMP 资源系统(getString / rememberResourceEnvironment)+ compose.runtime | 留盘不进构建 |
uitextcompose-android | 2 | Android Context.getString + LocalConfiguration + compose | 留盘不进构建 |
| 两个 sample | — | AGP / compose app 工程 | 留盘不进构建 |
值得单独说明的是上游库没有 expect/actual——全部平台差异被 CMP 资源系统吸收,这本来是 KMP 的优雅形态,但恰恰意味着没有一个"补 actual 就能编"的缝:@Composable 与 AnnotatedString 长在方法签名和 value class 内部,停编隔离是唯一出路。
1.4 页面边界
标题区 UI-TEXT-COMPOSE · OPENHARMONY / 版本握手行(CMP 文本蓝图库 · Kotlin/Native ohosArm64/ohosX64)
语言切换 English / Română 双按钮(切换后全部蓝图即时重解析)
数量切换 1 / 2 / 5 / 30 / 35 五档(复数分类联动)
场景卡 ×9 raw / res / pluralRes / res·annotated / pluralRes·annotated / compound 1-3 / ToS&Privacy
规则对照卡 同一模板走 android / cmp 两套规则的输出并排展示
状态行 最近一次解析结果(如"已解析 · locale=ro · quantity=30")
页面启动时先发 op:"spec" 握手,随后把全部蓝图画出来。资源表镜像上游 sample 的 strings.xml(en/ro 双语),切语言只是换一张模板表重新 resolve——这正是上游"蓝图在渲染期解析、语言变了自动重刷"的鸿蒙等价物。
第 2 阶段:源码集拆分与目标打通
2.1 为什么停编而不是硬接
社区此前审核通过的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / segmented-control / combo-breaker 用的),配套 CMP 1.9.2。而上游 ui-text-compose 构建在 CMP 1.8 / Kotlin 2.1.20 / AGP 8.10——两套 Compose 发行版无法共存于一个 Gradle 构建。社区里唯一"用上 ohos Compose 制品"的先例 molecule 也只用了 compose.runtime(1.12.0-0.1.0-11 线),compose.ui.text 这一级没有任何 ohos 变体。所以上游 Compose 层 4 个文件 git mv 进 src/composeMain:不挂任何 target、不注册 sourceSet,Kotlin 编译器看不到它们;保留在树里一是对照上游 API,二是等制品对齐后接回。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
rootProject.name = "ohos_ui_text_compose"
include(":uitextcompose-core")
include(":example:nativeApp")
gradle/libs.versions.toml 收敛到只剩一行插件:
[versions]
kotlin = "2.2.21-1.0.0"
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
2.2 编译纯 Kotlin 内核
uitextcompose-core/build.gradle.kts 收敛为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
实测:
./gradlew.bat :uitextcompose-core:compileKotlinJvm :uitextcompose-core:compileKotlinOhosArm64 :uitextcompose-core:compileKotlinOhosX64 # BUILD SUCCESSFUL
第 3 阶段:算法内核移植(技术难点)
3.1 上游算法的形状
上游 UITextUtil 的全部逻辑建立在 androidx.compose.ui.text.buildAnnotatedString 之上,核心是三条正则:
| 上游正则 | 语义 |
|---|---|
(?<!%)%(\d+\$)?[s] | android 规则:编号 %1$s + 未编号 %s,负向单字符 lookbehind 拒绝 % 前缀 |
%(\d+\$)[s] | cmp 规则:仅编号形式 |
%%([0-9]+\$)?s | 转义占位符,仅 android 规则对字面分段剥掉一个 % |
外加一套注解栈语义:handleUITextAnnotations 把 SpanStyle / ParagraphStyle / LinkAnnotation 列表按嵌套 withStyle / withLink 包住文本;编号占位符映射 n - 1、未编号按出现顺序、%5$s 越界直接抛 IndexOutOfBoundsException。
3.2 移植的关键决策:手写扫描器替代 lookbehind
Kotlin/Native 对 regex lookbehind 的支持在各目标上没有保证——第一条正则的 (?<!%) 不能照抄。移植时手写了单字符 lookbehind 的扫描器 scanTemplate / matchPlaceholder:逐字符扫描,% 起头时手工解析 (\d+\$)?s,android 规则下检查前一个字符是否为 %,cmp 规则下要求必须有编号形式。接受集与上游正则完全一致,且规避了 Native 正则引擎差异。
private fun matchPlaceholder(text: String, start: Int): Pair<Int, Int?>? {
var i = start + 1
var number: Int? = null
val digitStart = i
while (i < text.length && text[i] in '0'..'9') i++
if (i > digitStart) {
if (i >= text.length || text[i] != '$') return null
number = text.substring(digitStart, i).toIntOrNull() ?: return null
i++
}
if (i < text.length && text[i] == 's') return (i + 1) to number
return null
}
扫描器输出(字面分段 + 每个占位符的 arg 下标)与上游 baseString.split(regex) + findAll 的结构一一对应。甚至上游的一个怪癖也被忠实复现:%%1$s 在 cmp 规则下,正则无 lookbehind 会匹配内部的 %1$s,输出是字面量 % + 替换结果(如 "%X")——本 fork 原样保留、不修,因为鸿蒙侧要的是与上游可对照的行为,不是"更合理"的行为。
3.3 新 commonMain 的五个文件
InternalApi.kt 上游 opt-in 注解,原样保留(唯一没动内容的文件)
UiTextAnnotation.kt Span/Paragraph/Link 纯数据注解(颜色 packed Long ARGB、字重 100..900)
UiTextValue.kt 蓝图树 Raw/Res/PluralRes/Compound + UiTextArg + PlaceholderRule 枚举
UiResolvedText.kt UiSpanSpec/UiParagraphSpec/UiSegment/UiResolvedText(解析结果模型)
UiTextResolver.kt 算法移植:扫描器 + 注解栈合并 + 嵌套分流 + 相邻同样式段合并
AnnotatedString 的语义翻译成两条纯数据规则:注解栈按字段合并、内层覆盖外层(mergeSpan);嵌套参数按"解析结果的分段是否全无样式"分流——全无样式等价上游的纯 String(getString 返回值),外层 withStyle/withLink 会重刷;带样式等价上游的 AnnotatedString,append 时保留自身样式不被重刷。
3.4 JVM 冒烟抓出的三个保真度 bug
算法移植最容易翻车的地方不是编译,而是语义走样。本次把桥的协议逻辑放进 example/nativeApp 的 commonMain(@CName 只是 ohosMain 里的薄包装),给 nativeApp 加 jvm() 目标跑端到端 JSON 冒烟——ArkTS 将要走的完整链路(请求 JSON 进、响应 JSON 出)在 JVM 上先跑一遍。这套冒烟真实抓出了三个 bug:
- plural 自动参数缺失:上游
pluralRes(resource, quantity)省略 builder 块时默认arg(quantity.toString())——桥最初没移植这个默认行为,无显式参数的 plural 蓝图直接越界报错; - 基础链接不覆盖字面分段:上游
handleUITextAnnotations(baseAnnotations)包住整个 builder,withLink对字面量同样生效——桥最初只在参数分支查链接,“Proceed to checkout” 整段模板的链接注解失效; - 无注解嵌套不吃外层注解:上游
getString返回 String 时withStyle会重刷——桥最初一刀切地让所有嵌套保持自身样式,legal 卡的链接注解失效。
三个 bug 全部修复并由测试锁定。这正是"JVM 交叉验证是内核没漂的唯一可信证明"的现场版:不用等真机,协议层走样在桌面就被拦下了。
3.5 测试矩阵
./gradlew.bat :uitextcompose-core:jvmTest :example:nativeApp:jvmTest # 34 项全绿
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
UiTextResolverTest(core commonTest) | 21 | 两条占位符规则、%%s/%%1$s 转义矩阵(含 cmp 怪癖)、编号重排、注解栈合并、嵌套两种语义、基础/参数链接、段落、compound、越界抛错 |
BridgeProtocolTest(nativeApp jvmTest) | 12 | spec 握手、raw/res/plural/compound 端到端、嵌套样式、链接段、段落上报、规则对照、Unicode 往返、错误兜底(坏 JSON / 未知 op / 越界全部 ok:false) |
DocExampleTest(nativeApp jvmTest) | 1 | 与本仓 README.OpenHarmony_CN.md 第 5.10 节完整示例绑定的契约测试——代码与文档必须同步改 |
第 4 阶段:桥协议建模
4.1 JSON 契约
com.radusalagean.uitextcompose.core/**(纯 Kotlin 算法内核)
↓ UiTextValue 蓝图树 / UiTextResolver
Dispatch.kt + MiniJson(commonMain,JVM 可测)
↓ UTF-8 JSON
N-API uiTextDispatch()
↓
ArkUI Index.ets(Text + Span 渲染)
请求与响应:
请求 : {"op":"resolve","locale":"ro","texts":[ blueprint, ... ]} // 1 条单蓝图,>1 条自动 compound
blueprint = {"kind":"raw","text":"..."}
| {"kind":"res","template":"Salut, %1$s!","rule":"cmp",
"args":[{"value":"Radu","annotations":[ann]}],"annotations":[ann]}
| {"kind":"plural","quantity":30,"template":"<已选中的复数形式>",...}
ann = {"type":"span","color":"#FF2563EB","fontWeight":700,...}
| {"type":"paragraph","align":"center","lineHeight":30}
| {"type":"link","url":"https://...","color":"#FF2563EB","underline":true}
响应 : {"ok":true,"op":"resolve","locale":"ro","plain":"Salut, Radu!",
"segmentCount":2,"segments":[{"text":"Salut, ","span":null,"link":null},
{"text":"Radu","span":{...},"link":null}],"paragraph":null}
{"ok":false,"op":"resolve","error":"..."}
三个设计决策:其一,模板字符串由 ArkTS 侧提供——鸿蒙资源系统(ResourceManager)在 ArkTS 侧,Kotlin 侧拿不到资源 id,而占位符/注解语义与资源来源无关;其二,复数形式的选择放在 ArkTS 侧(one/few/other 分类),桥只收已选中的模板——与上游"plural 选择委托平台 getQuantityString/getPluralString"的分工一致;其三,手写 MiniJson 递归下降解析器——蓝图树嵌套(blueprint → args → annotations)必须递归解析,qrose 时代的扁平扫描器不够用,而 kotlinx.serialization 不引入,.so 保持零第三方依赖,C ABI 恒为 String → String。
4.2 错误通道
单一入口整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("UiTextDispatch")
fun uiTextDispatch(request: String): String {
val op = try { /* 解析 op */ } catch (t: Throwable) { return err("resolve", ...) }
return try {
when (op) { "spec" -> specJson(); "resolve" -> resolveJson(...); else -> err(...) }
} catch (t: Throwable) {
err(op, t.message ?: t.toString()) // {"ok":false,"op":"...","error":"..."}
}
}
非法 JSON、未知 op/kind/type、缺 template/url/texts、%5$s 越界(IndexOutOfBoundsException),全部走同一个 ok:false 通道,ArkTS 侧统一渲染成 解析失败: <error 原文>。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libuitextcompose_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosuitextcompose.so)
│ UiTextDispatch
▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
// example/nativeApp/src/ohosMain/kotlin/Bridge.kt —— @CName 只是薄包装
@CName("UiTextDispatch")
fun uiTextDispatch(request: String): String {
return dispatch(request) // 协议逻辑在 commonMain/Dispatch.kt,JVM 测试覆盖
}
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
UiTextDispatch | uiTextDispatch(req) | resolve(四种蓝图 + 两套规则 + 注解栈)/ spec(版本与能力握手)一口进 |
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"uiTextDispatch", nullptr, NapiUiTextDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosuitextcompose_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosuitextcompose_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与业务符号),不拖整份生成头。
5.4 CMake 和类型声明
add_library(ohosuitextcompose SHARED IMPORTED)
set_target_properties(ohosuitextcompose PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(uitextcompose_napi SHARED napi_init.cpp)
target_link_libraries(uitextcompose_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosuitextcompose)
OHOS_ARCH 为空或 libohosuitextcompose.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。三个必须一致的名字:nm_modname = "uitextcompose_napi" ↔ ArkTS import 'libuitextcompose_napi.so' ↔ index.d.ts 导出 uiTextDispatch。
export const uiTextDispatch: (request: string) => string;
第 6 阶段:ArkUI 宿主
6.1 九场景卡与规则对照
Demo 复刻上游 multiplatform sample 的全部场景(MainViewModel 的 9 个 ExampleEntryModel),外加一张规则对照卡:
| 卡片 | 蓝图 | 验收点 |
|---|---|---|
| raw | {"kind":"raw","text":"Radu"} | 单段无样式 |
| res | greeting 模板 + arg("Radu") | 编号占位符替换 |
| pluralRes | quantity + 已选复数形式 | en 单复数 / ro 单-少-多三分类 |
| res · annotated | cartStatus + 嵌套 plural + 嵌套 res | 「30 products」保留自身样式(带注解嵌套);「coșul…」吃 arg 级红色(无注解嵌套继承外层) |
| pluralRes · annotated | 数量参数绿色 + 基础加粗 | 注解栈字段级合并、内层覆盖 |
| compound 1/2/3 | 多蓝图拼接 + 链接 + 段落 | 链接可点(toast 显示 URL)、整段居中行高 30 |
| ToS & Privacy | legal 模板 + 两个链接嵌套参数 | 蓝色下划线链接段、最近点击行 |
| 规则对照 | 同一模板 Save %%s now: hello %s! 双规则并排 | android:Save %s now: hello world!;cmp:Save %%s now: hello %s! |
6.2 语言与数量热切换
private rebuild(): void {
// 换 locale 状态 → 换一张模板表(镜像上游 strings.xml en/ro)→ 全部蓝图重新 resolve
this.resText = this.resolve([resBp]);
this.pluralText = this.resolve([this.pluralBlueprint()]);
// ... 12 张卡片全部重发
this.status = `已解析 · locale=${this.locale} · quantity=${this.quantity}`;
}
罗马尼亚语复数按 CLDR 规则在 ArkTS 侧分类(1 → one;0 与 2…19 → few;其余 → other),quantity 五档切换直接演示 1 produs / 2 produse / 30 de produse 三分类。每张卡片下方还有灰色的 plain: 行——buildString() 与 buildAnnotatedString() 双输出同屏对照。
6.3 渲染与交互边界
分段样式映射成 ArkTS Text + Span:span.color → fontColor、fontWeight(100…900 整数)→ fontWeight、underline/lineThrough → decoration、italic → fontStyle;链接段单独走可点击的 Text 分块(ArkTS 的 Span 不支持点击事件),this.getUIContext().getPromptAction().showToast 弹出目标 URL。段落注解作用于 Text 的 textAlign 与 lineHeight。
四、完整代码对照
4.1 整体架构
com.radusalagean.uitextcompose.core/**(移植自上游 UITextUtil 的纯 Kotlin 内核)
│ UiTextValue 蓝图树 / UiTextAnnotation 注解模型 / UiTextResolver 占位符算法
example/nativeApp commonMain(Dispatch.kt + MiniJson.kt,JVM 可测)
│ UiTextDispatch 协议逻辑
example/nativeApp ohosMain(Bridge.kt)
│ @CName 薄包装
libuitextcompose_napi.so
│ uiTextDispatch
Index.ets
│ JSON parse + Text/Span 渲染
▼
ArkUI 真机页面(单页滚动 · 12 张卡片)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_ui_text_compose、上游 4 个 Compose 依赖模块移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 + kotlin-multiplatform 插件别名(仅此一条) |
build.gradle.kts(根) | 版本 1.0.0-ohos.1、全局禁用 Ohos*Test* 任务 |
uitextcompose-core/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,composeMain 不挂 target |
uitextcompose-core/src/commonMain/**(5 文件) | 纯 Kotlin 算法内核 |
uitextcompose-core/src/commonTest/** | 21 项算法交叉验证 |
uitextcompose-core/src/composeMain/**(4 文件) | 上游 Compose 层,git mv 停编 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib(baseName=ohosuitextcompose)、jvm() 测试目标、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../commonMain/Dispatch.kt | 协议逻辑(resolve/spec 分发、蓝图解析、响应序列化) |
nativeApp/.../commonMain/MiniJson.kt | 递归下降 JSON 解析器(JVal 树、\uXXXX、转义) |
nativeApp/.../ohosMain/Bridge.kt | @CName("UiTextDispatch") 薄包装 |
nativeApp/.../jvmTest/** | 12 项桥协议冒烟 + 1 项文档契约 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(九场景 + 规则对照 + 双语热切换) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(16 节,含桥协议全字段表) |
4.3 关键 API 对照
| 层次 | 上游 API | 桥协议等价物 |
|---|---|---|
| 蓝图 | UIText { raw("...") } | kind:"raw" + text |
| 蓝图 | res(Res.string.x) { arg(...) } | kind:"res" + template + args |
| 蓝图 | pluralRes(Res.plurals.x, n) | kind:"plural" + quantity + 已选模板(无 args 时自动注册数量为 %1$s) |
| 蓝图 | compound(多条目拼接) | texts 数组多条目(>1 自动 Compound) |
| 参数 | arg("plain") / arg(UIText { ... }) | args[i].value / args[i].nested(完整蓝图) |
| 注解 | +SpanStyle / +ParagraphStyle / +LinkAnnotation.Url | type:"span" / "paragraph" / "link" |
| 规则 | android 模块 / multiplatform 模块 | rule:"android" / rule:"cmp" |
| 输出 | buildString() / buildAnnotatedString() | 响应 plain / segments + paragraph |
| 刷新 | rememberResourceEnvironment() 语言变化自动重组 | ArkTS locale 状态变化 → 重新发起 resolve |
4.4 ArkTS 与 Kotlin 的边界
const raw = uiTextDispatch(JSON.stringify({
op: 'resolve', locale: 'ro',
texts: [{ kind: 'res', template: 'Salut, %1$s!', rule: 'cmp',
args: [{ value: 'Radu', annotations: [{ type: 'span', color: '#FF2563EB', fontWeight: 700 }] }] }]
}));
const res = JSON.parse(raw); // res.segments = [{text:'Salut, ',span:null}, {text:'Radu',span:{...}}]
val value = UiTextValue.Res(
template = "Salut, %1\$s!",
args = listOf(UiTextArg("Radu", listOf(UiTextAnnotation.Span(color = 0xFF2563EB, fontWeight = 700)))),
rule = PlaceholderRule.CMP,
)
val resolved = UiTextResolver.resolve(value) // plain + segments,与桥输出逐字段一致
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象、AnnotatedString 或未校验的动态结构。
五、关键决策说明
决策 1:停编 composeMain 而不是硬接或重写
compose.ui.text 没有 ohos 制品(molecule 先例也只覆盖 compose.runtime),硬接等于再打一套 Compose 发行版;而在 ArkTS 里重写一套拼串框架则丢掉上游算法的全部测试资产。停编 + 算法移植让两条占位符规则、转义矩阵、注解栈语义以纯 Kotlin 形态继续被 JVM 测试锁定,git mv 保留接回路径。
决策 2:桥出纯数据分段而不是渲染结果
文本库的产物是 AnnotatedString,它的纯数据等价物就是"分段 + 扁平样式 + 链接"列表。相比位图方案,分段数据让 ArkTS Text+Span 原生渲染——清晰、可选中、随系统字号缩放,且与上游 buildString()/buildAnnotatedString() 双输出天然对齐。
决策 3:协议逻辑放 commonMain,JVM 冒烟先行
@CName 是 Native-only,但协议逻辑(JSON 解析、蓝图建模、响应序列化)与平台无关。把它拆进 commonMain、给 nativeApp 加 jvm() 目标,ArkTS 将要走的完整 JSON 链路就能在桌面先跑——本次的三个保真度 bug(plural 自动参数、基础链接覆盖字面段、无注解嵌套继承)全部是这样在真机之前被拦下的。
决策 4:模板与复数选择留在 ArkTS 侧
鸿蒙资源系统在 ArkTS 侧,Kotlin 拿不到资源 id;上游本来就把 plural 分类委托给平台(getQuantityString / getPluralString)。桥只收已本地化、已分类的模板字符串,专注占位符与注解规则——语义边界与上游完全一致。
决策 5:手写 MiniJson,.so 零第三方依赖
蓝图树嵌套(blueprint → args → annotations)需要递归 JSON 解析,扁平扫描器不够;但引入 kotlinx.serialization 会让 .so 多出一串传递依赖。手写约两百行递归下降解析器(JVal 树、字符串转义、\uXXXX),C ABI 恒为 String → String,产物里只有 Kotlin/Native 运行时。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(Git Bash)、JDK 21(Temurin jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。
6.2 JVM 交叉验证
export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
./gradlew.bat :uitextcompose-core:jvmTest :example:nativeApp:jvmTest
34 项用例全部通过(构成见第 3.5 节表格)。其中 DocExampleTest 是文档契约测试:它把本仓 README 第 5.10 节的完整示例 JSON 逐段断言(plain、7 个分段、每段的样式字段、合并行为),代码与文档谁漂了都过不了这一关。
6.3 HAP 验证
:uitextcompose-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: UiTextDispatch / libohosuitextcompose_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libuitextcompose_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
KN so(debugShared 未 strip)arm64 约 3.9MB、x86_64 约 3.6MB,另带匹配 triple 的 libc++_shared.so(约 1.8MB),由 copyKnSoArm64 / copyKnSoX64 任务自动回填 entry/libs/{arm64-v8a,x86_64}。
6.4 功能用例
- 启动后版本握手行显示
1.0.0-ohos.1 · upstream 1.0.0 · kotlin 2.2.21-1.0.0(来自真实 Native 调用); - en 下 res 卡显示
Hi, Radu!,plain:行与分段渲染一致; - quantity 切到 1:pluralRes 卡变
1 product;ro 下切 2 / 30:分别变2 produse(few)与30 de produse(other)——罗马尼亚语三分类生效; - res · annotated 卡:
Ai 30 de produse în coșul tău de cumpăraturi.——「30 de produse」加粗+绿色(带注解嵌套保留自身样式),「coșul tău de cumpăraturi」红色(无注解嵌套继承 arg 级注解); - compound 2 的
Finalizează comanda为蓝色下划线链接段,点击弹 toast 显示https://example.com; - compound 3 整段居中、行高 30(
paragraph注解作用于Text层); - ToS 卡两个链接段分别可点,「最近点击」行更新目标 URL;
- 规则对照卡:android 行
Save %s now: hello world!、cmp 行Save %%s now: hello %s!——%s替换与%%s还原的差异并排可见; - 点 English / Română,全部 12 张卡片(含 plain 行)即时变化,状态行同步
已解析 · locale=xx · quantity=xx。
6.5 验证结论
算法内核、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的蓝图解析会话:版本握手、四种蓝图、两套占位符规则、嵌套参数两种语义、注解栈合并、链接与段落样式、双语热切换(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/ui-text-compose.git
cd ui-text-compose
export JAVA_HOME="/c/Program Files/Eclipse Adoptium/jdk-21.0.12.101-hotspot"
./gradlew.bat :uitextcompose-core:jvmTest :example:nativeApp:jvmTest # 34 项交叉验证
./gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 用 DevEco Studio 打开 example/harmonyApp(不要打开仓库根目录),配置签名后 Build HAP(s)
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.uitextcompose -m entry
hdc shell hilog | grep UiText
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算适配,必须让算法内核真正编译成
ohosArm64/ohosX64产物——但反过来,只编译不测试也不算:三个保真度 bug 全部是 JVM 冒烟抓出来的,没有一个等到真机; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- Windows 上
gradlew.bat启动器自身要求JAVA_HOME——gradle.properties的org.gradle.java.home盖不住启动阶段,报错只有一行JAVA_HOME is not set; - Kotlin 正则的 lookbehind(
(?<!%))在 Native 目标上没有支持保证,改手写单字符 lookbehind 扫描器,接受集与上游逐一致; String.format是 JVM-only API——Native 侧的\u转义要手写 hex 查表;Kotlin 也没有\f字面量转义,JSON 解析器里的 form-feed 用字面字符;- 上游嵌套语义有两种情形,不是一条规则:无注解嵌套(
getString返回 String)吃外层withStyle/withLink,带注解嵌套(AnnotatedString)不被重刷——按"解析结果分段是否全无样式"分流; - 上游
pluralRes省略 builder 块时自动注册数量参数——桥不移植这个默认行为,无参 plural 蓝图直接越界; - ArkTS 的
TextAlign枚举成员是Start / Center / End,没有Right——.textAlign(... TextAlign.End)才能编过(编译器会提示 Did you mean ‘RIGHT’,那是字符串近似匹配的误导); promptAction.showToast已废弃,换this.getUIContext().getPromptAction().showToast(...);ArkTS 的Span不支持点击事件,链接段要用独立Text分块 + FlexWrap 布局;- ArkTS 严格模式要求对象字面量有显式类型标注——蓝图/参数/注解全部先声明
interface Blueprint / Arg / Ann,嵌套字面量靠上下文类型推导; - HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套(libuitextcompose_napi.so/libohosuitextcompose.so/libc++_shared.so)缺一不可; - KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();nm_modname/ import 名 /index.d.ts三处名字必须一致。
8.2 已知问题
- 鸿蒙侧无
AnnotatedString/@Composable,桥输出纯数据分段由 ArkTS 渲染——不是在鸿蒙上跑 Compose 文本组件; SpanStyle大部分字段(fontFamily、letterSpacing、textBaseline、shadow、textGeometricTransform、textIndent、locale等)未镜像,协议字段可平滑扩展;- 段落样式是整段级近似(一个
paragraph作用于整次解析结果),上游ParagraphStyle的块级嵌套语义未在协议里展开; - 上游无注解路径走平台
String.format(%%通用反转义、%d等非s占位符也替换),桥统一走 annotated 算法(只处理s占位符、只还原%%s形态)——差异已记录在 README 已知限制; uitextcompose-multiplatform/uitextcompose-android/ 两个 sample 留盘不进构建(源文件留在树里),本 fork 不发 Maven Central;composeMain四文件等社区compose.ui/text的 ohos 制品与 CMP 1.9.2+ 对齐后接回,桥协议保持不变、ArkTS 侧无感。
8.3 未来优化方向
- 扩展
span注解镜像字段(fontFamily/letterSpacing/textGeometricTransform); - 把
StyledString(ArkUI 富文本模型)作为可选渲染路径,替代手排Span列表; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 uitextcompose-core(Compose 层 + 算法混编)
→ 确认 compose.ui/text 无 ohos 制品 → Compose 层 git mv 停编
→ 算法内核纯 Kotlin 移植(两条正则 + 注解栈 + 嵌套语义)
→ 手写 lookbehind 扫描器(规避 Native 正则差异)
→ 根构建替换(cmp_qrose 模式,Kotlin 2.2.21-1.0.0 / Gradle 8.14.1)
→ ohosArm64 / ohosX64 KLIB + JVM 交叉验证(34 项)
→ C ABI(@CName UiTextDispatch)+ 蓝图 JSON 协议 + MiniJson
→ C++ N-API(DisposeString 纪律)
→ ArkTS Text/Span 渲染
→ ArkUI 真机单页验收(九场景 + 规则对照 + 双语热切换)
9.2 封装层次
- 内核层:移植自上游的蓝图树、占位符扫描器、注解栈合并与嵌套分流(纯 Kotlin,零依赖);
- 桥层:协议逻辑(commonMain、JVM 可测)+
@CName薄包装(ohosMain),双 ABI 动态库; - N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:资源模板表、蓝图构造、解析分发、
Text+Span渲染与链接交互; - DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 停编是手段,语义不漂才是目的:UI 类库适配的验收物不是"页面能显示",而是"同一输入产出与上游一致的解析结果"——两条占位符规则、转义矩阵、嵌套两种情形都要有测试逐条锁死,连上游怪癖(
%%1$s在 cmp 规则下拆字面量)都原样保留; - 把协议逻辑放进 commonMain,让 JVM 冒烟跑在真机前面:
@CName只是 Native 的壳,JSON 进出的协议本体与平台无关——三个保真度 bug 若等到真机才发现,排查成本至少翻一个数量级; - 桥的形态跟着产物走:条码库出 PNG、图表库出坐标、文本库出分段样式——桥只传数据不传框架,
AnnotatedString的等价物就是{text, span, link}列表,ArkTS 侧原生渲染反而保留了可选中与系统字号缩放。
9.4 适配成果
当前适配已完成:
- ui-text-compose
1.0.0(Apache-2.0)算法内核的双 ABI Kotlin/Native 编译(上游 Compose 层git mv停编保留,接回路径清晰); - 1 个 C ABI 符号(
UiTextDispatch,resolve/spec 双 op)和 C++ N-API 包装层(llvm-nm验证全局 T 符号); - 34 项 JVM 交叉验证全绿(21 算法 + 12 协议 + 1 文档契约)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
- DevEco Stage ArkUI 单页验收页面(四种蓝图、两套规则、嵌套语义、链接与段落样式、en/ro 双语热切换、quantity 五档联动);
- AtomGit 项目文档、OpenHarmony README(中英双份)、本文与真机效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
- AtomGit/oh-tpc/ui-text-compose
- KMP&CMP 鸿蒙社区
- ui-text-compose 上游
- Kotlin Multiplatform
- OpenHarmony N-API
- DevEco Studio
- OpenHarmony ArkUI
这里写自定义目录标题
- 一、背景
- 二、实现路线图
- 三、逐步实现过程
- 四、完整代码对照
- 五、关键决策说明
- 六、测试与验证
- 七、运行效果
- 八、遗留问题与改进方向
- 九、总结
- 适配过程 FAQ
- Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- Q2:CMake 报 `Missing .../libxxx.so` 或找不到 Kotlin/Native 动态库?
- Q3:ArkTS 报 `Cannot find module 'libxxx_napi.so'` 或调用返回 `undefined`?
- Q4:真机安装报 `signature verification failed` 或 `install failed`?
- Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- Q6:Gradle 报 `ohosArm64` 目标不存在或 Kotlin/Native 编译失败?
- Q7:HAP 构建报 SDK 版本不匹配或 `compatibleSdkVersion` 错误?
- Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 参考文档
- 欢迎使用Markdown编辑器
欢迎使用Markdown编辑器
你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
新的改变
我们对Markdown编辑器进行了一些功能拓展与语法支持,除了标准的Markdown编辑器功能,我们增加了如下几点新功能,帮助你用它写博客:
- 全新的界面设计 ,将会带来全新的写作体验;
- 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示;
- 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示;
- 全新的 KaTeX数学公式 语法;
- 增加了支持甘特图的mermaid语法1 功能;
- 增加了 多屏幕编辑 Markdown文章功能;
- 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间;
- 增加了 检查列表 功能。
功能快捷键
撤销:Ctrl/Command + Z
重做:Ctrl/Command + Y
加粗:Ctrl/Command + B
斜体:Ctrl/Command + I
标题:Ctrl/Command + Shift + H
无序列表:Ctrl/Command + Shift + U
有序列表:Ctrl/Command + Shift + O
检查列表:Ctrl/Command + Shift + C
插入代码:Ctrl/Command + Shift + K
插入链接:Ctrl/Command + Shift + L
插入图片:Ctrl/Command + Shift + G
查找:Ctrl/Command + F
替换:Ctrl/Command + G
合理的创建标题,有助于目录的生成
直接输入1次#,并按下space后,将生成1级标题。
输入2次#,并按下space后,将生成2级标题。
以此类推,我们支持6级标题。有助于使用TOC语法后生成一个完美的目录。
如何改变文本的样式
强调文本 强调文本
加粗文本 加粗文本
标记文本
删除文本
引用文本
H2O is是液体。
210 运算结果是 1024.
插入链接与图片
链接: link.
图片:
带尺寸的图片:
居中的图片:
居中并且带尺寸的图片:
当然,我们为了让用户更加便捷,我们增加了图片拖拽功能。
如何插入一段漂亮的代码片
去博客设置页面,选择一款你喜欢的代码片高亮样式,下面展示同样高亮的 代码片.
// An highlighted block
var foo = 'bar';
生成一个适合你的列表
- 项目
- 项目
- 项目
- 项目
- 项目1
- 项目2
- 项目3
- 计划任务
- 完成任务
创建一个表格
一个简单的表格是这么创建的:
| 项目 | Value |
|---|---|
| 电脑 | $1600 |
| 手机 | $12 |
| 导管 | $1 |
设定内容居中、居左、居右
使用:---------:居中
使用:----------居左
使用----------:居右
| 第一列 | 第二列 | 第三列 |
|---|---|---|
| 第一列文本居中 | 第二列文本居右 | 第三列文本居左 |
SmartyPants
SmartyPants 是一个文本转换工具,主要功能是将普通的 ASCII 标点符号自动转换为更美观的印刷体标点符号。例如:
| 原始符号 | 转换后 | 说明 |
|---|---|---|
"引号" | “引号” | 直引号变弯引号 |
'单引号' | ‘单引号’ | 直单引号变弯单引号 |
-- | – | 两个连字符变短破折号 |
--- | — | 三个连字符变长破折号 |
... | … | 三个点变省略号 |
创建一个自定义列表
-
Markdown
- Text-to- HTML conversion tool Authors
- John
- Luke
如何创建一个注脚
一个具有注脚的文本。2
注释也是必不可少的
Markdown将文本转换为 HTML。
KaTeX数学公式
您可以使用渲染LaTeX数学表达式 KaTeX:
Gamma公式展示 Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n−1)!∀n∈N 是通过欧拉积分
Γ ( z ) = ∫ 0 ∞ t z − 1 e − t d t . \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,. Γ(z)=∫0∞tz−1e−tdt.
你可以找到更多关于的信息 LaTeX 数学表达式here.
新的甘特图功能,丰富你的文章
- 关于 甘特图 语法,参考 这儿,
UML图表
可以使用UML图表进行渲染,例如下面产生的一个序列图:
- 关于 UML图表 语法,参考 这儿,
流程图
- 关于 Mermaid 语法,参考 这儿,
FLowchart流程图
我们依旧会支持flowchart.js的流程图语法:
- 关于 Flowchart流程图 语法,参考 这儿.
导出与导入
导出
如果你想尝试使用此编辑器, 你可以在此篇文章任意编辑。当你完成了一篇文章的写作, 在上方工具栏找到 文章导出 ,生成一个.md文件或者.html文件进行本地保存。
导入
如果你想加载一篇你写过的.md文件,在上方工具栏可以选择导入功能进行对应扩展名的文件导入,
继续你的创作。
注脚的解释 ↩︎
更多推荐


所有评论(0)