开发工具: 华为云码道

DevEco Studio: 26.0.0(26.0.0.851)

原始仓库: hoiii13/set_primarySwatch_color

本文配套仓库: oh-flutter/set_primarySwatch_color

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 这次适配实际做了五件事

  1. 在 pubspec.yaml 的 platforms 里注册 ohos,补 ArkTS 插件壳(getPlatformVersion);
  2. 修掉 上游色阶算法的括号缺陷(第 4 章的核心),并补回归用例;
  3. 修掉 上游模板生成的三个编译不过的测试(引用了不存在的 SetPrimarySwatchColor 类和 MyApp);
  4. 重写 example 演示页:主题随选色真正重建、每档色阶打印十六进制、标签颜色按色块亮度自适应、显示通道返回的平台版本;
  5. 真机上把 4 个基色 × 10 档色值逐个对账,确认修复生效。

其中 2 和 3 是仓库里真实存在的缺陷,不是「为了适配而改」。

二、环境准备

环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。

完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:

flutter --version
flutter doctor -v
hdc list targets

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

编辑用户

2.1 开发套件版本与工程中的 SDK 版本配置

项目版本
Flutter3.47.7-ohos-1.0.0(oh-3.47-dev,framework revision 4b5928c43f)
Dart3.13.3
DevEco Studio26.0.0(26.0.0.851)
OpenHarmony SDK6.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。

代入真机数据,全部对上:

基色 Bb + b8 位截断真机 B现象
#9E9E9E → 158316316 & 0xFF = 60 (0x3C)60 ✓灰变橄榄
#2196F3 → 243486486 & 0xFF = 230 (0xE6)230 ✓蓝偏亮
#F44336 → 54108108(未越界)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 color4 个基色的 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 MaterialColor10 个键都存在且非空

需要如实说明:这四条用例在本机没有执行成功,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.470.0.1-ohos-1.0.0-beta.1main

这两个字段(组织名、分支名)是模板最容易写错的地方:初版写成了 CPF-Flutter 组织和 master 分支,照着文档装会直接拉不到代码。适配时用 git branch --show-current 确认过再写。

6.2 修掉失效的示例与测试(这次的真实缺陷)

上游模板生成的三个测试全部编译不过,flutter analyze 直接报 2 个 error:

文件原写法问题
test/set_primary_swatch_color_test.dartSetPrimarySwatchColor()库里没有这个类,只有 SetMaterialColor
example/integration_test/plugin_integration_test.dartSetPrimarySwatchColor()同上
example/test/widget_test.dartpumpWidget(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 演示页的三块内容与操作

  1. 基色 + 平台版本:左上角色块是当前基色,右边一行 基色 #FF2196F3 → swatch[500] = #FF2196F3,用来验证「500 档等于基色」;下面一行 平台版本: OpenHarmony-6.1.1.120 来自通道。
  2. 19 色色板:一行 6 个圆点,点任意一个都会重建主题,AppBar 与底部按钮同步换色。
  3. 十档色阶 + 两个按钮:色条上带 档位 #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 复算逐位一致):

基色500600700800900
#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 名,多个示例共用。同设备安装另一个示例会覆盖它,验证时若界面不对,先重装确认跑的是哪个包。

相关链接

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐