Flutter 插件 ios_color_picker_with_title 鸿蒙适配实战:纯 Dart 底部取色弹层、三面板切换、持久化历史颜色
开发工具: 华为云码道
DevEco Studio:
26.0.0(26.0.0.851)
ios_color_picker_with_title 是一个带标题的 iOS 风格取色器,复刻了系统 UI 的颜色面板:顶部一排 Grid / Spectrum / Sliders 切换,下面是取色区域和不透明度滑杆,底部是当前色块与历史颜色。它同时提供一条 iOS 原生取色器通道,但主体功能是纯 Dart 实现的。本文以 ios_color_picker_with_title 1.0.2 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

插件目前支持 android、ios、linux、macos、windows、web、ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以提交 1681b13 为参考。

一、插件简介与适配目标
取色器是主题设置、便签、画板这类应用的基础组件。ios_color_picker_with_title 的做法是用 Flutter 自己画出一个 iOS 风格的底部弹层,而不是调用系统取色器:弹层用 showModalBottomSheet 打开,三种面板分别用色板网格、HSL 色域和 RGB 分量滑杆实现,选中结果通过回调交给业务页面。
例如,笔记应用可以让用户挑一个背景色,绘图工具可以用它设置画笔颜色,设置页可以用它调整强调色。弹层底部会保留最近用过的颜色,长按其中一个可以删除。
取色有两条路径:一条是纯 Dart 的自定义弹层 showIOSCustomColorPicker,全平台可用;另一条是 showNativeIosColorPicker,通过平台通道调用 iOS 系统的 UIColorPickerViewController,仅 iOS 可用。本次适配的目标是让前者在鸿蒙上完整可用,并让插件本身能被正常注册。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


工程使用的工具链和 SDK 配置如下:
| 项目 | 版本或配置 | 用途 |
|---|---|---|
| Flutter OHOS SDK | 3.47.7-ohos-1.0.0 | Flutter 编译与 OHOS 平台工具链 |
| Flutter 分支 | oh-3.47-dev | oh-flutter 对应开发分支 |
| Dart SDK | 3.13.3 | Dart 语言与包管理环境 |
| DevEco Studio | 26.0.0(26.0.0.851) | 工程 Sync、签名配置与 hvigor 构建 |
| HarmonyOS 开发套件 | 7.0.0(API 26) | 开发套件版本及对应的 API 级别 |
compileSdkVersion | 26.0.0 | 编译时使用的 SDK API |
targetSdkVersion | 26.0.0 | 应用面向的行为版本 |
compatibleSdkVersion | 6.1.1(24) | 当前工程声明的最低兼容版本 |
| 插件版本 | 1.0.2 | pubspec.yaml 中的包版本 |
| 原生语言 | ArkTS | HarmonyOS 插件实现 |
| 插件产物 | HAR | 被应用 entry 模块依赖 |
2.1 开发套件版本与工程中的 SDK 版本配置
7.0.0(API 26) 和 26.0.0 涉及 HarmonyOS 开发套件版本(API 版本)及其底座 OpenHarmony 的版本号体系。在本文工程中,相关版本的含义与配置方式如下:
7.0.0(API 26)表示 HarmonyOS 开发套件版本为7.0.0,对应 API 26。26.0.0是本文 HarmonyOS 应用工程中compileSdkVersion和targetSdkVersion的属性值。6.1.1(24)是本文工程中compatibleSdkVersion的属性值,声明最低兼容 API 24。
对应的 product 配置为:
{
"name": "default",
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS"
}

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 24。模板默认生成的是 compatibleSdkVersion: "26.0.0",装到 API 24 的测试机上会报 9568297 older sdk version,所以这里把它降到了与真机一致。
需要注意,降 compatibleSdkVersion 只影响"能不能装"和"按哪个行为版本运行",编译仍使用本机 DevEco 的 API 26 SDK。这是 hvigor 的默认行为:工程不写 compileSdkVersion 时按当前安装的 SDK 编译。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。
在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yaml、LICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。
color_picker_with_title 现可从 AtomGit 获取。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。
3.2 将代码拉取到宿主机
在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:
git clone https://atomgit.com/oh-flutter/color_picker_with_title.git
cd color_picker_with_title
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD
git clone 会创建 color_picker_with_title/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yaml、lib/ 和 example/。Git 仓库名是 color_picker_with_title,Dart 包名是 ios_color_picker_with_title,两者并不相同,写依赖和 --project-name 时不要混用。
适配后的仓库使用 master 分支,并在适配提交上打了 1.0.2-ohos-1.0.0 标签。需要与本文代码完全一致时,在干净仓库中执行:
git switch --detach 1.0.2-ohos-1.0.0
适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

图 1:在宿主机终端输入 AtomGit 仓库拉取命令。
3.3 在仓库根目录确认适配分支与版本标记
接着在 color_picker_with_title/ 根目录确认分支与版本标记。上游仓库只有一个 master 分支,适配改动直接落在这个分支上,并通过 tag 固定版本。分支名与 tag 的对应关系如下:
git branch --show-current
git tag --list
git log --oneline -3
git remote -v
git status --short --branch
分支策略与本系列其余库保持一致:分支名使用 feat/ohos_库名称_版本号,库名称取 pubspec.yaml 的 name,版本号取此次适配的基线版本,本例为 feat/ohos_ios_color_picker_with_title_1.0.2。适配完成后合并到 master,再在合并后的提交上打 tag:
git switch -c feat/ohos_ios_color_picker_with_title_1.0.2
git tag 1.0.2-ohos-1.0.0
如果该分支已存在,使用 git switch feat/ohos_ios_color_picker_with_title_1.0.2 切换即可。

图 2:在 color_picker_with_title 仓库根目录查看分支、TAG 与远程地址。
3.4 自动补全 OHOS 适配结构
分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件:
flutter create --template=plugin --platforms=ohos --project-name ios_color_picker_with_title .
git status --short
git diff -- pubspec.yaml lib example
--template=plugin指定插件模板。--platforms=ohos指定需要补全的平台。--project-name ios_color_picker_with_title使用 Dart 包名,而不是仓库目录名。- 最后的
.表示在当前插件目录补全工程,不是另建一层目录。
该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yaml、lib/ 和 example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。
这个库在结构补全之外还有两处不得不改的地方。一是 super_tooltip:上游锁定的 2.0.9 在当前 Flutter 上无法编译,它自己的 TooltipPositionDelegate 与 package:flutter/widgets 新导出的同名类冲突;升到 2.1.1 后必须同步改写 lib/custom_picker/history_colors.dart 里的长按删除气泡样式(新版本把平铺参数拆成了配置对象),细节见 4.4。二是 shared_preferences:历史颜色和面板标签都靠它持久化,而 pub.dev 版本没有 OpenHarmony 实现,需要在 example 里显式引入 shared_preferences_ohos。
如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:
cd example
flutter create --platforms=ohos .
cd ..
配套仓库已经包含 ohos/ 和 example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos ios_color_picker_with_title;已有插件使用上面的 . 在当前目录补全。

图 3:在插件根目录输入 OHOS 结构补全命令,并检查依赖与平台声明的 diff。
3.5 适配后的项目目录
适配后的关键目录如下:
color_picker_with_title/
├── lib/
│ ├── show_ios_color_picker.dart
│ ├── custom_picker/
│ │ ├── ios_color_picker.dart
│ │ ├── pickers_selector_row.dart
│ │ ├── history_colors.dart
│ │ ├── color_observer.dart
│ │ ├── shared.dart
│ │ ├── helpers/cache_helper.dart
│ │ └── pickers/
│ └── native_picker/
│ ├── ios_color_picker.dart
│ ├── ios_color_picker_method_channel.dart
│ ├── ios_color_picker_platform_interface.dart
│ └── ios_color_picker_web.dart
├── ohos/
│ ├── index.ets
│ ├── oh-package.json5
│ └── src/main/
│ ├── ets/components/plugin/IosColorPickerWithTitlePlugin.ets
│ └── module.json5
├── example/
│ ├── lib/main.dart
│ └── ohos/
│ └── entry/
├── android/ ios/ linux/ macos/ windows/
└── pubspec.yaml
项目根目录如下,其中包含 ohos/、example/,以及 OpenHarmony 中英文说明和变更记录文件:

图 4:适配后的 color_picker_with_title 项目目录。
| 文件 | 主要职责 |
|---|---|
lib/show_ios_color_picker.dart | 对外入口,声明 IOSColorPickerController 与两条取色路径 |
lib/custom_picker/ios_color_picker.dart | 弹层主体:标题、面板切换、不透明度、当前色块与历史行 |
lib/custom_picker/pickers_selector_row.dart | Grid / Spectrum / Sliders 三个面板的切换与渲染 |
lib/custom_picker/history_colors.dart | 历史颜色分页、长按删除气泡、shared_preferences 读写 |
lib/custom_picker/helpers/cache_helper.dart | SharedPreferences 单例封装 |
lib/native_picker/* | iOS 原生取色器的平台接口与 MethodChannel 实现 |
IosColorPickerWithTitlePlugin.ets | 注册 ios_color_picker 通道,提供 getPlatformVersion |
插件 module.json5 | 声明 HAR 模块及设备类型 |
示例 entry module.json5 | 声明宿主应用 Ability、设备类型和权限场景 |
example/lib/main.dart | 展示预览卡片、两个取色按钮、描边设置和历史颜色 |
四、Dart 接口与通道分析
这个库的 Dart 层同时存在两种实现:纯 Dart 的弹层,和仅 iOS 可用的原生通道。阅读 lib/show_ios_color_picker.dart、lib/native_picker/ 和 lib/custom_picker/ 之后可以确认:业务真正使用的是 showIOSCustomColorPicker,它一路走到底都是 Flutter 自己的 widget,不经过任何原生代码;showNativeIosColorPicker 才会触发通道,并且入口处就有 assert(Platform.isIOS)。因此 OHOS 侧要做的是让插件壳能被注册、让原生通道对未实现的方法明确返回,而不是去桥接一个鸿蒙并不存在的系统取色器。
本例的对应关系如下:
| Dart 入口或模型 | 通道协议 | OHOS 实现 | 应保持的行为 |
|---|---|---|---|
showIOSCustomColorPicker(...) | 无 | 无 | 用 showModalBottomSheet 打开纯 Dart 弹层,选色结果走 onColorChanged |
showNativeIosColorPicker(...) | ios_color_picker / pickColor | result.notImplemented() | 仅 iOS 有效;OHOS 上不调用,Dart 侧已有 Platform.isIOS 断言 |
IosColorPickerPlatform.getPlatformColor(...) | ios_color_picker / pickColor | 同上 | 参数 darkMode、defaultColor、title 原样透传 |
| 原生取色回调 | ios_color_picker_stream 事件流 | 不实现 | 仅 iOS 使用,OHOS 上不订阅 |
原生端需要保持通道名和方法名与 Dart 一致。系统能力不可用时,通过 result.notImplemented() 或错误回调通知 Dart 层,不要静默返回空值。
4.1 跨端架构与调用时序
Flutter 侧和 HarmonyOS 侧之间实际只有一条会被用到的通道,另有一条 iOS 专用的事件流:
MethodChannel('ios_color_picker'):承载pickColor,只有 iOS 原生取色器会调用;EventChannel('ios_color_picker_stream'):iOS 原生取色器把用户拖动中的颜色持续回传;- 纯 Dart 弹层:不经过任何通道,全部在 Flutter 渲染层内完成。
实线部分是鸿蒙上真正执行的路径:IOSColorPickerController 只负责创建 ColorController 并打开弹层,之后所有交互都在 IosColorPicker 内部完成。虚线部分是 iOS 原生取色器,鸿蒙上不会进入。
4.1.1 一次完整选色的时序
面板标签在 dispose 时写回,历史颜色在新增或删除时写回。两条写入都依赖 SharedPreferences,这也是本库在鸿蒙上唯一需要额外补依赖的地方。
4.2 颜色模型:lib/custom_picker/extensions.dart
库内部统一用 Flutter 的 Color 和 HSVColor 表示颜色,序列化交给三个扩展方法:
extension ColorExtensions on Color {
/// Converts a [Color] to a `Map<String, double>`.
Map<String, double> toMap() {
return {
"red": r,
"green": g,
"blue": b,
"alpha": a,
};
}
}
extension MapToColorExtension on Map<Object?, Object?> {
/// Converts a `Map<String, double>` to a [Color], clamping to valid range.
Color toColor({bool clampValues = true}) { ... }
}
extension HexColor on Color {
static Color fromHex(String hexString) { ... }
String toHex({bool leadingHashSign = false}) =>
'${leadingHashSign ? '#' : ''}'
'${red.toRadixString(16).padLeft(2, '0')}'
'${green.toRadixString(16).padLeft(2, '0')}'
'${blue.toRadixString(16).padLeft(2, '0')}';
}
| 表示形式 | 使用位置 | 说明 |
|---|---|---|
Color | 业务回调、当前色块 | 对外统一类型 |
HSVColor | SlidePicker、AreaColorPicker | 色相/饱和度/明度换算与滑杆取值 |
Map<String, double> | ios_color_picker 通道参数 | defaultColor 的原生参数格式,分量为 0–1 的 r/g/b/a |
#AARRGGBB 字符串 | shared_preferences | 历史颜色逐条存为字符串 |
适配时已经把 toMap() 从旧的 red/green/blue/alpha 改成 r/g/b/a,因此它在新 Flutter 上不再报警告;toHex() 里仍是旧的 red/green/blue,flutter analyze 会给出 3 条 deprecated_member_use。连同 slider_helper.dart、slider_picker.dart、utils.dart 里同样的访问器,一共 12 条。这些属于上游实现,保留原样可避免在适配提交里混入与平台无关的重构,也不影响运行。
4.3 公开 API 与平台接口
IOSColorPickerController 是业务唯一需要持有的对象:
class IOSColorPickerController {
Color selectedColor = Colors.green;
Future<void> showNativeIosColorPicker({
required ValueChanged<Color> onColorChanged,
Color? startingColor,
bool? darkMode,
String? title,
}) async { ... }
void showIOSCustomColorPicker({
required BuildContext context,
required ValueChanged<Color> onColorChanged,
Color? startingColor,
}) async { ... }
void cancelColorSubscription() { ... }
void dispose() { ... }
}
各方法的行为如下:
showIOSCustomColorPicker:打开纯 Dart 弹层,返回void,不等待用户选择;选色过程通过onColorChanged持续回调;showNativeIosColorPicker:打开 iOS 原生取色器,入口处断言Platform.isIOS,非 iOS 平台在 debug 模式下直接抛断言错误;selectedColor:记录最近一次选中的颜色,也是下次打开弹层时的起始色;cancelColorSubscription:取消ios_color_picker_stream的事件订阅,只在走过原生路径后才需要;dispose:内部调用cancelColorSubscription,页面销毁时调用一次即可。
IosColorPickerPlatform 定义平台接口,默认实例是 MethodChannelIosColorPicker,测试里可以替换成 mock 实现。
4.4 通道协议与渲染实现分析
4.4.1 通道名称必须与 Dart 完全一致
// lib/native_picker/ios_color_picker_method_channel.dart
final methodChannel = const MethodChannel('ios_color_picker');
// lib/show_ios_color_picker.dart
static const _eventChannel = EventChannel('ios_color_picker_stream');
通道名不能用包名推。模板默认会按插件名生成 ios_color_picker_with_title,而 Dart 实际用的是 ios_color_picker,两者不一致时 ArkTS 侧永远不会被调用。适配时按 Dart 的写法注册,这一点在 IosColorPickerWithTitlePlugin.ets 中已经固定。
4.4.2 pickColor 在 OpenHarmony 上返回 notImplemented
final color = await methodChannel.invokeMethod<Map>('pickColor', map);
if (color == null) return null;
return Map<String, double>.from(color).toColor();
Dart 侧只关心返回值是不是一个颜色 Map。OpenHarmony 标准 SDK 没有等价于 UIColorPickerViewController 的系统取色器组件,硬造一个原生面板既无法与 iOS 语义对齐,也没有必要——纯 Dart 弹层已经覆盖了同样的能力。因此 ArkTS 侧对这个方法统一回 notImplemented,并在代码里写明理由。
4.4.3 长按删除气泡:super_tooltip 2.1.x 的参数映射
history_colors.dart 用 SuperTooltip 做长按删除。2.0.9 无法在当前 Flutter 上编译,因为它自己的 TooltipPositionDelegate 与 package:flutter/widgets 新导出的同名类冲突;升到 2.1.1 后,原来平铺在 SuperTooltip 上的样式参数被拆成了配置对象,因此逐项映射:
barrierConfig: const BarrierConfiguration(show: false, sigmaX: 16, sigmaY: 16),
style: const TooltipStyle(hasShadow: false, bubbleDimensions: EdgeInsets.zero),
arrowConfig: const ArrowConfiguration(length: 8, tipDistance: 17),
positionConfig: const PositionConfiguration(preferredDirection: TooltipDirection.up),
| 旧参数(2.0.x) | 新参数(2.1.x) | 保持的效果 |
|---|---|---|
showBarrier | BarrierConfiguration.show | 长按时不出现遮罩 |
sigmaX / sigmaY | BarrierConfiguration.sigmaX/sigmaY | 遮罩模糊半径 |
hasShadow | TooltipStyle.hasShadow | 气泡不带阴影 |
bubbleDimensions | TooltipStyle.bubbleDimensions | 气泡内边距由内容决定 |
arrowLength / arrowTipDistance | ArrowConfiguration.length/tipDistance | 尖角长度与离目标的距离 |
popupDirection | PositionConfiguration.preferredDirection | 气泡朝向屏幕上方 |
映射后视觉效果与 2.0.x 一致,真机长按历史颜色可以看到相同的 “Delete” 气泡(见 8.6 的 fig5-delete.jpeg)。
4.4.4 历史颜色与面板标签的持久化
// lib/custom_picker/helpers/cache_helper.dart
Future<void> _ensureInitialized() async {
if (!_isInitialized) {
_sharedPreferences = await SharedPreferences.getInstance();
_isInitialized = true;
}
}
| 键 | 类型 | 写入时机 | 读取时机 |
|---|---|---|---|
history_colors | List<String> | 新增、删除历史颜色 | 弹层首次构建 |
selector_index | int | PickersSelectorRow.dispose | 面板切换行 initState |
两者都由 shared_preferences 承担。它的 pub.dev 发行版没有 OpenHarmony 实现,如果没有额外引入 shared_preferences_ohos,SharedPreferences.getInstance() 会抛 MissingPluginException:异常被异步调用吞掉后,界面表现就是历史颜色一行空白、下次打开弹层又回到 Grid 面板。适配时在 example 里补了这个依赖,真机上历史颜色和面板标签都能跨进程保留。
五、补全 OHOS 原生实现与工程配置
5.1 在 IosColorPickerWithTitlePlugin.ets 中补全插件壳
业务侧不直接接触 ArkTS。需要补全的是插件类本身:注册一个名为 ios_color_picker 的 MethodChannel,对 getPlatformVersion 返回系统版本,对其他方法(包括 pickColor)明确返回未实现。
IosColorPickerWithTitlePlugin 实现 FlutterPlugin 和 MethodCallHandler。下面是类中的主要成员和方法,完整文件还包含 getUniqueClassName() 等插件接口。
原生插件位于:
ohos/src/main/ets/components/plugin/IosColorPickerWithTitlePlugin.ets
5.1.1 引入 Flutter 和 HarmonyOS 能力
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
import deviceInfo from '@ohos.deviceInfo';
其中:
FlutterPlugin负责接入 Flutter Engine 生命周期;MethodChannel接收 Dart 发来的命令;MethodCall/MethodResult承载一次调用的参数与结果;deviceInfo提供osFullName,用于拼出版本字符串。
5.1.2 连接 Flutter Engine
private channel: MethodChannel | null = null;
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "ios_color_picker");
this.channel.setMethodCallHandler(this);
}
通道名必须是 ios_color_picker,与 4.4.1 中 Dart 侧的写法逐字一致。改成包名会让 Dart 调用落到"未实现方法"上。
5.1.3 通道方法
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
result.success(`OpenHarmony ${deviceInfo.osFullName}`);
} else {
// pickColor only has a native implementation on iOS; on OpenHarmony
// users should use the pure-Dart custom picker (showIOSCustomColorPicker).
result.notImplemented();
}
} catch (e) {
result.error("IOS_COLOR_PICKER_ERROR", `method ${call.method} failed: ${e}`, null);
}
}
getPlatformVersion 是模板保留的方法,Dart 层没有调用方,保留它是为了让插件在被直接调用通道时仍有可用返回值。pickColor 走 notImplemented,Dart 侧会收到 MissingPluginException;由于 showNativeIosColorPicker 在进入通道之前就有 Platform.isIOS 断言,正常使用时不会走到这一步。
try/catch 把异常统一转成 IOS_COLOR_PICKER_ERROR,避免原生侧的异常把调用方挂住。
5.1.4 Engine 解绑时释放资源
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
}
}
Flutter Engine 销毁时清掉通道 Handler,避免残留引用。
5.2 声明插件和宿主权限
本库不需要任何系统权限:纯 Dart 弹层只做界面绘制,历史颜色通过 shared_preferences 写入应用沙箱,原生侧唯一的通道方法 getPlatformVersion 读取的是系统版本属性。
5.2.1 插件 HAR 的权限
ohos/src/main/module.json5 保持模板内容即可,不需要 requestPermissions:
{
"module": {
"name": "ios_color_picker_with_title",
"type": "har",
"deviceTypes": ["default", "tablet"]
}
}
5.2.2 应用 entry 的权限
example/ohos/entry/src/main/module.json5 里有一条模板自带的 ohos.permission.INTERNET:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
这条声明与本库的取色、历史记录都无关,删掉不影响示例运行。保留时也需要注意:权限声明和运行时授权是两个步骤,module.json5 中的声明不会自动完成授权。
5.3 注册并导出插件
pubspec.yaml 通过以下配置声明 OHOS 插件类:
flutter:
plugin:
platforms:
ohos:
pluginClass: IosColorPickerWithTitlePlugin
插件的 ohos/index.ets 需要导出实现:
import IosColorPickerWithTitlePlugin from './src/main/ets/components/plugin/IosColorPickerWithTitlePlugin';
export default IosColorPickerWithTitlePlugin;
执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码。本例生成的注册文件中包含三个插件:
flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
flutterEngine.getPlugins()?.add(new SharedPreferencesPlugin());
flutterEngine.getPlugins()?.add(new IosColorPickerWithTitlePlugin());
SharedPreferencesPlugin 来自 shared_preferences_ohos。它出现在这里,才说明历史颜色的持久化真的接通了;只看到 IosColorPickerWithTitlePlugin 时,取色功能正常但历史颜色会是空的。通常不应手工编辑 GeneratedPluginRegistrant.ets,因为下次构建可能覆盖它。
注册异常的排查步骤见第九节 MissingPluginException。
5.4 检查 example 的 OHOS 应用结构
本例的 example/ohos/build-profile.json5 应在 products 中设置版本。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料保留在本地。
{
"app": {
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS",
"targetSdkVersion": "26.0.0"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。
本例 AppScope/app.json5 里的 bundleName 沿用了同系列示例的 com.example.color_system_example。签名 Profile 是按包名签发的,改包名必须同时换一份匹配的 Profile,否则 SignHap 会失败;如果要把示例发布成独立应用,记得把这两处一起换掉。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:
| 文件 | 应写清楚的内容 |
|---|---|
README.OpenSource | 上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖 |
README.md | 原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息 |
README.OpenHarmony_CN.md | 简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题 |
README.OpenHarmony.md | 与中文说明对应的英文文档 |
CHANGELOG.OpenHarmony.md | OHOS 新增能力、适配版本、兼容限制与测试范围 |
LICENSE / NOTICE | 保留上游许可证;NOTICE 按许可证和原项目要求保留或补充 |
example/README.md | 依赖方式、运行目录、签名、操作步骤与效果图;覆盖取色、历史颜色与删除 |
pubspec.yaml、ohos/oh-package.json5 | 核对包名、版本、插件注册、仓库地址、许可证和依赖 |
.gitignore | 忽略构建缓存及本机签名材料,不漏提交必要源码和配置 |
README.OpenSource 记录库本身的来源与版本。本例的包名为 ios_color_picker_with_title,版本为 1.0.2,采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。两份 README.OpenHarmony 里的依赖地址、TAG 名称和环境版本要与最终提交保持一致,CHANGELOG.OpenHarmony.md 需要同时记录 shared_preferences_ohos 这一项额外依赖。
6.2 提交前检查
提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:
git branch --show-current
git diff --check
git status --short
git diff --stat
git diff
检查 diff 中的接口、平台注册和依赖变化,移除本机路径及签名信息,并使文档中的版本和分支与提交内容一致。
6.3 提交并推送到 AtomGit
文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:
git add ohos pubspec.yaml example test .gitignore
git add README.md README.OpenSource README.OpenHarmony_CN.md
git add README.OpenHarmony.md CHANGELOG.OpenHarmony.md
git diff --cached --check
git diff --cached --stat
git diff --cached
git commit -m "feat: add OHOS support for ios_color_picker_with_title 1.0.2"
git remote -v
git branch --show-current
git push -u origin feat/ohos_ios_color_picker_with_title_1.0.2
本次适配实际拆成了三个提交:0895090 补 ohos 结构与插件壳、升级 super_tooltip 并重写示例页,5a5636e 把 compatibleSdkVersion 对齐到真机的 6.1.1(24),1681b13 给 example 加上 shared_preferences_ohos 并校正文档里的仓库地址与 TAG 名称。TAG 1.0.2-ohos-1.0.0 打在 1681b13 上。
DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 feat/ohos_ios_color_picker_with_title_1.0.2。
推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围、依赖及权限、测试环境、操作结果、已知限制,并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。
七、使用根目录 example 演示接入
仓库自带 example/,可以直接用来调试插件和体验取色效果。
7.1 本地适配时使用路径依赖
当前 example/pubspec.yaml 的依赖是:
dependencies:
flutter:
sdk: flutter
ios_color_picker_with_title:
path: ../
shared_preferences_ohos:
git:
url: https://atomgit.com/openharmony-sig/flutter_packages.git
path: packages/shared_preferences/shared_preferences_ohos
../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。shared_preferences_ohos 是本库特有的补充项,它声明自己实现 shared_preferences,只在 ohos 平台生效,在其他平台上是被忽略的空实现。
从插件根目录执行:
cd example
flutter pub get
flutter pub deps
检查 example/pubspec.lock 中是否出现 shared_preferences_ohos,来源为 git,resolved-ref 指向 flutter_packages 的某个提交。
7.2 通过 AtomGit 引入插件
业务应用通过 AtomGit 引入时,将 ios_color_picker_with_title 的 path 配置替换为下面的 Git 依赖。这里固定到本文使用的 TAG:
dependencies:
flutter:
sdk: flutter
ios_color_picker_with_title:
git:
url: https://atomgit.com/oh-flutter/color_picker_with_title.git
ref: 1.0.2-ohos-1.0.0
注意 url 里的仓库名是 color_picker_with_title,而 dependencies 的键是 Dart 包名 ios_color_picker_with_title;两者不一致时以包名为准,仓库名只影响 URL。
使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_ios_color_picker_with_title_1.0.2。正式发布后可固定到 tag 或 commit。
使用取色器历史的项目还要记得按 7.1 的方式补上 shared_preferences_ohos,它属于宿主应用的依赖,不会随插件自动带入。
7.3 调用接口实现取色
下面的页面展示一个取色按钮和一个颜色预览块,可用于 example/lib/main.dart。仓库中的完整 Demo 还提供描边宽度、描边颜色、预览开关和历史颜色。
import 'package:flutter/material.dart';
import 'package:ios_color_picker_with_title/show_ios_color_picker.dart';
void main() {
runApp(const MaterialApp(home: ColorPickerPage()));
}
class ColorPickerPage extends StatefulWidget {
const ColorPickerPage({super.key});
State<ColorPickerPage> createState() => _ColorPickerPageState();
}
class _ColorPickerPageState extends State<ColorPickerPage> {
final IOSColorPickerController _controller = IOSColorPickerController();
Color _selected = Colors.green;
void _pickColor() {
_controller.showIOSCustomColorPicker(
context: context,
startingColor: _selected,
onColorChanged: (Color color) {
if (!mounted) return;
setState(() => _selected = color);
},
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('iOS 风格取色器')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 160,
height: 160,
decoration: BoxDecoration(
color: _selected,
borderRadius: BorderRadius.circular(12),
),
),
const SizedBox(height: 12),
Text(_selected.toARGB32().toRadixString(16).toUpperCase()),
const SizedBox(height: 24),
FilledButton(
onPressed: _pickColor,
child: const Text('打开取色器'),
),
],
),
),
);
}
}
startingColor 传当前颜色,用户在弹层里拖动时不会被重置;onColorChanged 会随拖动连续触发,因此 setState 要加 mounted 判断。
7.4 页面退出时的处理
IOSColorPickerController 内部持有 EventChannel 的订阅字段,原生路径用过之后必须取消。纯 Dart 路径不会创建订阅,但统一在 dispose 中调用 _controller.dispose() 是最省心的写法,两种情况都覆盖。
多个页面都需要同一份取色状态时,可以让应用级服务持有 selectedColor,页面只负责打开弹层并回传结果,避免每个页面各自维护一份控制器。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
从插件仓库根目录执行:
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
本机实测结果:
- 插件
flutter analyze:14 条提示,全部为 info/warning,没有 error。其中 13 条是deprecated_member_use——lib/custom_picker/extensions.dart、utils.dart、pickers/slider_picker/slider_helper.dart、pickers/slider_picker/slider_picker.dart各 3 条,用的是color.red/green/blue/value这批已废弃的访问器;另有 1 条在example/lib/main.dart:97,同样是backgroundColor.value。剩下 1 条是test/ios_color_picker_method_channel_test.dart里未使用的局部变量platform。 - example 单独执行
flutter analyze:1 条提示,即上面那处value,建议改用toARGB32()。 flutter test:本机工具链无法完成。flutter_tester启动后直接挂住(此前其他库报Invalid SDK hash),因此这两个用例本次未执行。仓库里的test/目录保留原样:ios_color_picker_method_channel_test.dart中的断言被上游注释掉了,ios_color_picker_test.dart用MockIosColorPickerPlatform验证getPlatformColor的参数透传,属于接口层测试,与 OHOS 行为无关。
Dart 测试覆盖接口和页面逻辑,取色面板的真实交互还需要在鸿蒙设备上验证,这也是第八节其他小节的主要内容。
8.2 确认设备连接
hdc list targets
flutter devices
设备首次连接电脑时,需要在手机端确认调试授权。列表为空时,检查 USB 连接、调试模式和电脑授权。
8.3 配置签名
真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:
- 用 DevEco Studio 打开
example/ohos,不是仓库根目录; - 等待工程 Sync 成功,确认 Project 视图中存在
entry模块; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 选择或生成签名; - 确认设备、应用包名、证书和 Profile 匹配;
- 再回到终端执行 Flutter 构建或运行。
签名材料保存在本机,公开仓库中只保留构建所需的通用配置。
8.4 运行示例
以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:
flutter run -d <device-id>
也可以先构建 HAP:
flutter build hap --debug
典型产物位于:
example/ohos/entry/build/default/outputs/default/
目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。
本次适配时,flutter build hap --debug 在本机停在 hvigor 的 ohpm install 阶段,报 00306053 Specification Limit Violation 和 BATCH RECURSION exceeds STACK limits。这种情况下可以先手动装依赖,再直接调 hvigor:
cd example/ohos
"D:/Program Files/Huawei/DevEco Studio/tools/ohpm/bin/ohpm" install
然后在 PowerShell 中补上 JAVA_HOME 再执行(路径含空格,用 Bash 直接调 .bat 容易失败):
$env:JAVA_HOME="D:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="$env:JAVA_HOME\bin;" + $env:PATH
cd D:\flutter\lib3\ios_color_picker_with_title\example\ohos
& "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" assembleHap -p product=default -p buildMode=debug --no-daemon
完成后产物出现在同一路径下,用 hdc install 安装即可:
hdc -t <device-id> install -r example/ohos/entry/build/default/outputs/default/entry-default-signed.hap
这条绕行方式是为了绕开本机 ohpm 的环境问题,正式发布仍应走 flutter build hap。
8.5 在设备上测试取色
- 打开应用,确认首页显示预览卡片、当前色值与"历史颜色:暂无记录";
- 点击"Custom picker (all platforms)",确认弹出底部取色面板,标题为 Colors;
- 在 Grid 面板点一个色块,确认背后首页的背景色同步变化;
- 切到 Spectrum,在色域里拖动,确认颜色连续变化;
- 切到 Sliders,拖动 RED / GREEN / BLUE 三个滑杆,确认色值和数值同步;再把 OPACITY 拖到 40%,确认底部色块出现透明网格;
- 点历史行末尾的"+",确认当前颜色被加入历史;
- 长按历史里任意一个颜色,确认弹出 Delete 气泡;
- 点右上角关闭,确认首页使用最终颜色,历史行出现刚选过的颜色;
- 杀掉应用重新打开,再进入取色器,确认仍停留在上次的面板,历史颜色也还在。
本文已在真机完成第 1–9 步(见 8.6,设备 TLR-AL00 / API 24)。其中第 5 步会看到 Sliders 面板底部的黄黑溢出提示条,属布局计算问题,说明见 9.11。
8.6 鸿蒙设备运行效果
完成适配后,showIOSCustomColorPicker 在鸿蒙设备上可以正常打开、选色并回传给业务页面;三种面板、不透明度、历史颜色和长按删除都可用,历史颜色与面板标签在应用重启后仍然保留。
首页与三种面板:
| 示例首页 | Grid 面板 | Spectrum 面板 |
|---|---|---|
| 预览卡片、取色按钮、描边设置,历史为空 | 12×10 色板网格,底部一行默认历史色 | HSL 色域,圆环标记当前颜色 |
Sliders 面板与历史颜色操作:
| Sliders 面板 | 加入历史颜色 | 长按删除 |
|---|---|---|
| 三个分量滑杆,不透明度拖到 40%,底部为 24 像素溢出提示 | 点"+"把当前色写入 history_colors | super_tooltip 气泡显示 Delete |
选色结果与持久化:
| 选色后的首页 | 重启后重新打开 |
|---|---|
背景色变为 FFB92D5D,历史行出现两条记录 | 直接停在 Sliders 面板,历史颜色仍在 |
Sliders 面板底部的黄黑条纹是 Flutter 的溢出提示:弹层高度按 340 + componentsHeight(context) 写死,三个分量滑杆加不透明度行在该机 361vp 的逻辑宽度下超出约 24 像素。这是上游的布局计算问题,与平台无关,Release 包不显示提示条,功能也不受影响;需要彻底解决时按 9.11 调整高度计算。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
典型错误如下:
Missing SDK components. SDK path: ...,
missing components: toolchains,ets,js,native,previewer.
这个错误发生在 Hvigor 同步阶段。通常需要检查构建工具使用的 SDK 路径、组件是否完整,以及 Hvigor 与 SDK 的版本是否匹配。
处理顺序:
- 在 DevEco Studio SDK Manager 中确认 API 26 组件已经下载完整;
- 检查 Flutter 和 DevEco Studio 使用的 SDK 路径是否一致;
- 避免误用
/Applications/DevEco-Studio.app/Contents/sdk之类的不完整目录; - 确认 SDK 根目录下存在
toolchains、ets、js、native、previewer; - 执行
flutter config --ohos-sdk <正确路径>; - 重新执行
flutter doctor -v和 DevEco Studio Sync。
因为 Sync 和 Compile 首先读取本机 SDK。手机 API 版本只在部署、安装和运行时参与兼容判断。即使完全不连接手机,本地 SDK 不完整时也会得到相同错误。
当前工程的 compatibleSdkVersion 是 6.1.1(24),因此 API 24 设备在安装版本门槛上是满足的;取色器不依赖任何设备专属能力,装上即可使用。
9.2 DevEco Studio 中看不到 entry 模块
插件的 ohos/ 目录是 HAR 模块,可安装应用的 entry 模块位于 example/ohos/entry。
请直接使用 DevEco Studio 打开:
color_picker_with_title/example/ohos
如果仍看不到 entry,先解决 SDK Sync 错误,再检查 example/ohos/build-profile.json5 的 modules 是否包含 ./entry。同步失败时,Project Structure 无法正确解析模块,签名界面也可能不显示 entry。
9.3 无法手动签名
签名配置依附于可构建的应用模块和 product。只有 HAR 插件模块、工程 Sync 失败,或者打开了错误目录时,DevEco Studio 都可能无法提供 entry 签名入口。
建议先确认:
- 打开的是
example/ohos; - SDK 组件完整并且 Sync 成功;
entry的模块类型为entry;defaultproduct 和 target 已正确关联;- 当前账号、证书和调试设备状态有效。
9.4 点击取色按钮没有弹出面板
按以下顺序检查:
- 是否调用的是
showIOSCustomColorPicker,而不是showNativeIosColorPicker——后者在鸿蒙上会命中assert(Platform.isIOS); showIOSCustomColorPicker的context是否来自当前可见页面的build,用已经销毁的context打开showModalBottomSheet会抛异常;- 页面是否被
Scaffold或MaterialApp包裹,showModalBottomSheet需要Navigator与Overlay; IosColorPicker使用了固定高度340 + componentsHeight(context),如果外层再套一层高度受限的容器(例如固定高度的Column),弹层内容会被裁剪,看起来像"打开了但空白";hilog中是否出现Channel not implemented,出现时说明走到了原生路径,见 9.5。
9.5 MissingPluginException
这通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用。从仓库根目录执行:
cd example
flutter clean
flutter pub get
flutter run -d <device-id>
如果仍然出现,检查自动生成的插件注册文件中是否包含 IosColorPickerWithTitlePlugin 与 SharedPreferencesPlugin,同时核对 pubspec.yaml、ohos/index.ets 和 oh-package.json5。
需要说明的是:本库的取色功能不走通道,即使插件未注册,弹层也能正常打开和选色。真正受影响的是历史颜色和面板标签,表现为历史一行为空、重启后回到 Grid,处理方式见 9.6。
9.6 历史颜色为空,或者再次打开不记得上次的面板
原因是 shared_preferences 没有 OpenHarmony 实现。CacheHelper 里的 SharedPreferences.getInstance() 会抛 MissingPluginException,异常发生在异步初始化中,界面不会报错,只是历史颜色列表为空、selector_index 写不进去。
处理方式:在宿主应用(本例是 example/pubspec.yaml)中补充依赖,然后重新 flutter pub get 并完整重建:
dependencies:
shared_preferences_ohos:
git:
url: https://atomgit.com/openharmony-sig/flutter_packages.git
path: packages/shared_preferences/shared_preferences_ohos
验证办法有两个:查看 example/pubspec.lock 是否出现 shared_preferences_ohos;查看 GeneratedPluginRegistrant.ets 中是否出现 new SharedPreferencesPlugin()。两者都出现后,历史颜色和面板标签才能跨进程保留。
9.7 编译成功但安装失败
常见原因包括:
- HAP 未签名或使用了错误的 Profile;
- 设备未加入调试设备列表;
- 包名与签名 Profile 不匹配;
- 安装包的
compatibleSdkVersion高于设备 API; - 手机上已经安装了使用不同证书签名的同包名应用。
示例工程的 bundleName 是 com.example.color_system_example,与同系列其他示例相同。如果设备上已经装过同包名、不同证书的应用,安装会失败,需要先卸载旧应用或换一个包名(换包名要同时换签名 Profile)。
9.8 flutter create 不认识 ohos,或包名不合法
先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name ios_color_picker_with_title;仓库名 color_picker_with_title 不能直接作为 Dart 包名。生成后检查 diff,再补充 ArkTS 业务实现。
9.9 AtomGit 依赖提示找不到分支或无权限
先检查 URL 是否指向已同步的目标仓库,再确认 feat/ohos_ios_color_picker_with_title_1.0.2 已推送。分支尚未推送时,可以先使用第七节的 TAG 或提交号 1681b13。私有仓库还需在本机配置 Git 认证。
9.10 改了本地 ArkTS,Demo 为什么没变化
先检查 example/pubspec.yaml:Git 依赖读取远程提交,不会自动读取本地插件改动。本地联调切回 path: ../;测试远程版本则先提交推送,再更新依赖并核对 pubspec.lock 的 resolved-ref。原生代码变动后停止应用并重新构建运行,不能只做 Dart 热重载。
另外,修改 pubspec.yaml 的依赖(例如新增 shared_preferences_ohos)之后必须完整重建一次,GeneratedPluginRegistrant.ets 才会重新生成;只做热重载时新插件不会注册。
9.11 Sliders 面板底部溢出 24 像素
现象是切到 Sliders 面板后,不透明度行上方出现一条黄黑警示条,提示 BOTTOM OVERFLOWED BY 24 PIXELS;Release 包不显示警示条,但内容仍会被裁掉 24 像素。
原因在弹层高度:
Container(
height: 340 + componentsHeight(context),
...
)
componentsHeight(context) 在 lib/custom_picker/shared.dart 中按宽度推算:
double componentsHeight(BuildContext context) {
return (maxWidth(context) - 32) - (((maxWidth(context) - 32) / 12) * 2);
}
在 1084×2412、DPR 3 的机型上,逻辑宽度约 361vp,算出的高度只够放 Grid 与 Spectrum 的内容;Sliders 面板是三个"标签 + 滑杆 + 数值"行,比网格高出约 24 像素,于是溢出。
两种改法:
- 把 Sliders 面板的高度从固定值改成按内容自适应,例如把
SlidePicker外面那层Container的height去掉,让内部的Column决定高度,弹层高度相应改为null并用ConstrainedBox约束上限; - 保持固定高度,在切到 Sliders 时按比例减少
componentsHeight的系数(例如把/ 12改成/ 14),代价是网格面板会略矮。
这两种改法都会改变上游布局参数,本次适配没有动它,只在文档和本节记录现象,避免适配提交里混入界面调整。
相关链接
更多推荐



所有评论(0)