Kotlin Multiplatform for OpenHarmony 实战:Circuit 架构库的 ArkTS 语义复刻

本文详述Slack开源KMP架构库Circuit 0.39.0在OpenHarmony平台的适配实践,采用纯ArkTS语义复刻策略,不编译Kotlin源码,聚焦返回栈三条不变式(根不可弹出、pop回传来源、resetRoot深度归1)与RetainedState两段式恢复机制。通过语义层与引擎层分离设计,将ArkUI Router和页面生命周期精准映射,实现在DevEco Studio 26 + HarmonyOS SDK 6.0.0(20)环境下的可验证复现。

适配信息

  • 适配仓库(oh-tpc 官方迁移仓):https://atomgit.com/oh-tpc/Circuit
  • 鸿蒙化代码分支:ohos/Circuit(PR 已提交,合并后生效)
  • 上游库:slackhq/Circuit
  • 适配路线:纯 ArkTS 语义复刻 + 桥接 @kit;代码提交于 ohos/ 目录(符合官方「在对应分支创建鸿蒙化目录」要求,不编译上游 Kotlin 源码)
  • 适配所用上游版本:0.39.0(GitHub/Maven Central 最新 release,已核对)
  • 运行环境:HarmonyOS SDK 6.0.0(20) / Kotlin 2.2.21 / KMP 2.2.21 / CMP 1.9.2 / DevEco Studio 26.0.0(DevEco 26 版本)
  • 环境搭建指引:https://atomgit.com/wdracky/kmp-ohos-adapters/blob/main/环境搭建指引.md

三方库适配征文 · 第二批 · 上游 slackhq/Circuit 0.39.0

摘要

本文记录 Slack 开源架构库 Circuit 0.39.0 在 OpenHarmony 上的适配实践。Circuit 是纯 KMP+Compose 架构库,不含平台 API,全部由「契约 + 内存结构」构成,因此适配重点不在 API 桥接,而在语义逐条对齐。文章采用「纯 ArkTS 语义复刻 + 桥接 @kit」路线,将实现拆为语义层(Screen / Navigator / Presenter / PopResult / RetainedStateRegistry)与引擎层(OhosRouterNavigator / OhosRetainedStore),逐条守住返回栈的三条不变式(根 Screen 不可弹出、pop 必须回传来源 Screen、resetRoot 后深度恒为 1),并用页面生命周期模拟 RetainedState 的两段式恢复。全文给出实测环境版本、适配映射表、核心代码、踩坑记录与 FAQ,可在 DevEco Studio 26 + HarmonyOS SDK 6.0.0(20) 下直接复现。

目录

一、Circuit 是什么

Circuit 是 Slack 的 KMP+Compose 架构库:以 Screen 为最小 UI 单元,Presenter 生产状态、Navigator 管导航、RetainedState 跨配置变更保持状态,Screen 间可回传结果。它把「屏幕 + 状态 + 导航 + 状态保持」收敛为一套跨平台契约。

环境搭建

在动手复刻之前,先把工具链对齐。本文全部代码在以下环境实测通过:
在这里插入图片描述

组件版本说明
DevEco Studio26.0.0(DevEco 26 版本)鸿蒙官方 IDE,负责工程创建、编译与真机调试
HarmonyOS SDK6.0.0(20)对应 API 20,ArkTS 严格模式
Kotlin2.2.21上游 Circuit 0.39.0 的编译版本
KMP(Kotlin Multiplatform)2.2.21与 Kotlin 版本保持一致
CMP(Compose Multiplatform)1.9.2上游 UI 层依赖版本

搭建步骤:

  1. 安装 DevEco Studio 26(DevEco 26 版本),首次启动时按向导下载 HarmonyOS SDK 6.0.0(20)。
  2. 新建一个 Empty Ability 工程,确认 build-profile.json5 中 compileSdkVersion 为 6.0.0(20)。
  3. 若需对照上游 Kotlin 侧实现,安装 Kotlin 2.2.21 与 KMP 2.2.21 插件,CMP 依赖锁定 1.9.2。
  4. 将 ohos/src/ 下的 .ets 文件复制进工程 ets/ 目录,在页面中 import 即可运行验收页。

完整的环境搭建指引(含 SDK 下载、环境变量、常见报错处理)见:
https://atomgit.com/wdracky/kmp-ohos-adapters/blob/main/环境搭建指引.md

二、适配路线

Circuit(Kotlin)鸿蒙语义层鸿蒙引擎层
Screen(key + Ui)Screen interface—
NavigatorNavigator interface + CircuitBackStackOhosRouterNavigator(pushUrl/back/clearAndPushUrl)
Presenter<S>.present()Presenter<S> interface—
PopResultPopResult interface—
rememberRetainedRetainedStateRegistryOhosRetainedStore(onPageHide/Show 驱动)
Screen → 路由key→路由名映射routeFor()(detail/xxx→DetailDemo)

整体分层与调用关系如下:

Circuit 上游 Kotlin 契约

鸿蒙语义层(纯 ArkTS)

鸿蒙引擎层(桥接 @kit / @ohos)

ArkUI Router / 页面生命周期

CircuitBackStack 返回栈

RetainedStateRegistry 状态注册表

OhosRouterNavigator

OhosRetainedStore

三、核心实现:返回栈的三条不变式

Circuit 的 Navigator 有三条必须守住的不变式,语义层逐条实现:

public pop(result?: PopResult): boolean {
  if (this.screens.length <= 1) {
    return false;  // 不变式 1:根 Screen 不可弹出
  }
  const popped: Screen = this.screens.pop() as Screen;
  this.lastResult = result ?? { fromScreenKey: popped.key, payload: {} };
  return true;     // 不变式 2:pop 必须回传来源 Screen
}
public resetRoot(screen: Screen): void {
  this.screens = [screen];  // 不变式 3:resetRoot 后 depth 恒为 1
  this.lastResult = null;
}

引擎层 OhosRouterNavigator 把这三条不变式同步到 ArkUI Router:goTo→pushUrl、pop→getRouter().back()、resetRoot→clearAndPushUrl。语义栈与路由栈始终一致,验收页可对 snapshotKeys() 直接断言。

三条不变式与引擎层同步的完整流程:

goTo

pop

是

否

resetRoot

Navigator 操作

操作类型

push 新 Screen 入栈

栈深度 <= 1 ?

返回 false:根 Screen 不可弹出

弹出栈顶并回传 PopResult

栈重置为单元素,depth 恒为 1

OhosRouterNavigator 同步路由栈

goTo → pushUrl

pop → getRouter().back()

resetRoot → clearAndPushUrl

语义栈与路由栈一致,snapshotKeys() 可断言

四、RetainedState:两段式恢复

Circuit 的 RetainedState 解决「配置变更后状态不丢」。鸿蒙引擎层用页面生命周期模拟:

// OhosRetainedStore
onHostHide(): void { this.registry.saveAll(); }    // 对应 ON_STOP
onHostShow(): void { this.registry.restoreAll(); } // 对应 ON_START

语义层 saveAll() 保留已求值项、restoreAll() 清空 provider——重建后 getValue(key) 直接命中缓存,provider 不再执行。验收页「模拟隐藏→恢复」按钮验证保持值不丢。

两段式恢复的时序如下:

"Provider 缓存" "RetainedStateRegistry" "OhosRetainedStore" "页面生命周期" "用户/系统" "Provider 缓存" "RetainedStateRegistry" "OhosRetainedStore" "页面生命周期" "用户/系统" "页面隐藏(ON_STOP)" "onHostHide()" "saveAll()" "保留已求值项" "页面显示(ON_START)" "onHostShow()" "restoreAll()" "清空 provider,仅留缓存" "getValue(key) 直接命中,provider 不再执行"

五、踩坑记录

  1. 两层导航的边界:ArkUI Navigation 要求两页同容器,与「两页各自独立」验证目标冲突;引擎层选择 Router(各页独立),Screen→路由映射放在引擎层,语义层零平台知识。
  2. getRouter() 的获取时机:需在页面 onReady/aboutToAppear 之后 getUIContext().getRouter(),过早获取为空。
  3. 结果载荷类型:ArkTS 禁 unknown,PopResult 的 payload 用 Record<string, Object> 显式约束,跨层传递零丢失。

六、总结

Circuit 是纯架构库——没有一行平台 API,全部是「契约 + 内存结构」。这类库的适配重点是不变式逐条对齐(根不可弹/resetRoot 深度/pop 回传),以及把生命周期事件正确接到引擎层。本系列已完成 MVIKotlin、Kodein-DI、KMM-ViewModel 等架构/状态库,Circuit 补上了「Screen 级导航 + 状态保持」这块拼图。

一句话总结:Circuit 这类「零平台 API」的架构库,鸿蒙化的关键不是翻译代码,而是把上游的不变式与生命周期语义一条条对齐到 ArkUI 的 Router 与页面生命周期上;语义层保持零平台知识,平台差异全部收敛在引擎层,是这类库可复用、可验收的适配范式。


常见问题(FAQ)

Q1:鸿蒙化代码在哪里?为什么官方仓默认分支看不到?
A:鸿蒙化代码提交在 ohos/Circuit 分支的 ohos/ 目录下(语义层 + 引擎层 + 示例 + 博客)。PR 合并前,官方仓默认分支仍是上游镜像,因此看不到 ohos/ 目录;合并后即可在默认分支查看。

Q2:为什么是 ArkTS 语义复刻,而不是直接编译上游 Kotlin/Native 源码?
A:OpenHarmony 的 ArkTS 运行时不直接执行 Kotlin/Native 产物。本路线用 ArkTS 等价复刻上游核心语义(接口 / 算法 / 状态机),并通过桥接 @ohos.* 系统能力实现真实能力,可在 DevEco 中独立编译验证,不依赖上游构建链。

Q3:如何本地验证?
A:将 ohos/src/ 下的 .ets 文件复制到你的 HarmonyOS 工程 ets/ 目录,在页面中 import 即可;各库均提供 Demo 验收页(见本系列 demo 工程),多数能力无需密钥或联网即可跑通。

Q4:上游版本如何对应?
A:各库 ohos/README.OpenHarmony.md 声明了所适配的上游版本与 tag/commit,请以该文件为准。

Q5:DevEco Studio 用哪个版本?低版本能跑吗?
A:本文实测环境为 DevEco Studio 26(DevEco 26 版本) + HarmonyOS SDK 6.0.0(20)。低版本 IDE 可能缺少 API 20 的 SDK 与 ArkTS 严格模式校验,建议直接对齐 26 版本,避免因工具链差异导致的编译报错。

Q6:Kotlin / KMP / CMP 版本必须严格一致吗?
A:建议对齐 Kotlin 2.2.21、KMP 2.2.21、CMP 1.9.2,这是上游 Circuit 0.39.0 的编译基线。版本错配时,Kotlin 侧对照代码可能出现 API 差异,但鸿蒙侧 ArkTS 复刻代码本身不依赖这些版本,可独立编译运行。

Q7:环境搭建遇到问题怎么办?
A:优先查阅环境搭建指引:https://atomgit.com/wdracky/kmp-ohos-adapters/blob/main/环境搭建指引.md ,其中覆盖了 SDK 下载失败、环境变量未生效、真机签名等常见问题;仍无法解决可在 KMP&CMP 鸿蒙社区提问。

欢迎加入 KMP&CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值