1. 为什么要在鸿蒙上做配色助手?先聊聊背景和选型
近一年我一直在做 Flutter 跨平台应用,手头同时维护着移动端和桌面端的几个项目。鸿蒙生态设备量涨得飞快,手机、平板、智慧屏、车机都在跑同一个系统,很多客户那边明确提了“能不能上鸿蒙”的需求。但鸿蒙的 App 开发栈和安卓不一样,直接用 Java/Kotlin 那套旧思路开发,适配成本很高。我之前在技术社区看到 Flutter 官方对鸿蒙的适配逐渐成熟,也看到不少团队已经把 Flutter 应用跑到了鸿蒙设备上,加上 Flutter 本身的跨端能力,这是一个值得投入的方向。
这个“配色方案助手”并不是一个很复杂的工具型应用——它要做的事情很朴素:帮设计师和开发者在鸿蒙设备上快速生成、管理、预览配色方案。我之所以选这个项目作为 Flutter 鸿蒙开发的切入点,有几点考虑:
第一,配色工具的业务逻辑非常独立,不依赖系统级 API,也不依赖太多硬件能力。它涉及的核心技术点集中在 UI 渲染、状态管理、颜色空间转换、文件导出、数据持久化这几个层面,这些恰恰是 Flutter 的强项,也是鸿蒙适配中比较成熟的部分。用它来验证 Flutter 在鸿蒙上的表现,干扰项少,问题定位清晰。
第二,鸿蒙生态里的设计师和开发者确实缺一个趁手的取色和配色工具。我自己用过的几个鸿蒙端取色应用,要么是纯 Web 包装,交互动画掉帧明显;要么只做了最简单的色板列表,完全没有颜色关系分析、对比度检查、跨端同步这些能力。用 Flutter 做这个工具,能在保证交互流畅度的同时,快速覆盖多端。
第三,开发周期可控。Flutter 在鸿蒙上的性能表现,和我预期中“跨端框架会有明显性能损失”的刻板印象不太一样,后面我会用具体数据说明。项目从零到完成第一版,大概花了两周业余时间,如果直接走鸿蒙原生开发,这个速度很难达到。
在进入技术细节之前,先把项目的基本架构摆出来。这个项目我采用 Flutter 3.22 版本,配合 OpenHarmony SDK 进行编译适配。引擎层走的是 Flutter 官方对鸿蒙的适配分支,Dart 代码和 UI 层完全复用,只有平台通道、文件读写、系统设置这些和系统交互的模块做了条件编译。项目命名叫 flutter_color_scheme_assistant,内部主要分为四大模块:
- 色板生成引擎:负责从基础色出发,按不同配色规则生成整套色板。
- 取色与调色面板:支持从图片取色、手动调整 HSV/HSL 值、微调透明度。
- 对比度检查工具:基于 WCAG 规范计算文本和背景的对比度,给出可达性评级。
- 方案管理模块:本地保存配色方案,支持导出为 JSON、CSS 变量、Android XML、Flutter 主题代码。
业务功能不复杂,但“Flutter + 鸿蒙”这套组合在工程落地上有不少值得记录的细节。我会按实际开发顺序,把从环境搭建、核心实现到性能调优的完整过程拆开讲,其中会重点说几个我踩过的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建阶段踩过的坑:VS Code toolchain 报错与 SDK 对齐
环境搭建是第一个让我头疼的地方。网上关于 Flutter 鸿蒙开发的教程不少,但很多只写了“安装 OpenHarmony SDK,配置 Flutter 分支”,实际操作起来,版本对齐才是最大的坑。
2.1 先说那个著名的 VS Code 报错
社群和热搜里经常有人反馈一个问题:在 VS Code 里创建 Flutter 项目,跑安卓构建时提示 unable to find suitable visual studio toolchain。这个报错我在 Windows 环境首次搭建时也撞上了。当时我一度很困惑,因为整个项目是纯 Dart 代码,怎么会牵扯到 Visual Studio?
后来查完定位清楚了,这就是 Flutter 的 Gradle 插件在解析本地构建环境时,把 Java 工具链、C++ 工具链和 Windows SDK 环境做了一个综合探测。VS Code 虽然只是个编辑器,但 Flutter 通过 Gradle 执行原生构建时,会去检查系统里有没有 Windows 的 C++ Build Tools。如果电脑上没装 Visual Studio 或者 Windows SDK,就会出现这个错误。
解决办法不复杂,打开 Visual Studio Installer,安装“使用 C++ 的桌面开发”工作负载,里面包含 MSVC 编译器、Windows SDK 和 CMake 工具。装完之后重启 VS Code,再次触发构建,这个报错就会消失。
注意:如果你不打算在 Windows 上构建安卓目标,只在鸿蒙设备上调试,这个坑可以绕开。但 Flutter 的 Gradle 插件默认配置会对所有已启用的平台做检查,最省事的做法还是把 C++ Build Tools 装上,一劳永逸。
2.2 Flutter 与鸿蒙 SDK 的版本对齐
环境问题真正的难点不是 VS Code,而是 Flutter 版本和 OpenHarmony SDK 版本的配对关系。Flutter 的鸿蒙适配是通过 OpenHarmony 的 SIG 社区分支在维护,它并不是随 Flutter 主仓同步发布的,而是基于特定 Flutter 版本做定制。
我建的项目的适配版本组合是:
| 组件 | 版本 |
|---|---|
| Flutter | 3.22.0 |
| OpenHarmony SDK | 5.0.0 (API 12) |
| DevEco Studio | 5.0.0 Release |
| ohos 插件 | 1.1.1 |
这个组合我实测下来是能跑通的。如果 Flutter 版本和 ohos 插件不匹配,常见的表现是:flutter build hap 能执行,但生成 HAP 包后在鸿蒙设备上安装失败,日志里报 signature or hap error 或者 install failed due to incompatible target sdk。这类问题排查起来非常耗时间,因为日志并不会直接告诉你“SDK 版本不匹配”,而是以运行时错误的形式出现。
版本对齐也影响 Flutter 引擎的运行时表现。比如 API 12 之前的鸿蒙版本在 I/O 吞吐和图形合成上对 Flutter 的 SVG 渲染支持不完整,动画较多时容易出现纹理丢失。升级到 API 12 之后,这类问题的出现频率明显降低。
另外一个容易忽略的点是 OpenHarmony SDK 的下载来源。SDK 分为 Public SDK 和 Full SDK,在这里建议直接用 DevEco Studio 自带的 SDK Manager 下载,不要手工从百度和华为开发者网站上下载压缩包。手工下载的 SDK 经常缺少 ets 和 toolchains 目录,配置时会导致构建脚本找不到 hvigorw。
2.3 FVM 管理多版本 Flutter 的必要性
我在同一台机器上维护着三个 Flutter 项目,分别对应不同 Flutter 版本。如果直接使用系统全局 Flutter,每次切换项目都要重新下载对应版本的 SDK,非常浪费时间。最终决定使用 FVM(Flutter Version Management)做多版本管理。
FVM 的基本操作很简单:
bash复制# 安装 FVM
dart pub global activate fvm
# 设定项目所需 Flutter 版本
fvm use 3.22.0
# 查看当前项目使用的 Flutter 版本
fvm flutter --version
项目目录下会生成 .fvmrc 文件,里面记录的是目标版本号。团队协作时每个人 clone 代码后执行 fvm install 就能自动拉取对应版本的 Flutter,避免“在我机器上能跑”这种尴尬。
使用 FVM 之后还要注意 IDE 的 Dart SDK 路径配置。VS Code 里需要在 .vscode/settings.json 中指定:
json复制{
"dart.flutterSdkPath": ".fvm/flutter_sdk",
"dart.lineLength": 100
}
这个配置的目的是让 VS Code 的 Dart 插件使用项目本地 Flutter SDK 而不是全局 SDK,否则代码提示和断点调试会指向错误版本。
3. 配色方案助手的核心功能:算法、数据结构与跨端 UI 适配
环境跑通之后,接下来是真正的开发工作。这个项目的核心不是调用某个现成的配色 API,而是自己实现一套配色逻辑,并把这套逻辑和 Flutter 的响应式 UI 无缝衔接起来。这一节我会详细讲配色算法的实现、数据模型设计、取色面板的关键技术点,以及鸿蒙多端适配的注意事项。
3.1 配色算法:HSL 色彩空间的数学基础
配色生成引擎的核心是色彩空间转换。我在实现时没有直接用 Flutter 的 Color 类提供的 computeLuminance 之类的方法,而是自己实现了一套 RGB 到 HSL 的转换逻辑。原因很简单:配色规则的制定(互补色、类似色、三角色)全部是基于色相(Hue)的偏移计算的,而 RGB 空间做这种偏移效果很差。比如你要生成某个颜色的互补色,RGB 模式下没法通过简单的 r = 255 - r 得到,效果会非常奇怪,但 HSL 模式下只需要把色相值调整 180 度。
RGB 到 HSL 的转换逻辑如下(Dart 实现):
dart复制({double h, double s, double l}) rgbToHsl(int r, int g, int b) {
final rn = r / 255.0;
final gn = g / 255.0;
final bn = b / 255.0;
final maxV = [rn, gn, bn].reduce((a, b) => a > b ? a : b);
final minV = [rn, gn, bn].reduce((a, b) => a < b ? a : b);
final delta = maxV - minV;
double h = 0;
if (delta != 0) {
if (maxV == rn) {
h = 60 * (((gn - bn) / delta) % 6);
} else if (maxV == gn) {
h = 60 * (((bn - rn) / delta) + 2);
} else {
h = 60 * (((rn - gn) / delta) + 4);
}
}
final l = (maxV + minV) / 2;
final s = delta == 0 ? 0 : delta / (1 - (2 * l - 1).abs());
return (h: h < 0 ? h + 360 : h, s: s, l: l);
}
这个转换是调色面板的基础。用户拖动色相滑块时,实际上是在 h 值上做增减,然后通过 HSL 到 RGB 的逆变换刷新界面。操作延迟要控制在 16ms 以内,否则会明显感觉到 UI 卡顿。我在实测中发现,如果每次滑块变化都重新构建整个色板列表,在低端鸿蒙设备上帧率会掉到 50 帧以下。优化方案是:把滑块所在的色相环区域做局部的 RepaintBoundary,让它独立重绘,不牵连整个页面。
生成配色方案时,我内置了五套算法:
- 互补色:h + 180,形成最大对比。
- 类似色:h ± 30,相邻色彩,过渡柔和。
- 三角色:h、h + 120、h + 240,均匀分布。
- 矩形色:h、h + 60、h + 180、h + 240,适合做仪表盘类 UI。
- 单色调:固定 h,只调整 s 和 l,形成层次感。
其实配色算法并不难,难的是把算法结果合理地在手机上展示出来。色卡列表的滚动性能、不同屏幕尺寸下的间距适配、深浅色模式下的色卡边框区分,这些 UI 层面的问题才是花时间最多的。
3.2 数据模型设计:既面向 UI 也面向导出
配色方案的数据结构需要兼顾 UI 展示和代码导出。我一开始的设计只存了一个颜色列表,后来发现导出 Flutter 主题代码时,没有语义信息很难生成可读性好的代码。比如用户生成了一套以橙色为基色的“类似色方案”,如果数据模型只存 [0xFFE65100, 0xFFFFB300, ...],导出代码时根本不知道哪个是 primaryColor,哪个是 accentColor。
最终的数据模型做了如下设计:
dart复制class ColorSchemeModel {
String id;
String name;
String baseColorHex;
List<SchemeColorItem> colors;
double contrastRatio;
DateTime createdAt;
ColorSchemeModel({
required this.id,
required this.name,
required this.baseColorHex,
required this.colors,
required this.contrastRatio,
required this.createdAt,
});
}
class SchemeColorItem {
String role; // primary / secondary / accent / background / surface
String hexValue;
String description;
}
每个颜色都有自己的“角色”(role),这样 UI 上可以按角色分类展示,导出代码时也能准确对应 Flutter 的 ColorScheme 类字段。
持久化我选择了最简单的方案:JSON 文件存储到应用私有目录,用 path_provider 获取路径。没有引入数据库,因为配色方案数据量很小,几百个方案用 JSON 文件完全够用,引入数据库反而多了一个依赖和一层复杂度。
提取配色方案文件的关键代码:
dart复制Future<File> saveSchemeFile(ColorSchemeModel model) async {
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/schemes/${model.id}.json');
await file.writeAsString(jsonEncode(model.toJson()));
return file;
}
3.3 从图片取色的实现:像素采样与主色彩提取
取色面板里有一个功能广受好评:从图片中提取主题色。这个功能的技术难度不在“能不能取到颜色”,而在“如何高效地采样并得出有代表性的颜色”。
我采用的方法是:
- 读取图片像素数据,等间隔采样,避免遍历所有像素导致内存峰值过高。
- 将采样得到的 RGB 值按色相分组,统计每个分组的像素数量。
- 选取像素数量最多的 5 个分组作为候选主色。
- 对候选主色做饱和度排序,排除灰色系(纯度太低的主色视觉效果弱)。
Flutter 中读取图片像素用 toByteData(format: ui.ImageByteFormat.rawRgba),采样周期设置是每 4 个像素取一个。这个密度在 1080P 图片上已经能保证主色提取足够稳定,同时性能消耗控制在 20ms 以内。
主色提取的 Dart 核心实现:
dart复制List<Color> extractMainColors(ui.Image image) {
final byteData = image.toByteData(format: ui.ImageByteFormat.rawRgba);
final pixels = byteData!.buffer.asUint8List();
// 按 8 个像素间隔采样
final step = 8;
final colorCount = <int, int>{};
for (int i = 0; i < pixels.length; i += step * 4) {
final r = pixels[i];
final g = pixels[i + 1];
final b = pixels[i + 2];
// 量化到 /32 * 32,减少色相分组数
final key = (r ~/ 32 << 10) | (g ~/ 32 << 5) | (b ~/ 32);
colorCount[key] = (colorCount[key] ?? 0) + 1;
}
// 按计数排序
final entries = colorCount.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
// 取前 5 个非灰色
final result = <Color>[];
for (final entry in entries) {
final r = ((entry.key >> 10) & 31) * 32;
final g = ((entry.key >> 5) & 31) * 32;
final b = (entry.key & 31) * 32;
if ((r - g).abs() < 30 && (g - b).abs() < 30 && (r - b).abs() < 30) {
continue; // 灰色系跳过
}
result.add(Color.fromARGB(255, r, g, b));
if (result.length >= 5) break;
}
return result;
}
3.4 对比度检查:不是你想象中那么简单
对比度检查模块是设计师的高频功能,它按照 WCAG 2.0 的规范计算前景色和背景色的对比度比值,并给出 AA、AAA 或 Fail 的评级。
公式本身不复杂,关键在“相对亮度”的计算。WCAG 规范里相对亮度不是简单地把 RGB 值套一个加权公式,而是一个分段函数,需要对每个颜色通道做非线性变换:
dart复制double getLuminance(Color color) {
double channelToLinear(double c) {
return c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4).toDouble();
}
final r = channelToLinear(color.r / 255);
final g = channelToLinear(color.g / 255);
final b = channelToLinear(color.b / 255);
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
double contrastRatio(Color a, Color b) {
final la = getLuminance(a);
final lb = getLuminance(b);
final lighter = max(la, lb);
final darker = min(la, lb);
return (lighter + 0.05) / (darker + 0.05);
}
之所以要强调这一点,是因为我见过很多开发者在做“文字颜色自动黑白切换”功能时,直接判断背景颜色的亮度值大于 128 就认为“应该用黑字”,小于 128 就“应该用白字”。这个逻辑在彩色背景上经常出错。举例:纯蓝色(#0000FF)的 RGB 亮度是 29,按亮度直觉应该用白字,但实际白色文字在蓝色上的对比度只有 2.44,远远达不到 WCAG 的 AA 标准。而黄色(#FFFF00)的亮度是 226,黑色文字的对比度高达 19.5。所以,依赖 WCAG 计算而不是直觉判断,是工具类应用必须做对的事情。
3.5 鸿蒙不同设备形态下的 UI 适配
鸿蒙设备不像安卓手机那么单一,它有折叠屏、平板、智慧屏和车机。我的应用在设计时尤其注意了窗口宽度变化的场景。Flutter 的 LayoutBuilder 可以用来做响应式判断,当宽度小于 600dp 时使用单栏布局,600dp 到 840dp 之间使用双栏(左侧色板列表,右侧调色面板),超过 840dp 则使用三栏结构。
适配过程中有一个细节值得提:平板和折叠屏上,系统的安全区域(SafeArea)处理方式和手机不一样。鸿蒙的折叠屏在展开状态下,屏幕中间有铰链区域,如果不避开中缝,UI 元素会显示在铰链下方。Flutter 的 SafeArea 组件默认只处理系统状态栏和底部导航栏,不会自动避开铰链。解决方案是使用 window.displayCutout 参数获取切割区域,或者干脆在布局中预留对称的安全边距。
代码层面的处理:
dart复制LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
if (width < 600) {
return _buildPhoneLayout();
} else if (width < 840) {
return _buildTabletLayout();
} else {
return _buildDesktopLayout();
}
},
)
这种响应式适配在整个项目里花了大量时间调试。测试的方式是在 DevEco Studio 的 Previewer 中切换不同的设备类型,以及在真机上通过折叠和展开屏幕观察布局变化。
4. 鸿蒙适配中的性能与内存优化:从卡顿到流畅的排障过程
开发初期,应用在我的测试平板(麒麟 990 芯片)上运行还算流畅,但换到相对低端的鸿蒙手机后,色板列表快速滚动时出现了明显的卡顿。这一节我会把排查过程完整记录下来,各位做 Flutter 鸿蒙开发时遇到性能问题可以直接对照排查。
4.1 问题定位:是谁拖慢了帧率
通过 Flutter DevTools 的 Performance Overlay 观察,快速滚动时 UI 线程的帧耗时从 8ms 飙到了 34ms,栅格化线程倒是正常。UI 线程耗时集中在两个方法里:ColorSchemeCard.build 和 Image.asset 的解码流程。
问题根源是色卡卡片里的每个颜色块都是一个 Container,每个 Container 设置了自己的 BoxDecoration 和圆角 borderRadius,从而导致每张卡片都有多层视图+绘制指令。而且这些卡片在列表项 build 中还会重新计算颜色关系,显然不应该每帧都做。
4.2 优化手段:RepaintBoundary 与 shouldRepaint 控制
第一轮优化给列表项加了 RepaintBoundary,让卡片在滚动时不需要重复重绘。这个方法立竿见影,帧耗时降到了 12ms 左右。但 12ms 在低端设备上仍有一些波动。
第二轮优化针对解耦计算:把配色方案生成计算从 build 方法挪到初始化时。用户选择一个基础色后,立即调用一次配色算法,把结果缓存到 ColorSchemeModel 中,build 时只做数据展示,不再执行任何色相偏移计算。同时给 Model 增加了 equals 判断,每次 setState 前检查数据是否真的变化,避免无意义重建。
第三轮优化涉及图片解码。取色功能从图库选择一张大图后,Flutter 默认会把原图完整解码到内存,4K 图片在这个环节会吃掉 30 多 MB 内存,对低端设备压力很大。解决办法是先用 decodeImageFromList 配合缩略图尺寸做一次预解码,拿到 ui.Image 后再按需求取色。
优化前后的帧耗时对比:
| 场景 | 优化前 UI 帧耗时 | 优化后 UI 帧耗时 |
|---|---|---|
| 色板列表快速滚动 | 34ms | 9ms |
| 调色滑块拖动 | 22ms | 6ms |
| 取色主页面加载 | 45ms | 18ms |
4.3 内存优化:Flutter 在鸿蒙上的内存管理差异
Flutter 在鸿蒙上有一个和安卓不一样的地方:导航栈的管理。安卓端 Flutter 路由默认是懒加载,路由页面在退栈后会被回收;而鸿蒙端 OpenHarmony 的 Flutter 适配有一个阶段对 flutter_assets 里缓存的图片资源回收不彻底,导致页面频繁跳转后内存只增不减。
我在测试中绕过的方案是:
- 对于可选的大尺寸图片资源,使用
flutter_lottie加载网络资源时缓存策略设置为 LRU,避免无限缓存。 - 对于页面级状态,使用
AutomaticKeepAliveClientMixin配合PageStorageKey保证页面退出后不会重建大量资源。 - 主动调用
debugMemoryAllocations工具检测路径跳转后的内存水位。
内存泄漏还有一个更隐蔽的来源:动画控制器没有销毁。如果你在 State.dispose() 中没有调用 AnimationController.dispose(),重复进入页面必然导致内存上涨。在用 DevTools 的内存快照对比后,我发现 Flutter 在鸿蒙上的泄漏检测结果比安卓更严格,会把未释放的 ImageStreamCompleter 直接标记为泄漏,最终导致应用在长时间使用后被系统杀掉,所以严格释放资源非常关键。
4.4 用 Isolate 做后台取色计算
图片主色提取在 4K 图上如果做全像素遍历,耗时能到 700ms,这期间 UI 完全冻结,体验极差。Flutter 在鸿蒙上同样支持 compute() 函数把任务分发到后台 isolate,我在取色模块用了这个方案。
dart复制final extractedColors = await compute(extractMainColors, image);
这里有一个使用陷阱:传给 compute() 的参数必须是顶级函数或静态方法,不能是实例方法。我在第一次实现时把 extractMainColors 定义成了 _ColorExtractor 类的实例方法,编译不报错,但运行时直接崩掉,提示找不到入口函数。这个错误在调试器里定位也很隐蔽,因为它不显示堆栈,只说 invalid isolate spawn。
对鸿蒙的 isolate 支持,我的实测结论是:耗时任务可以放心用,切换成本在 1-2ms 左右,对 UI 线程基本无感。但不要频繁创建 isolate,尽量复用,因为每创建一个新线程,内存占用都有额外开销。
5. 真机验证与发布前检查:最容易忽略的几个细节
开发完成只是第一步,真机验证和发布前的检查才是决定应用能不能上架的关键。这一节不会重复大家能搜到的官方文档,而是聚焦几个我发版前踩过、且网上讨论较少的问题。
5.1 HAP 包签名与安装失败的排查
鸿蒙应用最终交付的是 HAP 包,和安卓 APK 签名逻辑类似但不是一回事。用 flutter build hap 构建完之后,默认是 debug 签名的,不能直接安装到非开发者设备上。
如果 Hugo / hvigor 构建时没有配置签名信息,在真机上安装 HAP 会报 install failed due to invalid signature。这个问题的排查路径是:
- 打开 DevEco Studio,进入
File > Project Structure > Signing Configs。 - 勾选
Automatically generate signature,登录华为账号,自动生成调试证书和 Profile。 - 构建完成后,在
build/outputs/hap/release目录下用hdc install安装。
如果不用 DevEco Studio 而是直接用命令行构建,需要在 oh-package.json5 里配置签名文件路径。注意:hdc 命令(HarmonyOS 调试命令)是鸿蒙自带的设备连接工具,相当于 Android 的 adb,参数格式不同,我因为混用 adb 命令折腾了很久,接口名和参数都不一样,这需要单独熟悉。
5.2 网络权限与沙箱目录的差异
如果你的应用需要访问网络(比如从远程服务器拉取色板资源),在鸿蒙上需要显式申请权限。和安卓的 AndroidManifest.xml 不同,鸿蒙的权限声明在 module.json5 文件里:
json5复制{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
}
]
}
}
不声明这个权限,真机上网络请求会静默失败,不会抛异常,也不会有明显的错误提示。我第一次测试时以为代码写错了,花了一个多小时排查,最后才意识到是权限问题。
此外,鸿蒙对应用沙箱目录的管理比安卓更严格。path_provider 返回的 getApplicationDocumentsDirectory() 路径和安卓、iOS 都不一样,调试的时候不能想当然地直接拼路径,要用 Path 包的 join 方法拼接,避免路径分隔符不兼容。
5.3 多语言与深色模式适配
鸿蒙系统用户设置的深色模式对配色应用的影响很大。如果深色模式下你仍然用亮色的背景色卡列表,视觉对比会非常刺眼。我针对深色模式做了专门的适配策略:
- 色卡背景不再使用纯白或纯黑,改用
Colors.grey.shade900和Colors.grey.shade50作为深浅模式的基础底色。 - 色卡边框在深色模式下降级为
Colors.white.withAlpha(30),避免深色背景下亮色边框过于抢眼。 - 对比度检查结果在深色模式下的文字颜色需要自动切换,确保始终满足自身检查标准。
这里用到的核心 API 是 MediaQuery.platformBrightnessOf(context) 和 Theme.of(context).brightness。还有一个容易踩的坑:Flutter 在鸿蒙上PlatformDispatcher.instance.platformBrightness 的获取时机,在框架初始化完成之前拿到的值可能是错的,导致首帧闪烁成错误主题。解决方案是延迟到 WidgetsFlutterBinding.ensureInitialized() 之后再读取。
5.4 版本发布前的功能自检清单
以下是我在每次发版前都会过一遍的检查项,也许可以帮你少走一些弯路:
- 冷启动后快速进入取色页,UI 是否能立即响应,不能有白屏。
- 打开深度模式下再切回普通模式,色板主题是否同步更新。
- 快速连续切换 20 套配色方案,内存增长不能超过 50MB。
- 大图取色过程中,界面不能卡死,应显示 loading 转圈动画。
- 导出 JSON 文件后,重新导入恢复,数据不丢失。
- 横竖屏切换后,色板列表布局不乱。
- 弱网环境下,远程色板加载失败要有明确提示,不能白屏卡死。
- HAP 包签名验证通过,能在未开启开发者模式的真机上安装。
这些检查项有一个共同点:只有真机验证才能暴露问题。模拟器(Previewer)和真机的渲染引擎、放缩逻辑、内存回收策略都有差异,尤其是深色模式切换和横竖屏旋转,模拟器上跑完正常,到真机上翻车的概率依然不小。
写在最后:一些关于这套技术栈的个人体会
整个项目做下来,我最大的感受是:Flutter 在鸿蒙上已经不是“能不能用”的问题,而是“怎么用才顺手”的问题。框架本身的适配度比我想象中好,大部分业务代码可以做到零修改跨端复用。真正需要额外投入精力的,是系统能力接入、签名打包、设备适配这些工程层面的细节,而不是 Flutter 语法或者 Dart 语言。
如果让我给准备做 Flutter 鸿蒙开发的朋友一个建议,那就是开局一定先配好版本环境、跑通最小 Demo 的真机安装链路,再开始写业务代码。环境问题在项目后期再返工,排查成本会呈几何级上升。
配色方案助手这个项目还会继续迭代,我下一步计划加入两个功能:一个是把配色方案生成与鸿蒙的卡片服务(Service Widget)打通,用户可以在桌面卡片上直接预览色板;另一个是加入团队共享配色方案的能力,通过云同步让多端实时协作。这两个方向都需要更深入地调用鸿蒙的分布式能力,如果做成了,我再写一篇完整的经验分享。
