Flutter OHOS 纯 Dart 插件移植实录:set_primarySwatch_color 色板生成库适配,定位并修复色阶插值括号缺陷,补齐鸿蒙插件注册壳
开发工具: 华为云码道
DevEco Studio:
26.0.0(26.0.0.851)
set_primarySwatch_color 小到有点不像一个「库」:lib/ 里真正有用的代码只有 28 行,全部在 Dart 层,公开 API 只有一个方法——把任意 Color 展开成一条 10 档的 MaterialColor,好让 ThemeData(primarySwatch: ...) 能吃下任意品牌色:
theme: ThemeData(
primarySwatch: SetMaterialColor().createMaterialColor(const Color(0xFF2196F3)),
),
正因为核心逻辑全在 Dart,适配这类库的「工程量」看起来接近零:加一个 ArkTS 插件壳、注册 ohos 平台、把 example 跑到真机上,就完事了。但这次真机验证时,演示页的色阶表把问题摆在了明面上——灰色基色的 600 档是橄榄色。
顺着这 4 个像素往下查,问题落在一对括号上:上游蓝通道写成了 b + ((ds < 0 ? b : (255 - b) * ds).round()),.round() 作用在整个三元表达式上,于是所有比 500 更深的档位蓝通道都变成了 b + b,超过 255 再按 8 位回绕。灰色 #9E9E9E 的 600 档因此算成 #8E8E3C,青色偏绿、红色偏品红——这个库在任何平台上都算错了深色档,只是没人把色值逐个打印出来看。

本文以 set_primarySwatch_color 0.0.1 为例,记录一个纯 Dart 插件在鸿蒙上的适配全过程:插件壳怎么写、注册怎么验证、示例页怎么把「算出来的值」显示出来,以及上面那个括号缺陷是怎么被发现、推导、修掉并在真机上逐个色值对账的。文中的代码以提交 d5b4edd 为参考,TAG 为 0.0.1-ohos-1.0.0-beta.1。

一、插件简介与适配目标
1.1 公开面:一个类、一个方法
final MaterialColor swatch =
SetMaterialColor().createMaterialColor(const Color(0xFF9E9E9E));
返回的 MaterialColor 里带 10 个键:50/100/200/300/400/500/600/700/800/900。用法就一句 primarySwatch: swatch,之后 AppBar、ElevatedButton、Theme.of(context).primaryColor 都会跟着变。
1.2 插件壳提供的通道
lib/ 下另外两个文件是 flutter create --template=plugin 留下的标准结构:平台接口 + MethodChannel 实现。通道上只有一个方法,和核心功能无关:
| 要素 | 值 |
|---|---|
| 通道名 | set_primary_swatch_color |
| 方法名 | getPlatformVersion |
| 参数 | 无 |
| 返回 | String——平台标识,鸿蒙侧返回 OpenHarmony-6.1.1.120 |
也就是说:这个库有没有「原生能力」并不影响它能不能用,通道只是模板残留。但适配不能因此跳过——插件注册是否成功、通道是否真的挂在引擎上,是能直接观察到的运行时事实,后面第 5 章用 hilog 证明它。
1.3 这次适配实际做了五件事
- 在
pubspec.yaml的platforms里注册ohos,补 ArkTS 插件壳(getPlatformVersion); - 修掉 上游色阶算法的括号缺陷(第 4 章的核心),并补回归用例;
- 修掉 上游模板生成的三个编译不过的测试(引用了不存在的
SetPrimarySwatchColor类和MyApp); - 重写 example 演示页:主题随选色真正重建、每档色阶打印十六进制、标签颜色按色块亮度自适应、显示通道返回的平台版本;
- 真机上把 4 个基色 × 10 档色值逐个对账,确认修复生效。
其中 2 和 3 是仓库里真实存在的缺陷,不是「为了适配而改」。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


2.1 开发套件版本与工程中的 SDK 版本配置
| 项目 | 版本 |
|---|---|
| Flutter | 3.47.7-ohos-1.0.0(oh-3.47-dev,framework revision 4b5928c43f) |
| Dart | 3.13.3 |
| DevEco Studio | 26.0.0(26.0.0.851) |
| OpenHarmony SDK | 6.1.1(24) |
| 真机 | TLR-AL00(HUAWEI nova 14),OpenHarmony-6.1.1.120,API 24 |
宿主工程的产物配置在 example/ohos/build-profile.json5:
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS",
}
]

compatibleSdkVersion 跟真机 ROM(6.1.1(24))对齐,hdc install 才不会因为 API 级别不匹配被拒。
2.2 本机构建环境的两处坑
这两个坑和具体库无关,但每次构建都会撞上,先写在前面:
# 1. WorkBuddy 注入的 NODE_OPTIONS(safe-delete shim)会打断 hvigor 清理构建缓存,
# 报 SAFE_DELETE_BULK_CONFIRM_REQUIRED,最后一行是 COMPILE RESULT:FAIL,
# 看起来像 ArkTS 语法错误,其实是环境问题。
$env:NODE_OPTIONS=""
# 2. hvigor 调 java 需要 jbr\bin 在 PATH 里(不是 jbr 目录本身)
$env:JAVA_HOME="D:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="D:\Program Files\Huawei\DevEco Studio\jbr\bin;D:\Program Files\Huawei\DevEco Studio\tools\ohpm\bin;" + $env:PATH
还有一个只影响「用哪条命令构建」的事实:在 Git Bash 里跑 flutter build hap --debug,工具链会去调 wmic.exe / reg.exe 探测 Windows 版本,而这两个程序在本机被安全策略拦截,命令以退出码 1 结束。实际构建结果是成功的(输出里能看到 ✓ Built build\ohos\hap\entry-default-signed.hap),所以判断成败要看这一行和 HAP 的时间戳,不能只看退出码。第 8 章给出完整命令。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
上游是 hoiii13/set_primarySwatch_color(BSD 3-Clause License)。按约定,适配后的仓库统一放在 AtomGit 的 oh-flutter 组织下:
https://atomgit.com/oh-flutter/set_primarySwatch_color
本地保留两个远端,atomgit 放适配后的代码,origin 保留指向上游,方便随时 git diff origin/main 看适配改了哪些东西:
git remote add atomgit git@atomgit.com:oh-flutter/set_primarySwatch_color.git
git remote -v
# atomgit git@atomgit.com:oh-flutter/set_primarySwatch_color.git (fetch)
# atomgit git@atomgit.com:oh-flutter/set_primarySwatch_color.git (push)
# origin https://github.com/hoiii13/set_primarySwatch_color.git (fetch)
# origin https://github.com/hoiii13/set_primarySwatch_color.git (push)
3.2 将代码拉取到宿主机

仓库结构是标准插件模板:lib/(3 个文件)、android/、ios/、example/、test/。没有 ohos/,也没有任何多余依赖——pubspec.yaml 里只有 flutter 和 plugin_platform_interface。
这也是判断「适配成本」的第一手信息:没有原生实现要移植,没有系统能力要对接,android/、ios/ 里的代码只是各返回一个平台版本字符串。真正的活是验证 Dart 逻辑在鸿蒙上的行为,以及把模板里那些从来没被跑通过的测试修好。
3.3 在仓库根目录确认适配分支与版本标记

三点说明:
- 分支是
main(这个上游用的就是新模板默认分支,别照抄master)。TAG 按「上游版本 +-ohos-+ 适配版本」命名,这里是0.0.1-ohos-1.0.0-beta.1。 git status --short --branch显示## main...origin/main [ahead 5]是正常的:适配提交只推到atomgit,没有回推上游 GitHub,所以相对origin/main永远领先 5 个提交(1 个适配 + 1 个文档 + 1 个文档修正 + 1 个测试修复 + 1 个算法修复)。- 适配期间 TAG 曾指向过中间提交,最终用
git tag -f移到最后一个提交并git push -f,保证文档里的ref指向的树是验证过的那一份。
3.4 注册 ohos 平台并修复色阶算法

pubspec.yaml 里只加三行:
plugin:
platforms:
android:
package: com.example.set_primary_swatch_color
pluginClass: SetPrimarySwatchColorPlugin
ios:
pluginClass: SetPrimarySwatchColorPlugin
ohos:
pluginClass: SetPrimarySwatchColorPlugin
ohos段只有pluginClass,没有package——ArkTS 侧的类通过ohos/index.ets导出,不按包名寻址。
算法修复只有一行(第 4 章详述为什么):
- b + ((ds < 0 ? b : (255 - b) * ds).round()),
+ b + ((ds < 0 ? b : (255 - b)) * ds).round(),
flutter analyze 的结果:
4 issues found. (ran in 8.8s)
4 条全是 Color.red/green/blue/value 在 Flutter 3.27+ 的 deprecated info,属于上游代码风格,本次不改(改成 .r/.g/.b 是 API 迁移,会让适配 diff 混入无关改动)。适配前的状态是 2 个 error(测试文件引用不存在的类),已经在第 6 章修掉。
3.5 适配后的项目目录

四、Dart 接口与实现分析
4.1 调用链路
和 md5_plugin、quicktype_dart 那种「Dart 透传、原生干活」的库不同,这里主链路完全没有原生参与:
Dart: SetMaterialColor().createMaterialColor(0xFF2196F3)
│ 纯 Dart 计算 10 档色值,装进 Map<int, Color>
▼
Dart: MaterialColor(0xFF2196F3, {50: …, 100: …, …, 900: …})
│ 交给 ThemeData(primarySwatch: …)
▼
Flutter 框架:AppBar / 按钮 / 文字颜色按 primarySwatch 取值
唯一会走到原生的是模板自带的 getPlatformVersion:
Dart: MethodChannelSetPrimarySwatchColor().getPlatformVersion()
│ MethodChannel('set_primary_swatch_color')
▼
ArkTS: SetPrimarySwatchColorPlugin.onMethodCall → deviceInfo.osFullName
▼
Dart: Future<String?> ← "OpenHarmony-6.1.1.120"
所以第 5 章的 ArkTS 代码价值不在业务,而在证明插件注册链路通了:注册成功才会有 Adding plugin: SetPrimarySwatchColorPlugin 和 Setting handler for channel 'set_primary_swatch_color' 两行日志。
4.2 唯一公开 API 与实现全文
lib/set_primary_swatch_color.dart 去掉注释后的全部逻辑:
class SetMaterialColor {
MaterialColor createMaterialColor(Color color) {
List strengths = <double>[.05];
Map<int, Color> swatch = {};
final int r = color.red, g = color.green, b = color.blue;
for (int i = 1; i < 10; i++) {
strengths.add(0.1 * i);
}
for (var strength in strengths) {
final double ds = 0.5 - strength;
swatch[(strength * 1000).round()] = Color.fromRGBO(
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
b + ((ds < 0 ? b : (255 - b) * ds).round()), // ← 缺陷在这里
1);
}
return MaterialColor(color.value, swatch);
}
}
4.3 算法拆解:十个档位是怎么来的
strengths是[0.05, 0.1, 0.2, …, 0.9],键取(strength * 1000).round(),正好落在50/100/…/900。ds = 0.5 - strength,可以理解成「离中间档的距离」:strength < 0.5→ds > 0→ 通道值向 **255(白)**插值:c + (255 - c) * ds;strength > 0.5→ds < 0→ 通道值向 **0(黑)**插值:c + c * ds;strength == 0.5→ds == 0→ 结果必须等于基色本身。
最后这条是这套算法唯一能拿来做硬断言的性质,也是后面回归用例的支点:swatch[500] == base。
4.4 真机上的第一处异常:灰色 600 档是橄榄色
演示页把每档色值直接印在色条上,所以真机截图本身就能当数据用。选灰色 #9E9E9E(Colors.grey)时,屏幕上出现的是:
50 #FFCACACA 400 #FFA8A8A8 700 #FF7E7E3C
100 #FFC5C5C5 500 #FF9E9E9E 800 #FF6F6F3C
200 #FFBBBBBB 600 #FF8E8E3C 900 #FF5F5F3C
300 #FFB1B1B1
50–500 都是标准中性灰,从 600 起 B 通道被按在 0x3C(60)不动,色条肉眼可见地偏橄榄。同一现象在其他基色上同样存在,只是表现不同——红色往品红跑、青色往绿跑:
| 基色 | 档位 | 按 R/G 的规则应有 | 修复前真机 | 修复后真机 |
|---|---|---|---|---|
#9E9E9E 灰 | 600 | #8E8E8E | #8E8E3C | #8E8E8E |
#9E9E9E 灰 | 900 | #5F5F5F | #5F5F3C | #5F5F5F |
#2196F3 蓝 | 600 | #1E87DB | #1E87E6 | #1E87DB |
#F44336 红 | 600 | #DC3C31 | #DC3C6C | #DC3C31 |
R、G 两个通道都是对的,只有 B 不对,而且同一个基色下 600–900 的 B 值是常数。这个「常数」就是破案的关键。
4.5 括号错位:.round() 落在了三元表达式外面
把上游那行按 Dart 的优先级还原一下:
b + ((ds < 0 ? b : (255 - b) * ds).round())
- 条件表达式里,
else分支是(255 - b) * ds(*优先级高于?:); - 但
.round()作用在整个条件表达式上; - 于是
ds < 0(即 500 档以上)时,条件表达式取then分支——b本身,.round()对整数无效,表达式退回b; - 整个通道变成
b + b。
代入真机数据,全部对上:
| 基色 B | b + b | 8 位截断 | 真机 B | 现象 |
|---|---|---|---|---|
#9E9E9E → 158 | 316 | 316 & 0xFF = 60 (0x3C) | 60 ✓ | 灰变橄榄 |
#2196F3 → 243 | 486 | 486 & 0xFF = 230 (0xE6) | 230 ✓ | 蓝偏亮 |
#F44336 → 54 | 108 | 108(未越界) | 108 (0x6C) ✓ | 红偏品红 |
Color.fromRGBO 的通道只保留 8 位,越界值被截断成低 8 位——所以 b 超过 127 时会出现「越算越暗反而变亮」的回绕,灰色恰好是这个区间,才显得那么刺眼。
顺带说明:这个缺陷与鸿蒙无关,Android/iOS 上同样存在。只是那些平台上没人把 10 档色值逐个打印出来看,而 primarySwatch[600] 之后的档位通常只影响少数控件,色偏很难被察觉。
4.6 修复与回归用例
修复就是补回括号,让 .round() 只作用于乘法:
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
test/set_color_test.dart 里补了四条断言(原有的 Test createMaterialColor 只断言类型,等于没测):
| 用例 | 断言内容 |
|---|---|
shade 500 is the exact base color | 4 个基色的 swatch[500] 严格等于基色 |
neutral base keeps neutral shades down to 900 | 中性基色的 600–900 档 R=G=B(这个用例在修复前必挂) |
dark shades get darker monotonically | 从 50 到 900 R 通道单调不增 |
createMaterialColor returns a MaterialColor | 10 个键都存在且非空 |
需要如实说明:这四条用例在本机没有执行成功,flutter test 报 Connection closed before test suite loaded.(工具链问题,第 9 章有完整报错)。实际验证走的是两条替代路径——纯 Dart 复算(把同一套公式在 VM 里跑一遍拿到期望值)+ 真机色条上的十六进制文本,两者逐档对齐(第 8 章表格)。
4.7 它和 Material 官方色板不是一回事
顺手记一个容易被误解的点:这个库生成的是近似色阶,不是 Google Material 的官方 palettes。比如 Colors.blue 的官方 600 是 #1E88E5,这里算出来是 #1E87DB;官方 900 是 #0D47A1,这里是 #145A92。差异来自「只按 ds 线性插值、不做色相/饱和度校正」。
对 primarySwatch 这种用途来说,近似完全够用(框架只会按需取某一档);但如果业务里要拿色阶做品牌色规范、对外输出色值,就不能依赖它,得用官方色板或自己定义。
五、补全 OHOS 原生实现与工程配置
5.1 插件壳:注册与注销
ohos/src/main/ets/components/plugin/SetPrimarySwatchColorPlugin.ets 全文 48 行,核心就是三个生命周期回调:
import deviceInfo from '@ohos.deviceInfo';
import {
FlutterPlugin, FlutterPluginBinding, MethodCall, MethodCallHandler,
MethodChannel, MethodResult,
} from '@ohos/flutter_ohos';
export default class SetPrimarySwatchColorPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
getUniqueClassName(): string {
return "SetPrimarySwatchColorPlugin"
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "set_primary_swatch_color");
this.channel.setMethodCallHandler(this)
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null)
this.channel = null
}
}
// onMethodCall 见 5.2 / 5.3
}
三个细节值得记下来:
getUniqueClassName()必须实现,返回的字符串要和注册表里的类名一致;- 通道名
set_primary_swatch_color必须与 Dart 侧MethodChannel('set_primary_swatch_color')完全一致,写错不会编译失败,只会在调用时抛MissingPluginException; onDetachedFromEngine里必须setMethodCallHandler(null),否则热重启后旧 handler 会留在引擎上,表现为「回调被调用了两次」。
5.2 getPlatformVersion 的返回格式
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
// Android returns "Android <release>", iOS returns "iOS <version>".
// On OpenHarmony osFullName is already "OpenHarmony-6.1.1.120", so do
// not prepend the os name again — that yields "OpenHarmony OpenHarmony-…".
result.success(deviceInfo.osFullName);
} else {
result.notImplemented()
}
} catch (e) {
result.error("PLUGIN_ERROR", `set_primary_swatch_color: failed to handle ${call.method}: ${JSON.stringify(e)}`, null)
}
}
首版这里写的是 result.success(\OpenHarmony ${deviceInfo.osFullName}`),想模仿 Android 的 “Android 13”` 格式,结果设备上打出来是:
平台版本: OpenHarmony OpenHarmony-6.1.1.120
deviceInfo.osFullName 本身就带 OpenHarmony- 前缀。改成直接返回原值后:
平台版本: OpenHarmony-6.1.1.120
这类「格式对齐」问题不会崩、不会编译失败,只在真人看界面时才会被发现——和 md5_plugin 那个 Base64/hex 的坑是同一类:返回值的形状要跨端对齐。
5.3 方法分发与错误语义
- 未实现的方法一律
result.notImplemented(),Dart 侧收到MissingPluginException; - 整个方法体包在
try/catch里,异常统一转成result.error("PLUGIN_ERROR", …, null),Dart 侧是PlatformException,code为PLUGIN_ERROR。
deviceInfo 在极早期(引擎刚起来、系统服务还没就绪)可能抛异常,try/catch 就是为了这种情况——虽然实际很难触发,但比让 Dart 侧拿到一个裸异常好排查。
5.4 声明插件与宿主配置
pubspec.yaml 的注册见 3.4;ohos/src/main/module.json5 声明这个 HAR:
{
"module": {
"name": "set_primary_swatch_color",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
ohos/oh-package.json5 的 main 指向 index.ets,index.ets 只做一件事——默认导出插件类:
import SetPrimarySwatchColorPlugin from './src/main/ets/components/plugin/SetPrimarySwatchColorPlugin';
export default SetPrimarySwatchColorPlugin;
宿主工程(example/ohos)不需要手写注册,EntryAbility 里调用生成文件即可:
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
}
5.5 注册产物与真机日志
冷启动时清空 hilog 再抓,能看到注册链路的两个关键点:
23:28:24.355 Flutter: FlutterEngineCxnRegistry --> Adding plugin: SetPrimarySwatchColorPlugin
23:28:24.355 Flutter: DartMessenger --> Setting handler for channel 'set_primary_swatch_color'
两行都出现,说明「插件被引擎认领」+「通道挂上了 handler」。如果只有第一行、没有第二行,通常是 onAttachedToEngine 里 setMethodCallHandler 没调;如果两行都没有,就是 pubspec.yaml 的 platforms.ohos 或 GeneratedPluginRegistrant.ets 的问题。
演示页上那行 平台版本: OpenHarmony-6.1.1.120 是同一个事实的界面证据:通道真的往返了一次。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
README.OpenHarmony.md # 英文说明:安装、约束、API、已知问题
README.OpenHarmony_CN.md # 中文同上
CHANGELOG.OpenHarmony.md # 适配变更记录
README.OpenSource # 上游来源与许可证声明
example/ohos/README.OpenHarmony*.md # 宿主工程说明
README 里的安装块必须写成 oh-flutter 组织下的地址,表格里的分支名要跟仓库实际默认分支一致:
dependencies:
set_primarySwatch_color:
git:
url: https://atomgit.com/oh-flutter/set_primarySwatch_color
ref: 0.0.1-ohos-1.0.0-beta.1
| Flutter 框架版本 | TAG 名称 | 分支名 |
|---|---|---|
| 3.47 | 0.0.1-ohos-1.0.0-beta.1 | main |
这两个字段(组织名、分支名)是模板最容易写错的地方:初版写成了 CPF-Flutter 组织和 master 分支,照着文档装会直接拉不到代码。适配时用 git branch --show-current 确认过再写。
6.2 修掉失效的示例与测试(这次的真实缺陷)
上游模板生成的三个测试全部编译不过,flutter analyze 直接报 2 个 error:
| 文件 | 原写法 | 问题 |
|---|---|---|
test/set_primary_swatch_color_test.dart | SetPrimarySwatchColor() | 库里没有这个类,只有 SetMaterialColor |
example/integration_test/plugin_integration_test.dart | SetPrimarySwatchColor() | 同上 |
example/test/widget_test.dart | pumpWidget(const MyApp()) | 示例入口是 SetPrimarySwatchDemoApp,MyApp 不存在 |
修法是「改用真实存在的类型」,而不是删测试:
// plugin 测试:默认实例 + mock 平台
final setPrimarySwatchColorPlugin = MethodChannelSetPrimarySwatchColor();
// integration test:通道 + 500 阶断言
final String? version =
await MethodChannelSetPrimarySwatchColor().getPlatformVersion();
expect(version, isNotNull);
expect(version!.isNotEmpty, true);
// widget test:构建真实入口
await tester.pumpWidget(const SetPrimarySwatchDemoApp());
expect(find.text('set_primary_swatch_color Demo'), findsOneWidget);
改完 flutter analyze:
# 插件根目录
4 issues found. # 全部是 deprecated info
# example
No issues found!
6.3 示例演示页重写
原始演示页只有一个静态 MaterialApp,色板点了不生效、色阶只有灰底黑字,演示价值几乎为零。重写后有三处结构性改动:
(1)主题真正随选色重建。 原来 ThemeData 在 MaterialApp 里用固定的 initial 构造,改的只是页面内的局部变量,所以 AppBar 永远是初始色。现在把选中的颜色提到 app 层状态,MaterialApp 跟着重建:
class _SetPrimarySwatchDemoAppState extends State<SetPrimarySwatchDemoApp> {
Color _color = Colors.blue;
void _pick(Color c) => setState(() => _color = c);
Widget build(BuildContext context) {
final MaterialColor swatch = SetMaterialColor().createMaterialColor(_color);
// 浅色基色(黄/青)作主色时,默认白字标题几乎看不清,按亮度选前景色。
final bool onDark =
ThemeData.estimateBrightnessForColor(_color) == Brightness.dark;
return MaterialApp(
theme: ThemeData(
primarySwatch: swatch,
appBarTheme: AppBarTheme(
backgroundColor: swatch[500],
foregroundColor: onDark ? Colors.white : Colors.black87,
),
useMaterial3: false,
),
home: DemoHomePage(picked: _color, onPick: _pick),
);
}
}
这一步不只是「好看」:primarySwatch 的全部意义就是让框架按档取色,AppBar 跟着换色才是这个库有效的直观证据。
(2)每档色阶打印十六进制。 色条右侧直接写 600 #FF8E8E8E。这个改动的价值在第 4 章体现——没有这行值,那个括号缺陷只能靠肉眼猜色:
final Color bg = swatch[shade]!;
final bool dark =
ThemeData.estimateBrightnessForColor(bg) == Brightness.dark;
return Container(
height: 34,
color: bg,
child: Text('$shade ${_hex(bg)}',
style: TextStyle(color: dark ? Colors.white : Colors.black87)),
);
(3)标签颜色按色块亮度决定。 原来按档位号拍脑袋(shade <= 400 ? 黑 : 白),浅色基色的 600 档会变成「白字压在浅黄上」,几乎不可读。改成按实际色块亮度判断后,黄色基色从 50 到 900 都清晰:
static Color _readableOn(Color bg) =>
ThemeData.estimateBrightnessForColor(bg) == Brightness.dark
? Colors.white : Colors.black87;
另外演示页把通道返回值也显示出来了(FutureBuilder + getPlatformVersion()),让「通道通了」这件事出现在截图里而不只是日志里。
6.4 提交前检查
flutter analyze # 4 info(均为上游 deprecated)
cd example && flutter analyze # No issues found!
git status --short # 工作区干净
6.5 提交并推送
git add -A
git commit -m "fix: correct blue channel parenthesis in createMaterialColor"
git tag -f 0.0.1-ohos-1.0.0-beta.1
git push atomgit main
git push -f atomgit 0.0.1-ohos-1.0.0-beta.1
TAG 用 -f 是因为它先前指向过中间提交;文档里的 ref 必须指向最终验证过的那份树,否则用户装到的还是带缺陷的版本。
七、使用 example 演示接入
7.1 本地适配时使用路径依赖
dependencies:
set_primary_swatch_color:
path: ../
example 里就是这么写的,改库代码后 flutter build hap 会直接带上新逻辑,不需要 pub get 重新解析版本。
7.2 通过 AtomGit 引入插件
dependencies:
set_primarySwatch_color:
git:
url: https://atomgit.com/oh-flutter/set_primarySwatch_color
ref: 0.0.1-ohos-1.0.0-beta.1
ref 也可以写分支名(main),但生产项目建议锁 TAG——分支会往前走,构建结果不可复现。
7.3 演示页的三块内容与操作
- 基色 + 平台版本:左上角色块是当前基色,右边一行
基色 #FF2196F3 → swatch[500] = #FF2196F3,用来验证「500 档等于基色」;下面一行平台版本: OpenHarmony-6.1.1.120来自通道。 - 19 色色板:一行 6 个圆点,点任意一个都会重建主题,
AppBar与底部按钮同步换色。 - 十档色阶 + 两个按钮:色条上带
档位 #AARRGGBB,底部swatch[600]按钮和primarySwatch 文字按钮展示按档取色的实际效果。
7.4 关于 bundleName
example/ohos/AppScope/app.json5 里的 bundleName 是 com.example.color_system_example——这是本机多个示例工程共用的 bundle 名(由宿主模板决定)。后果是同设备上装另一个示例会覆盖这个,真机验证时如果界面和预期不一致,先 hdc install -r 重装确认跑的是哪个包。这与本文的库本身无关,但排查时很容易被误导。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
cd set_primarySwatch_color # 插件根目录
flutter analyze # 4 issues(deprecated info)
cd example
flutter analyze # No issues found!
插件的 flutter test 在本机跑不起来(见 8.6),所以逻辑验证靠真机 + 纯 Dart 复算。
8.2 确认设备连接
hdc list targets
# 4UQ9K25508013016
8.3 构建 HAP
$env:NODE_OPTIONS=""
$env:JAVA_HOME="D:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="D:\Program Files\Huawei\DevEco Studio\jbr\bin;D:\Program Files\Huawei\DevEco Studio\tools\ohpm\bin;" + $env:PATH
Set-Location "D:\flutter\lib2\set_primarySwatch_color\example"
flutter build hap --debug
输出末尾:
Running Hvigor task assembleHap... 59.2s
✓ Built build\ohos\hap\entry-default-signed.hap.
产物 example/build/ohos/hap/entry-default-signed.hap,98.7 MB(debug 带未裁剪的引擎,属正常体积)。
如果先撞上 ohpm install failed,先在 example/ohos 里手动装依赖再构建:
Set-Location "D:\flutter\lib2\set_primarySwatch_color\example\ohos"
ohpm install
8.4 安装并启动
cd example/build/ohos/hap
hdc install -r entry-default-signed.hap
hdc shell aa start -b com.example.color_system_example -a EntryAbility
hdc install的目标写成相对文件名(先cd到 HAP 目录),写 Windows 绝对路径会被拼成c:\...\C:/...而报Error opening file。
8.5 真机界面
八张图对应的操作与结论:
| 图 | 操作 | 看到什么 |
|---|---|---|
fig0-home | 启动 | 蓝色主题,swatch[500] = #FF2196F3,平台版本 OpenHarmony-6.1.1.120 |
fig1-red | 点红色 | AppBar 与按钮转红,600 档 #FFDC3C31(修复前是 #FFDC3C6C) |
fig2-deep-purple | 点深紫 | 主题换色,色阶整体偏紫 |
fig3-teal | 点青色 | 600–900 逐渐变深,无回绕 |
fig4-yellow | 点黄色 | 浅色基色下 AppBar 标题自动转深色,色阶标签全程可读 |
fig5-grey | 点灰色 | 600–900 全为中性灰(#8E8E8E/#7E7E7E/#6F6F6F/#5F5F5F),这是修复生效的关键证据 |
fig6-swatch-scroll | 滚动到底 | 十档色值 + swatch[600] 按钮与 primarySwatch 文字 按钮 |
fig7-indigo | 点靛蓝 | 900 档到 #FF26316D,深色段无异常 |
8.6 真机数据汇总
修复后四个基色的深色档(真机色条上的值,与纯 Dart 复算逐位一致):
| 基色 | 500 | 600 | 700 | 800 | 900 |
|---|---|---|---|---|---|
#2196F3 蓝 | #2196F3 | #1E87DB | #1A78C2 | #1769AA | #145A92 |
#9E9E9E 灰 | #9E9E9E | #8E8E8E | #7E7E7E | #6F6F6F | #5F5F5F |
#F44336 红 | #F44336 | #DC3C31 | #C3362B | #AB2F26 | #922820 |
#009688 青 | #009688 | #00877A | #00786D | #00695F | #005A52 |
浅色档同理(50–400 段),例如蓝色为 #85C5F8/#7AC0F8/#64B6F7/#4DABF5/#37A0F4——这一段上游本来就是对的,修复未改动。
flutter test 的真实结果:
00:00 +0 -1: loading .../test/set_color_test.dart [E]
Failed to load "...": Connection closed before test suite loaded.
00:00 +0 -3: Some tests failed.
三个测试文件都没能加载,属于本机工具链问题(flutter_tester 起不来),不是用例失败。所以本文的结论来自「真机 + 纯 Dart 复算」两条独立路径,而不是单元测试通过。
九、FAQ:适配过程与使用问题
9.1 纯 Dart 的库为什么还要写 ohos 插件
因为 pubspec.yaml 一旦声明 plugin.platforms.ohos,工具链就会去找 ArkTS 实现并注册。这个库本身不需要原生能力,但模板结构在,注册壳能验证「引擎认得这个包」;未来若加原生能力(例如读系统色),通道已经就位。
9.2 为什么灰色 600 档会是橄榄色
上游蓝通道括号错位,ds < 0 时取到 b 本身,通道变成 b + b:158 + 158 = 316,8 位截断成 60 (0x3C),于是 #8E8E8E 变成 #8E8E3C。修复后恢复为 158 + round(158 × -0.1) = 142 = 0x8E。
9.3 为什么 500 档必须等于基色
ds = 0.5 - 0.5 = 0,三条插值公式都退化为 c + 0。这也是唯一的硬性质:如果 swatch[500] != base,说明公式或精度出了问题。
9.4 生成的色阶和 Material 官方色板不一致,正常吗
正常。这是线性插值得到的近似色阶(例如蓝色 600 档 #1E87DB,官方是 #1E88E5)。用于 primarySwatch 够用;要按品牌色规范对外输出色值,别用它。
9.5 那 4 条 Color.red is deprecated 要不要改
可以改,但不该混在适配提交里。改成 .r/.g/.b(0–1 的 double)会改变代码形状,让「适配 diff」和「API 迁移」纠缠在一起。本次的处理是保留上游写法,在 README 的「已知问题」里写清楚。
9.6 hdc install 报 Error opening file: no such file or directory
目标路径不能写 Windows 绝对路径。先 cd 到 HAP 所在目录,再 hdc install -r entry-default-signed.hap。
9.7 构建报 SAFE_DELETE_BULK_CONFIRM_REQUIRED
WorkBuddy 注入的 NODE_OPTIONS(safe-delete shim)拦截了 hvigor 的批量删缓存。构建前 $env:NODE_OPTIONS="" 即可;COMPILE RESULT:FAIL 容易误判成 ArkTS 语法错误。
9.8 构建命令退出码是 1,但 HAP 生成了,算成功吗
算。本机 flutter build hap 内部会调 wmic.exe / reg.exe,被安全策略拦截后进程以 1 退出,但 ✓ Built …hap 已经打印、产物时间戳也更新。判断依据是这一行 + 产物,不是退出码。
9.9 flutter test / flutter test integration_test 一直起不来
本机 flutter_tester 报 Connection closed before test suite loaded.,属于工具链问题。替代方案:把纯逻辑抽出来用 dart run 复算(本文字阶数据就是这么得到的),再在真机界面/日志上核对。文章里如实写「未执行」,不写成「测试通过」。
9.10 为什么演示 App 的 bundleName 是 com.example.color_system_example
宿主工程模板沿用这个 bundle 名,多个示例共用。同设备安装另一个示例会覆盖它,验证时若界面不对,先重装确认跑的是哪个包。
相关链接
- 原始仓库:hoiii13/set_primarySwatch_color
- 适配仓库:oh-flutter/set_primarySwatch_color
- 本文参考提交:
d5b4edd - 适配 TAG:
0.0.1-ohos-1.0.0-beta.1
更多推荐




所有评论(0)