开发工具: 华为云码道

本文配套仓库: oh-flutter/slider_gradient
鸿蒙适配后仓库https://atomgit.com/oh-flutter/slider_gradient

slider_gradient 是一个带渐变背景的 Flutter 滑块组件:轨道支持渐变 / 纯色两种背景,thumb 颜色沿 colors 渐变数组按位置插值,并提供单值与范围选择(双 thumb)两种模式。本文以 slider_gradient 0.2.0 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

插件的根目录 OHOS 模块(ohos/)只承载一个约 77 行的通道桩(SliderGradientPlugin.ets),复刻 Android / iOS 端的模板契约 slider_gradient / getPlatformVersion;组件本体 lib/slider_gradient.dart 没有任何 MethodChannel 调用,所有交互(GestureDetectorAnimationController、自定义布局 _ModalSliderLayout)都在 Flutter 框架内完成,因此只要 Flutter OH 引擎能跑起来,组件就自然可用。本文以 04a3751ec0b8037b558d98c39c5a2beb26158abc 为例,介绍源码准备、OHOS 工程配置、ArkTS 实现和 example 真机运行。

在这里插入图片描述

适配以提交 b9d29488a61dad56ce3a636a517c782270cc605a 为参考,落地在 main 分支,并打 TAG 0.2.1

在这里插入图片描述


KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

我先读取这 6 张截图,确认每张内容后再统一整理成表格。

6 张截图均为同一个 SliderGradient 示例页(OpenHarmony TLR-AL00,6.1.0.135(SP9C00E120R3P5))在不同拖动状态下的快照。整理成两张表:

表 1 · 页面模块与配置说明

模块关键配置预期表现
平台信息MethodChannel("slider_gradient").invokeMethod("getPlatformVersion")展示运行平台版本、通道/方法名,提供「重新查询」按钮
① 纯色背景isGradientBg=false轨道为主色/辅色双色填充,下方展示当前值、百分比、Thumb 颜色
② 渐变背景(悬浮 label)isGradientBg=true(默认)、isShowLabel=true蓝→青→绿→黄渐变轨道,thumb 上方悬浮 label 实时显示数值
③ 范围选择(双 thumb)isRange=true红→绿渐变轨道,双 thumb;拖动时自动选中距离更近的 thumb,展示左/右端值、跨度、左右 Thumb 颜色
④ 自定义样式thumb 16×32、圆角 8;轨道高度 18;三色渐变橙→红→紫三色渐变轨道,自定义圆角 thumb,悬浮 label 显示数值
操作事件(最近 8 条)滚动记录滑块结果、拖动开始、通道调用等事件,最新在最前

表 2 · 各截图实测状态快照

截图时刻① 纯色背景(当前值 / 百分比 / Thumb 颜色)② 渐变背景(当前值 / 百分比 / label / Thumb 颜色)③ 范围选择(左 / 右 / 跨度 / 左色 / 右色)④ 自定义样式(当前值 / 百分比 / label)操作事件记录
22:0230 / 30.0% / -50.00 / 50.0% / 50.0 / -未滚到未滚到
22:03(a)75 / 75.0% / #FF2196F350.00 / 50.0% / 50.0 / -未滚到未滚到
22:03(b)未显示50.00 / 50.0% / 50.0 / -20 / 60 / 40 / - / -70.00 / - / 70.0
22:04未显示未显示20 / 60 / 40 / - / -70.00 / 70.0% / 70.01. 纯色滑块:结果 75(75.0%);2. 纯色滑块:拖动开始;3. 通道调用成功:OpenHarmony TLR-AL00 6.1.0.135(SP9C00E120R3P5)
22:05未显示53.00 / 53.0% / 53.0 / #FF5BE2BB18 / 83 / 65 / #FFDF6352 / #FF6A9F5056.00 / - / 56.0
22:0640 / 40.0% / #FF2196F353.00 / 53.0% / 53.0 / #FF5BE2BB35 / 83 / 48 / #FFC07351 / -未滚到

走查结论:四个示例卡片渲染正常;拖动过程中当前值、百分比、悬浮 label、Thumb 颜色(随轨道位置自动取色)均实时联动更新;范围滑块双 thumb 可独立拖动、跨度随动;MethodChannel 通道调用成功返回平台版本;操作事件按顺序落盘,符合预期。

以下是操作的视屏,可以参考一下:

Example 启动授权 Example 启动授权 Example 启动授权


一、插件简介与适配目标

slider_gradient 把"渐变滑块"这种常见交互封装成 SliderGradient 组件。业务只需要声明一个 colors 数组和 onChange / onChangeBegin / onChangeEnd 三个回调,就能拿到带渐变背景的滑块,并且 thumb 颜色随 thumb 位置在 colors 之间用 Color.lerp 插值;进入范围模式后,两个 thumb 各自独立,且拖动时自动选中距离触点更近的 thumb。

SliderGradient(
  value: _value,
  min: 0,
  max: 100,
  colors: const [Color(0xFF4A90D9), Color(0xFF50E3C2)],
  onChange: (SliderData data) {
    debugPrint('value: ${data.value}');
  },
  onChangeEnd: (SliderData data) {
    setState(() => _value = data.value!);
  },
)

上游仅提供 Android / iOS 平台实现,OpenHarmony 平台无法直接使用。OHOS 适配目标有三个:

  1. 平台通道对齐:补全 ohos/ 根目录模块,新增 SliderGradientPlugin.ets(约 77 行)注册与 Android / iOS 完全一致的通道 slider_gradient 和方法 getPlatformVersion,使组件与插件注册表契约保持一致;
  2. Dart 层空安全迁移:上游为 pre-null-safety 写法(Key key@required、非空字段未初始化),而 OHOS 版 Flutter 3.44 自带 Dart 3.12,旧语法编译期直接报错;本次适配对 lib/slider_gradient.dart 做了最小化空安全迁移,公开 API 的名称、语义与默认值保持不变;
  3. example 在真机跑通 4 个场景:纯色背景、渐变背景(悬浮 label)、范围选择(双 thumb)、自定义样式,并演示如何调用 getPlatformVersion 获取 OpenHarmony ${deviceInfo.displayVersion}

二、环境准备

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

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

flutter --version
flutter doctor -v
hdc list targets

在这里插入图片描述

在这里插入图片描述

编辑用户

工程使用的工具链和 SDK 配置如下:

项目版本或配置用途
Flutter OHOS SDK3.44.9+ohos-0.0.1-canary1Flutter 编译与 OHOS 平台工具链
Flutter 分支oh-3.44.9-devCPF-Flutter 对应开发分支
Dart SDK3.12.2Dart 语言与包管理环境
HarmonyOS 开发套件7.0.0(API 26)开发套件版本及对应的 API 级别
compileSdkVersion26.0.0编译时使用的 SDK API
targetSdkVersion26.0.0应用面向的行为版本
compatibleSdkVersion5.1.0(18)当前工程声明的最低兼容版本
插件版本0.2.0(未随 OHOS 适配 bump)pubspec.yaml 中的包版本
实际 TAG0.2.1git tag -l 显示)适配提交的 git TAG
原生语言ArkTS根目录 ohos/ 插件模块(约 77 行通道桩)
插件产物HAR被应用 entry 模块依赖
设备HUAWEI nova 14(TLR-AL00)真机验证
系统 ROMOpenHarmony-6.1.1.120屏幕显示的 deviceInfo.displayVersion6.1.0.135(SP9C00E120R3P5)

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 应用工程中 compileSdkVersiontargetSdkVersion 的属性值;
  • 5.1.0(18) 是本文工程中 compatibleSdkVersion 的属性值,声明最低兼容 API 18。

对应的 product 配置为:

{
  "name": "default",
  "compatibleSdkVersion": "5.1.0(18)",
  "compileSdkVersion": "26.0.0",
  "targetSdkVersion": "26.0.0",
  "runtimeOS": "HarmonyOS"
}

在这里插入图片描述

这组配置使用 API 26 SDK 编译,并以 API 26 为目标版本,最低兼容 API 18。SliderGradient 组件本身不依赖具体设备 API,但通道桩在 onMethodCall 中读取 deviceInfo.displayVersion,要求 SDK 提供 @ohos.deviceInfo 模块。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

适配已有第三方插件时,先从 Pub 包信息或项目 README 确认上游地址、包名、版本和许可证。同步仓库时保留源码、许可证和提交历史,方便后续跟踪上游更新。

在 AtomGit 网页的新建仓库流程中,找到从已有仓库导入的入口,填写上游 Git 地址,选择自己有写权限的组织或个人空间,设置目标仓库名称后执行导入。完成后检查目标分支、pubspec.yamlLICENSE 和提交历史是否完整,并记录适配起点的提交号。如果上游本身托管在 AtomGit,也可以通过 Fork 获得自己的工作仓库。

slider_gradient 现可从 AtomGit 获取,远程为 git@atomgit.com:oh-flutter/slider_gradient.git。下面使用 AtomGit 地址拉取代码;需要提交修改时,使用自己有写权限的仓库或 Fork。

3.2 将代码拉取到宿主机

在安装了 Flutter OH 和 DevEco Studio 的开发电脑上打开终端,进入准备存放项目的目录,执行:

git clone https://github.com/dilireba521/slider_gradient
cd slider_gradient
pwd
ls
git remote -v
git status --short --branch
git rev-parse HEAD

在这里插入图片描述

git clone 会创建 slider_gradient/ 目录;cd 后的位置就是下文所说的插件仓库根目录,这里应能看到 pubspec.yamllib/example/ohos/。Git 仓库名是 slider_gradient,Dart 包名也是 slider_gradient

需要使用与本文相同的代码版本时,在没有未提交修改的仓库中切换到以下提交:

git switch --detach 04a3751ec0b8037b558d98c39c5a2beb26158abc

适配其他插件时,使用该插件对应版本的 tag 或 commit 作为分支起点。

路径提示:本文实际仓库位于 /Users/david/workspace/flutter/lib/slider_gradient(即 ~/workspace/flutter/lib/ 之下),不是 ~/workspace/flutter/slider_gradient。这是该 workspace 下多个 Flutter 插件共用一个父目录的常见路径陷阱,克隆或拉取时需要按实际位置进入。基线提交(不含 OHOS 改动)是 04a3751 “add range selection function”。

在这里插入图片描述

图 1:克隆 AtomGit 仓库 oh-flutter/slider_gradientgit log 显示 OHOS 适配提交 b9d2948 已打 TAG 0.2.1pubspec.yamlname: slider_gradientversion: 0.2.0

3.3 在仓库根目录创建适配分支

接着在 slider_gradient/ 根目录创建分支。分支名统一使用 feat/ohos_库名称_版本号,库名称取 pubspec.yamlname,版本号取此次适配的基线版本。本例为:

git switch -c feat/ohos_slider_gradient_0.2.0
git branch --show-current

如果该分支已存在,使用 git switch feat/ohos_slider_gradient_0.2.0 切换即可。基线提交(不含任何 OHOS 改动)是 04a3751,先 detach 验证可编译再回到 main 即可。

请添加图片描述

图 2:在基线 04a3751(“add range selection function”)上 detach 验证,回到 main 创建分支 feat/ohos_slider_gradient_0.2.0;TAG 命名规则为 git tag 0.2.0-ohos-1.0.0-beta.1,但实际 TAG 是 0.2.1(详见 9.3),git tag -l 列出 0.1.0 / 0.1.1 / 0.2.0 / 0.2.1

3.4 自动补全 OHOS 适配结构

分支创建后,仍在同一个插件根目录执行结构补全。以下命令适用于尚无 ohos/ 目录的既有 Flutter 平台插件

flutter create --template=plugin --platforms=ohos --project-name slider_gradient .
git status --short
git diff -- pubspec.yaml lib example
  • --template=plugin 指定插件模板;
  • --platforms=ohos 指定需要补全的平台;
  • --project-name slider_gradient 使用 Dart 包名,避免把含连字符的仓库目录名作为包名;
  • 最后的 . 表示在当前插件目录补全工程,不是另建一层 slider_gradient/

该命令生成 OHOS 平台脚手架,业务逻辑需要在 ArkTS 中实现。生成后通过 diff 检查 pubspec.yamllib/example/ 的变化,保留已有 API、其他平台注册项及依赖配置。不同 Flutter OH 版本生成的模板可能略有差异。

如果生成后 example/ohos/ 仍不存在,进入已有示例应用补全平台:

cd example
flutter create --platforms=ohos .
cd ..

配套仓库已经包含 ohos/example/ohos/,直接运行示例时可以跳过结构补全。新建插件则使用 flutter create --org com.nutpi --template=plugin --platforms=ohos slider_gradient;已有插件使用上面的 . 在当前目录补全。

请添加图片描述

图 3:在 slider_gradient/ 根目录执行 flutter create --template=plugin --platforms=ohos . 生成 ohos/,再 cd example && flutter create --platforms=ohos . 补全宿主工程 example/ohos/

3.5 适配后的项目目录

适配后的关键目录如下:

slider_gradient/
├── lib/
│   └── slider_gradient.dart         # 组件实现(纯 Dart,跨平台,已做空安全迁移)
├── android/                          # Android 模板通道桩
├── ios/                              # iOS 模板通道桩
├── ohos/
│   ├── index.ets
│   ├── oh-package.json5
│   └── src/main/
│       ├── ets/components/plugin/SliderGradientPlugin.ets   # 77 行通道桩
│       └── module.json5
├── example/
│   ├── lib/main.dart                 # 4 场景演示 + 平台信息卡 + 操作事件日志
│   └── ohos/
│       ├── AppScope/app.json5        # bundleName: com.example.slider_gradient_example
│       ├── entry/
│       └── build-profile.json5       # ⚠ 含本机签名材料,见 9.4
├── docs/
│   ├── SliderGradient-ohos-adaptation-blog.md
│   └── evidence/                     # 真机截图
├── README.OpenHarmony_CN.md
├── README.OpenHarmony.md
├── CHANGELOG.OpenHarmony.md
└── pubspec.yaml

项目根目录如下,其中包含 ohos/example/ohos/docs/evidence/ 以及 OpenHarmony 中英文说明和变更记录文件:

请添加图片描述

图 4:适配后的 slider_gradient 项目根目录,包含 lib/ohos/(含 77 行 SliderGradientPlugin.ets)、example/ohos/docs/evidence/ 和三份 OpenHarmony 文档。

文件主要职责
lib/slider_gradient.dart组件实现(纯 Dart,已空安全迁移)
ohos/index.ets导出 SliderGradientPlugin
ohos/src/main/ets/components/plugin/SliderGradientPlugin.ets注册通道 slider_gradient,处理 getPlatformVersion
ohos/oh-package.json5声明 HAR 模块信息(name: slider_gradientlicense: Apache-2.0,与上游 MIT 不一致,详见 9.5)
插件 module.json5声明 HAR 模块信息
示例 entry module.json5声明宿主应用 Ability 与 ohos.permission.INTERNET(Flutter 模板默认)
example/lib/main.dart4 场景演示 + 平台信息卡 + 操作事件日志
example/ohos/AppScope/app.json5bundleName com.example.slider_gradient_example(与示例工程一致,无残留)
example/ohos/build-profile.json5products.default + modules.entry,含本机签名材料,需在提交前剥离(见 9.4)
README.OpenHarmony_CN.md / README.OpenHarmony.md中英文安装说明、TAG 表、接口表、示例
CHANGELOG.OpenHarmony.mdOHOS 适配变更记录
docs/SliderGradient-ohos-adaptation-blog.md本仓库自带的适配技术笔记(223 行)
docs/evidence/3 张真机截图(首次启动 / 中段 / 底部)

四、Dart 接口与通道分析

OHOS 实现需要遵循 Dart 层已有的方法、参数和返回值约定。先阅读 lib/slider_gradient.dart,确认组件本体与平台通道的关系;本组件是纯 Dart 实现——lib/slider_gradient.dart 中没有任何 MethodChannel / Platform / invokeMethod 调用,因此适配重点落在通道契约对齐与空安全迁移上。适配其他已有平台的插件时,也应保留其公开接口和其他平台实现。

本例的对应关系如下:

Dart 入口或模型通道协议OHOS 实现应保持的行为
SliderGradient(组件本体)纯 Dart,GestureDetector + AnimationController + 自定义布局OHOS 上自然可用,不经通道
_MyHomePageState._queryPlatformVersion()(demo 中调用)slider_gradient / getPlatformVersionSliderGradientPlugin.ets 返回 OpenHarmony ${deviceInfo.displayVersion}模板契约,三端一致;返回值形如 OpenHarmony 6.1.0.135
SliderChangeCallback / SliderDataDart 内部回调,不经过通道单值模式回调 payload 含 value / color;范围模式回调 payload 含 values / colors

原生端需要保持通道名与方法名一致。SliderGradientPlugin 实现 FlutterPluginMethodCallHandler,对未实现方法统一返回 notImplemented(),保证通道桩可被 Flutter 工具链识别并通过 GeneratedPluginRegistrant 注册。

4.1 跨端架构与调用时序

组件本体是纯 Dart 组件,OHOS 上不需要为滑块本身打开通道;通道仅承担 demo 中的 getPlatformVersion 调用。整体架构如下:

重新查询按钮

getPlatformVersion

deviceInfo.displayVersion

OpenHarmony 6.1.0.135

Flutter 页面 MyHomePage

SliderGradient 组件

GestureDetector

AnimationController

_ModalSliderLayout 自定义布局

MethodChannel slider_gradient

ArkTS SliderGradientPlugin

组件交互(拖动 thumb、点击轨道、Color.lerp 计算 thumb 颜色、AnimatedBuilder 过渡)全部在 Flutter 框架内完成;通道仅承载一次性的"平台版本字符串"查询。

4.1.1 一次 getPlatformVersion 调用的时序
deviceInfo SliderGradientPlugin.ets MethodChannel slider_gradient _MyHomePageState Flutter App deviceInfo SliderGradientPlugin.ets MethodChannel slider_gradient _MyHomePageState Flutter App initState / 重新查询 invokeMethod("getPlatformVersion") onMethodCall(call, result) deviceInfo.displayVersion 6.1.0.135 result.success("OpenHarmony 6.1.0.135") String setState(_platformVersion)

注意组件本体走的是左侧虚线:拖动 thumb 不经过通道,OHOS 上只要 Flutter 引擎能跑起来就自然工作。

4.2 公开 API:组件属性与回调 payload

lib/slider_gradient.dart 中的 SliderGradient 是业务层唯一入口;SliderDataonChange / onChangeBegin / onChangeEnd 回调的 payload。

class SliderGradient extends StatefulWidget {
  SliderGradient({
    Key? key,
    this.label,
    this.thumbStyle = const ThumbStyle(),
    this.labelStyle = const LabelStyle(),
    this.sliderStyle = const SliderStyle(),
    this.isGradientBg = true,
    this.colors,
    this.min = 0,
    this.max = 100,
    this.isShowLabel = false,
    this.isRange = false,
    this.values,
    required this.value,
    required this.onChange,
    this.onChangeBegin,
    this.onChangeEnd,
    this.divisions,
  })  : assert(min < max),
        assert(isRange ? true : value >= min && value <= max),
        assert(isRange
            ? values != null &&
                values.length == 2 &&
                values.first <= values.last
            : true),
        super(key: key);
}

class SliderData {
  SliderData({this.color, this.value, this.values, this.colors});
  double? value;
  List<double>? values;
  Color? color;
  List<Color>? colors;
}
4.2.1 属性与样式默认值
名称类型默认必填备注
valuedouble单值模式当前数值;可被外部修改(initData 中会再次同步)
min / maxdouble0 / 100assert(min < max)
colorsList<Color>?nullnull 时主色取 Theme.of(context).primaryColor,辅色取白色
isGradientBgbooltruefalse 时轨道为主色 / 辅色双色填充
isRangeboolfalsetrue 时必须传入长度为 2 且递增的 values
valuesList<double>?nullisRange=true 时必填范围模式下的两个数值
isShowLabelboolfalsetrue 时在 thumb 上方显示悬浮 label
labelString?nulllabel 显示内容;null 时显示当前数值
onChangeSliderChangeCallback拖动或点击轨道时触发
onChangeBegin / onChangeEndSliderChangeCallback?null拖动开始 / 结束时触发
divisionsint?null(自动按 max - min 算)滑块等分数
sliderStyleSliderStyleSliderStyle(height: 16, radius: 4)轨道高度与圆角
thumbStyleThumbStyleThumbStyle(width: 16, height: 32, radius: 4, borderColor: 0xffE6E6E6)thumb 尺寸与边框颜色
labelStyleLabelStyleLabelStyle(color: 0xffffffff, size: 10)label 字体颜色与字号

回调触发时机如下:

  • onChange:每次 thumb 位置变化(拖动或点击轨道)都会触发,可执行多次;
  • onChangeBegin:用户开始拖动 thumb 时触发一次;
  • onChangeEnd:用户结束拖动时触发一次,业务通常在此回调里 setState 更新 _value
  • 单值模式 payload 含 valuedouble?)与 colorColor?,已按 Color.lerpcolors 数组中插值);
  • 范围模式 payload 含 valuesList<double>?)与 colorsList<Color>?),两个 thumb 各自独立。
4.2.2 组件内部:_ModalSliderLayout 与 thumb 颜色插值

_SliderGradientState 使用 SingleTickerProviderStateMixin 持有 AnimationController,自定义布局通过 _ModalSliderLayout extends SingleChildLayoutDelegate 完成,getPositionForChild 根据 progress0.01.0)把 thumb 放到轨道对应位置:


Offset getPositionForChild(Size size, Size childSize) {
  return Offset(size.width * progress - thumbWidth / 2, 0);
}

thumb 颜色在 _lerp 中按当前百分比在 colors 数组的相邻两色之间做线性插值:

Color _lerp(int len, double percent) {
  // ...
  return Color.lerp(widget.colors![_num - 1], widget.colors![_num], _percent);
}

代码中保留了一条针对华为机型的注释:labelTextHeight 必须给 TextPainter 显式传入 locale: Localizations.localeOf(context),否则华为设备上算出的 label 高度偏小(详见 9.6)。

4.3 空安全迁移的改动范围

上游 lib/slider_gradient.dart 是 pre-null-safety 写法,而 OHOS 版 Flutter 3.44 自带 Dart 3.12 强制 null-safety。本次迁移保留 API 名称、语义和默认值,仅做以下最小化改动:

  • Key keyKey? key@requiredrequired
  • 字段类型改可空:SliderChangeCallback?double?Color?String?List<Color>?List<double>?LabelStyle.fillColor
  • 内部状态字段加 late 声明(late AnimationController controllerlate Color _beginColorlate Color _endColor),并在 initState / initData 中显式赋值;
  • initData 中通过局部变量 final colors = widget.colors 绕过实例字段不参与类型提升的限制;
  • _doubleToInt 删除 dead null check,直接 int.parse(val.toString().split('.')[0])
  • pubspec.yamlsdk: ">=2.7.0 <3.0.0"sdk: ">=2.12.0 <4.0.0"flutter: ">=1.20.0" 保持不变。

4.4 Dart 通道协议分析

4.4.1 通道名称三端完全一致
static const MethodChannel _channel = MethodChannel('slider_gradient');

Dart demo 与 ArkTS 插件约定的通道名是 slider_gradient,方法名是 getPlatformVersion。任何一端拼写不一致都会出现“方法未实现”或“收不到返回值”等问题。

4.4.2 通道调用与错误处理
Future<void> _queryPlatformVersion() async {
  try {
    final Object? result =
        await _channel.invokeMethod('getPlatformVersion');
    if (!mounted) return;
    setState(() => _platformVersion = result?.toString() ?? '(空)');
    _log('通道调用成功:$_platformVersion');
  } catch (e) {
    if (!mounted) return;
    setState(() => _platformVersion = '调用失败: $e');
    _log('通道调用失败: $e');
  }
}

invokeMethod 返回 Future<Object?>。demo 把它 toString() 后塞进 _platformVersion,并写入操作事件日志;catch 兜底 MissingPluginException / PlatformException。OHOS 真机上正常路径返回形如 OpenHarmony 6.1.0.135(SP9C00E120R3P5)deviceInfo.displayVersion 包含 ROM build 号)。


五、补全 OHOS 原生实现与工程配置

5.1 在 SliderGradientPlugin.ets 中实现通道桩

SliderGradient 是纯 Dart 组件,所有交互都在 Flutter 框架内完成,因此 OHOS 原生侧只需要一个通道桩:在 pubspec.yaml 声明 ohos: pluginClass: SliderGradientPlugin,并在根目录 ohos/src/main/ets/components/plugin/SliderGradientPlugin.ets 中注册与 Android / iOS 完全一致的通道契约。核心实现如下:

import {
  FlutterPlugin,
  FlutterPluginBinding,
  MethodCall,
  MethodCallHandler,
  MethodChannel,
  MethodResult,
} from '@ohos/flutter_ohos';
import deviceInfo from '@ohos.deviceInfo';
import hilog from '@ohos.hilog';

/**
 * SliderGradientPlugin
 *
 * OpenHarmony implementation of the `slider_gradient` plugin channel.
 *
 * Channel contract (kept identical to the Android/iOS implementations):
 *   channel name: "slider_gradient"
 *   methods:
 *     - "getPlatformVersion" -> String, e.g. "OpenHarmony 6.1.0.135"
 */
export default class SliderGradientPlugin implements FlutterPlugin, MethodCallHandler {
  private static readonly TAG: string = 'SliderGradientPlugin';
  private static readonly DOMAIN: number = 0xFF00;
  private channel: MethodChannel | null = null;

  getUniqueClassName(): string {
    return 'SliderGradientPlugin';
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    try {
      this.channel = new MethodChannel(binding.getBinaryMessenger(), 'slider_gradient');
      this.channel.setMethodCallHandler(this);
      hilog.info(SliderGradientPlugin.DOMAIN, SliderGradientPlugin.TAG, 'onAttachedToEngine');
    } catch (err) {
      hilog.error(SliderGradientPlugin.DOMAIN, SliderGradientPlugin.TAG,
        `onAttachedToEngine failed: ${JSON.stringify(err)}`);
    }
  }

  onDetachedFromEngine(binding: FlutterPluginBinding): void {
    try {
      if (this.channel != null) {
        this.channel.setMethodCallHandler(null);
      }
      this.channel = null;
      hilog.info(SliderGradientPlugin.DOMAIN, SliderGradientPlugin.TAG, 'onDetachedFromEngine');
    } catch (err) {
      hilog.error(SliderGradientPlugin.DOMAIN, SliderGradientPlugin.TAG,
        `onDetachedFromEngine failed: ${JSON.stringify(err)}`);
    }
  }

  onMethodCall(call: MethodCall, result: MethodResult): void {
    try {
      if (call.method == 'getPlatformVersion') {
        const version = `OpenHarmony ${deviceInfo.displayVersion}`;
        hilog.info(SliderGradientPlugin.DOMAIN, SliderGradientPlugin.TAG,
          `getPlatformVersion -> ${version}`);
        result.success(version);
      } else {
        result.notImplemented();
      }
    } catch (err) {
      hilog.error(SliderGradientPlugin.DOMAIN, SliderGradientPlugin.TAG,
        `onMethodCall failed, method: ${call.method}, error: ${JSON.stringify(err)}`);
      result.error('error', `method ${call.method} failed: ${JSON.stringify(err)}`, null);
    }
  }
}
5.1.1 引入 Flutter 和 OpenHarmony 能力
  • FlutterPlugin / FlutterPluginBinding / MethodChannel / MethodResult 来自 @ohos/flutter_ohos,负责接入 Flutter Engine 生命周期并处理 Dart 调用;
  • deviceInfo 来自 @ohos.deviceInfo,提供 displayVersion 字符串(系统版本完整标识,语义最贴近"平台版本字符串");
  • hilog 来自 @kit.PerformanceAnalysisKit,负责原生侧诊断日志,本例 DOMAIN = 0xFF00TAG = 'SliderGradientPlugin'
5.1.2 连接 Flutter Engine 与错误处理

onAttachedToEngine 在 Engine 绑定插件时建立通道并设置 MethodCallHandleronDetachedFromEngine 在 Engine 解绑时清空 Handler 并把 channelnull,避免下一次 Engine 绑定时残留旧 Handler。两个入口都用 try / catch 包住,并通过 hilog 记录成功 / 失败日志。

5.1.3 处理 MethodChannel 命令

onMethodCall 仅识别 getPlatformVersion,返回 `OpenHarmony ${deviceInfo.displayVersion}`(如 OpenHarmony 6.1.0.135);其他方法统一 result.notImplemented()try / catch 中通过 result.error('error', ...) 把异常码 / 消息传回 Dart,触发 demo 的 catch 回调。

5.1.4 getUniqueClassName() 与 HAR 注册

getUniqueClassName() 必须返回 'SliderGradientPlugin',与 pubspec.yamlplugin.platforms.ohos.pluginClass: SliderGradientPlugin 完全一致;Flutter 工具链在生成 GeneratedPluginRegistrant.ets 时会校验这个类名。

5.2 声明插件和宿主权限

SliderGradient 不读取传感器、不访问网络、不申请任何系统权限——组件本身是纯 Dart 实现,通道桩只读 deviceInfo.displayVersion

5.2.1 插件 HAR 的权限

在插件的 ohos/src/main/module.json5不申请任何权限

{
  "module": {
    "name": "slider_gradient",
    "type": "har",
    "deviceTypes": ["default", "tablet"]
  }
}
5.2.2 应用 entry 的权限

最终安装的是宿主应用 example/ohos/entry。本例需要核对 example/ohos/entry/src/main/module.json5,保留 Flutter 模板自带的 ohos.permission.INTERNET(与插件功能无关):

{
  "module": {
    "requestPermissions": [
      {"name": "ohos.permission.INTERNET"}
    ]
  }
}

权限声明和运行时授权是两个步骤。接入应用时,还需根据目标 SDK 的权限定义处理授权要求;module.json5 中的声明不会自动完成运行时授权。INTERNET 属于普通权限,无需额外填写权限原因资源。

5.3 注册并导出插件

pubspec.yaml 通过以下配置声明 OHOS 插件类:

flutter:
  plugin:
    platforms:
      android:
        package: com.example.slider_gradient
        pluginClass: SliderGradientPlugin
      ios:
        pluginClass: SliderGradientPlugin
      ohos:
        pluginClass: SliderGradientPlugin

适配前 pubspec.yaml 声明的是 web: 平台,本次替换为 ohos: 平台;插件类名 SliderGradientPlugin 三端一致。

插件的 ohos/index.ets 需要导出实现:

import SliderGradientPlugin from './src/main/ets/components/plugin/SliderGradientPlugin';
export default SliderGradientPlugin;

执行 flutter pub get 和构建后,Flutter 工具会为应用生成插件注册代码:

import { FlutterEngine, Log } from '@ohos/flutter_ohos';
import SliderGradientPlugin from 'slider_gradient';

const TAG = "GeneratedPluginRegistrant";

export class GeneratedPluginRegistrant {
  static registerWith(flutterEngine: FlutterEngine) {
    try {
      flutterEngine.getPlugins()?.add(new SliderGradientPlugin());
    } catch (e) {
      Log.e(TAG, "Tried to register plugins with FlutterEngine (" + flutterEngine + ") failed.");
      Log.e(TAG, "Received exception while registering", e);
    }
  }
}

import SliderGradientPlugin from 'slider_gradient' 说明 entry 模块通过 oh-package 依赖了根目录 ohos/ 生成的本地 HAR。注册进引擎的是通道桩:它注册与 Dart demo 完全一致的通道名 slider_gradient,但仅暴露 getPlatformVersion 一个方法。组件本体是纯 Dart 实现,不经过通道

注册异常的排查步骤见第九节 MissingPluginException

5.4 检查 example 的 OHOS 应用结构

本例的 example/ohos/AppScope/app.json5bundleNamecom.example.slider_gradient_example,与示例工程名一致,无残留坑。下面是需核对的配置片段,请合并到现有工程;其中 signingConfig: "default" 需要与本机配置的签名名称一致,签名材料不应提交到公开仓库(详见 9.4):

{
  "app": {
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.1.0(18)",
        "runtimeOS": "HarmonyOS",
        "compileSdkVersion": "26.0.0",
        "targetSdkVersion": "26.0.0"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": ["default"]
        }
      ]
    }
  ]
}

配置后,在 DevEco Studio 中执行一次 Sync Project。如果 entry 模块正常识别,Project 视图中会出现 entry,并能打开 entry 模块的签名配置。


六、补全交付文件并提交适配分支

6.1 除代码外还要补全哪些文件

代码适配完成后,还需要整理安装说明、接口文档、版本记录和开源信息。接收仓库有专用模板时,按其格式填写:

文件应写清楚的内容
README.OpenSource上游名称、源码地址、适配版本或提交、版权及许可证信息;按仓库模板列出第三方依赖
README.md原项目说明、OHOS 支持入口、配套 Demo 和文档链接;保留上游信息
README.OpenHarmony_CN.md简介、AtomGit 安装方式、版本对应关系、环境约束、权限、接口表、示例、已验证范围和遗留问题
README.OpenHarmony.md与中文说明对应的英文文档
CHANGELOG.OpenHarmony.mdOHOS 新增能力、适配版本、兼容限制与测试范围
LICENSE / NOTICE保留上游许可证;NOTICE 按许可证和原项目要求保留或补充
example/README.md依赖方式、运行目录、签名、操作步骤与效果图;覆盖 4 个场景
pubspec.yamlohos/oh-package.json5核对包名、版本、插件注册、仓库地址、许可证和依赖
.gitignore忽略构建缓存及本机签名材料,不漏提交必要源码和配置

README.OpenHarmony_CN.md 记录库本身的来源与版本。本例的包名为 slider_gradient,pubspec 版本为 0.2.0(未随 OHOS 适配 bump,详见 9.7),采用 MIT 许可证;Flutter 和 HarmonyOS SDK 版本写入环境说明。

6.2 提交前检查

提交前先完成第八节的插件、example 和真机测试,再从根目录检查改动:

git branch --show-current
git diff --check
git status --short
git diff --stat
git diff

重点检查:

  • example/ohos/build-profile.json5 仍带着本机调试签名的绝对路径与口令,发布前必须剥离(见 9.4);
  • example/ohos/entry/src/main/module.json5 只保留 ohos.permission.INTERNET
  • docs/evidence/ 下保留关键证据(见第八节);
  • pubspec.yamlflutter.plugin.platformsweb: 替换为 ohos: pluginClass: SliderGradientPlugin,三端 pluginClass 一致。

6.3 提交并推送到 AtomGit

文档和代码整理完成后,在根目录暂存并提交。文件名按项目实际情况调整:

git add ohos pubspec.yaml lib .gitignore
git add example/pubspec.yaml example/lib example/ohos example/test docs
git add README.md 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: 完成 OpenHarmony 平台适配"
git remote -v
git branch --show-current
git push -u origin feat/ohos_slider_gradient_0.2.0

DevEco 可能向 example/ohos/build-profile.json5 写入本机签名配置,需要在提交前从暂存内容中移除(详见 9.4)。推送时,origin 应指向自己的 AtomGit 仓库,当前分支为 feat/ohos_slider_gradient_0.2.0

本文对应的实际提交是 b9d29488a61dad56ce3a636a517c782270cc605a(“feat: 完成 OpenHarmony 平台适配”,Co-Authored-By: AtomCode glm5.3-flash-pro),直接落在 main 分支并打 TAG 0.2.1(详见 9.3);该提交共 44 个文件、1543 行新增 / 146 行删除,包含根目录 ohos/ 通道桩、example/ohos/ 宿主工程、4 场景示例、docs/evidence/ 证据与三份 OpenHarmony 文档。

推送后在 AtomGit 发起合并请求,说明上游来源和版本、OHOS 实现范围(纯 Dart 组件 + 77 行通道桩)、依赖及权限(仅 INTERNET)、测试环境、操作结果、已知限制(详见第九节遗留问题),并附 Demo 运行图。目标分支和评审流程以接收仓库要求为准。


七、使用根目录 example 演示接入

仓库自带 example/,可以直接用来调试插件和体验 SliderGradient 在 OpenHarmony 真机上的四种用法。

7.1 本地适配时使用路径依赖

当前 example/pubspec.yaml 的依赖是:

dependencies:
  flutter:
    sdk: flutter
  slider_gradient:
    path: ../

../ 相对于 example/pubspec.yaml 指向插件根目录,修改根目录插件后可直接联调。

7.2 通过 AtomGit 引入插件

业务应用通过 AtomGit 引入时,将 slider_gradientpath 配置替换为下面的 Git 依赖。这里固定到 README 中声明的 TAG(注意:实际 TAG 名是 0.2.1,详见 9.3):

dependencies:
  flutter:
    sdk: flutter
  slider_gradient:
    git:
      url: https://atomgit.com/oh-flutter/slider_gradient.git
      ref: 0.2.1

使用自己的适配版本时,先推送分支,再将 url 改为对应仓库,ref 改为 feat/ohos_slider_gradient_0.2.0。正式发布后可固定到 tag 或 commit。

从插件根目录执行:

cd example
flutter pub get
flutter pub deps

检查 example/pubspec.lockslider_gradient 的来源为 git,并核对 urlrefresolved-ref。同时检查没有 dependency_overridespubspec_overrides.yaml 将其覆盖回本地依赖,确认应用使用的是 Git 依赖。

7.3 调用接口实现 4 场景演示

SliderGradient 的核心 API 只有一个组件,demo 把它扩展为 4 个场景,分别覆盖纯色背景、渐变背景(悬浮 label)、范围选择(双 thumb)、自定义样式,外加平台信息卡(演示 getPlatformVersion)与操作事件日志(最近 8 条)。下面这段可直接用于 example/lib/main.dart,对应仓库 docs/evidence/01_first_screen.jpeg02_scrolled.jpeg03_bottom.jpeg

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:slider_gradient/slider_gradient.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SliderGradient Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const MethodChannel _channel = MethodChannel('slider_gradient');

  /// 平台信息(通过插件通道 getPlatformVersion 获取)
  String? _platformVersion;

  /// ① 纯色滑块
  double _solidValue = 30;
  Color? _solidColor;

  /// ② 渐变滑块
  double _gradValue = 50;
  Color? _gradColor;

  /// ③ 范围选择
  List<double> _rangeValues = [20, 60];
  Color? _rangeColorL;
  Color? _rangeColorR;

  /// ④ 自定义样式
  double _customValue = 70;
  Color? _customColor;

  /// 操作事件(最近 8 条)
  final List<String> _events = [];

  
  void initState() {
    super.initState();
    _queryPlatformVersion();
  }

  Future<void> _queryPlatformVersion() async {
    try {
      final Object? result =
          await _channel.invokeMethod('getPlatformVersion');
      if (!mounted) return;
      setState(() => _platformVersion = result?.toString() ?? '(空)');
      _log('通道调用成功:$_platformVersion');
    } catch (e) {
      if (!mounted) return;
      setState(() => _platformVersion = '调用失败: $e');
      _log('通道调用失败: $e');
    }
  }

  void _log(String msg) {
    _events.insert(0, msg);
    if (_events.length > 8) _events.removeRange(8, _events.length);
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('slider_gradient OHOS demo'),
        actions: [
          IconButton(
            icon: const Icon(Icons.replay),
            onPressed: _queryPlatformVersion,
            tooltip: '重新查询',
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(8),
        children: [
          // 平台信息卡
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('平台信息', style: TextStyle(fontWeight: FontWeight.w600)),
                  const SizedBox(height: 4),
                  Text('MethodChannel("slider_gradient").invokeMethod("getPlatformVersion") 返回:'),
                  SelectableText(_platformVersion ?? '查询中…'),
                ],
              ),
            ),
          ),
          // ① 纯色背景
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('① 纯色背景', style: TextStyle(fontWeight: FontWeight.w600)),
                  const Text('isGradientBg=false,轨道为主色/辅色双色填充'),
                  SliderGradient(
                    value: _solidValue,
                    colors: const [Color(0xFF2196F3), Color(0xFFE3F2FD)],
                    isGradientBg: false,
                    onChange: (d) => setState(() {
                      _solidValue = d.value ?? _solidValue;
                      _solidColor = d.color;
                    }),
                    onChangeBegin: (_) => _log('纯色滑块:拖动开始'),
                    onChangeEnd: (d) => _log('纯色滑块:结果 ${d.value?.toStringAsFixed(0)}'),
                  ),
                  if (_solidColor != null)
                    Text('thumb 颜色:${_solidColor.toString()}'),
                ],
              ),
            ),
          ),
          // ② 渐变背景(悬浮 label)
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('② 渐变背景(悬浮 label)',
                      style: TextStyle(fontWeight: FontWeight.w600)),
                  const Text('isGradientBg=true(默认),isShowLabel=true,thumb 上方实时显示数值'),
                  SliderGradient(
                    value: _gradValue,
                    colors: const [Color(0xFFFF5252), Color(0xFF4CAF50)],
                    isShowLabel: true,
                    onChange: (d) => setState(() {
                      _gradValue = d.value ?? _gradValue;
                      _gradColor = d.color;
                    }),
                    onChangeBegin: (_) => _log('渐变滑块:拖动开始'),
                    onChangeEnd: (d) => _log('渐变滑块:结果 ${d.value?.toStringAsFixed(2)}'),
                  ),
                  if (_gradColor != null)
                    Text('thumb 颜色:${_gradColor.toString()}'),
                ],
              ),
            ),
          ),
          // ③ 范围选择(双 thumb)
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('③ 范围选择(双 thumb)',
                      style: TextStyle(fontWeight: FontWeight.w600)),
                  const Text('isRange=true,拖动时自动选中距离更近的 thumb'),
                  SliderGradient(
                    value: _rangeValues.first,
                    values: _rangeValues,
                    isRange: true,
                    colors: const [Color(0xFF4A90D9), Color(0xFF50E3C2)],
                    onChange: (d) => setState(() {
                      _rangeValues = d.values ?? _rangeValues;
                      _rangeColorL = d.colors?.first;
                      _rangeColorR = d.colors?.last;
                    }),
                    onChangeBegin: (_) => _log('Range 滑块:拖动开始'),
                    onChangeEnd: (d) => _log('Range 滑块:结果 ${d.values?.toList()}'),
                  ),
                  Text('当前范围:${_rangeValues.toList()}, '
                      '左 thumb 颜色:${_rangeColorL.toString()}, '
                      '右 thumb 颜色:${_rangeColorR.toString()}'),
                ],
              ),
            ),
          ),
          // ④ 自定义样式
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('④ 自定义样式',
                      style: TextStyle(fontWeight: FontWeight.w600)),
                  SliderGradient(
                    value: _customValue,
                    colors: const [
                      Color(0xFF9C27B0),
                      Color(0xFF03A9F4),
                      Color(0xFF8BC34A),
                    ],
                    thumbStyle: const ThumbStyle(
                      width: 16, height: 32, radius: 8,
                    ),
                    sliderStyle: const SliderStyle(height: 18, radius: 9),
                    isShowLabel: true,
                    onChange: (d) => setState(() {
                      _customValue = d.value ?? _customValue;
                      _customColor = d.color;
                    }),
                    onChangeBegin: (_) => _log('自定义滑块:拖动开始'),
                    onChangeEnd: (d) => _log('自定义滑块:结果 ${d.value?.toStringAsFixed(2)}'),
                  ),
                  if (_customColor != null)
                    Text('thumb 颜色:${_customColor.toString()}'),
                ],
              ),
            ),
          ),
          // 操作事件卡
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  const Text('操作事件(最近 8 条)',
                      style: TextStyle(fontWeight: FontWeight.w600)),
                  for (final e in _events)
                    Padding(
                      padding: const EdgeInsets.symmetric(vertical: 2),
                      child: Text(e),
                    ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}
7.3.1 4 场景行为差异
场景关键开关thumb 颜色来源回调 payload
① 纯色背景isGradientBg: falsecolors 首尾两色分别做为主色 / 辅色,thumb 取主色data.valuedata.color
② 渐变背景(悬浮 label)isGradientBg: trueisShowLabel: trueColor.lerp(colors[i], colors[i+1], percent)data.valuedata.color
③ 范围选择(双 thumb)isRange: truevalues: [20, 60]两个 thumb 各自按位置插值data.valuesdata.colors
④ 自定义样式thumbStylesliderStylecolors 三色数组三段渐变插值data.valuedata.color

7.4 页面退出时的异步处理

异步回调先检查 mounted,避免页面销毁后继续调用 setStateSliderGradient 不需要主动取消订阅,dispose 中无需额外清理。

多个页面都需要平台版本时,可以在应用启动阶段缓存一次 _platformVersion,各页面只读取缓存。


八、验证、构建与鸿蒙设备运行效果

8.1 分别验证插件与 example

从插件仓库根目录执行:

flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test

接口测试应覆盖 SliderGradient 的属性(value / colors / isRange / isShowLabel / thumbStyle / sliderStyle)、回调 payload(SliderData.value / values / color / colors)与边界条件(min < max 断言、isRange=truevalues 长度 / 顺序断言)。

当前仓库的运行结果为:flutter analyze 在根目录与 example 目录下合计报告 0 error、6 issue——must_be_immutableSliderGradientvalue / colors / values / divisions 是 mutable 字段,设计上由外部修改以联动 thumb 位置)、unused_element_isShowLabelClick)、invalid_null_aware_operatorsliderStyle?.height 在第 423 行,sliderStyle 非空)、test/slider_gradient_test.dart 中的 3 处未使用导入。flutter test 在本机因 flutter_testerWebSocketException: Invalid WebSocket upgrade request 无法启动,这是本地测试环境问题,不是被测代码缺陷。

8.2 确认设备连接

hdc list targets
flutter devices

设备首次连接电脑时,需要在手机端确认调试授权。本文适配使用的设备为 4UQ9K25508013016(HUAWEI nova 14,TLR-AL00),系统 ROM OpenHarmony-6.1.1.120(API 24),架构 ohos-arm64。列表为空时,检查 USB 连接、调试模式和电脑授权。

8.3 配置签名

真机安装的 HAP 通常需要有效签名。推荐使用 DevEco Studio 为 entry 模块配置自动签名:

  1. 用 DevEco Studio 打开 example/ohos,不是仓库根目录;
  2. 等待工程 Sync 成功,确认 Project 视图中存在 entry 模块;
  3. 打开 File > Project Structure > Signing Configs
  4. default product 选择或生成签名;
  5. 确认设备、应用包名 com.example.slider_gradient_example、证书和 Profile 匹配;
  6. 再回到终端执行 Flutter 构建或运行。

签名材料保存在本机,公开仓库中只保留构建所需的通用配置(详见 9.4)。

8.4 运行示例

以下命令在 example/ 目录执行,将 <device-id> 替换为设备列表中的实际 ID:

flutter run -d <device-id>

也可以先构建 HAP:

flutter build hap --debug

典型产物位于:

example/ohos/entry/build/default/outputs/default/

目录中通常包含已签名和未签名 HAP。真机安装应选择与当前设备匹配的已签名产物。

8.5 在设备上测试 4 个场景

  1. 打开应用,确认首屏渲染"平台信息"卡(显示 OpenHarmony 6.1.0.135(SP9C00E120R3P5))和"① 纯色背景"滑块;
  2. 拖动"① 纯色背景"滑块的 thumb,确认 thumb 沿轨道移动,操作事件卡出现"纯色滑块:拖动开始" / “纯色滑块:结果 N”;
  3. 滚动到"② 渐变背景",确认 thumb 上方出现悬浮 label(如 50.0),thumb 颜色随位置在红 / 绿之间插值;
  4. 滚动到"③ 范围选择",确认双 thumb 独立拖动且不互相穿越,触点自动选择距离更近的 thumb,回调 payload 包含 values: [v1, v2]colors: [c1, c2]
  5. 滚动到"④ 自定义样式",确认 thumb 尺寸(16×32,圆角 8)与轨道(高 18,圆角 9)符合 ThumbStyle / SliderStyle
  6. 点击右上角"重新查询"按钮,确认平台信息卡再次刷新,操作事件卡写入"通道调用成功:…";
  7. 切到后台再切回前台,确认页面状态不丢失;
  8. 卸载后重新安装,确认无缓存副作用。

8.6 鸿蒙设备运行效果

完成适配后,Flutter 应用能够在 OpenHarmony 6.1.1.120(API 24)真机上正常渲染 SliderGradient 的 4 个场景,通道桩 getPlatformVersion 返回 OpenHarmony 6.1.0.135(SP9C00E120R3P5)

下面是 6 张真机截图,分别对应首屏、中段(渐变 + 范围选择)、底部(自定义样式 + 操作事件):

首屏中段(滚动后)
平台信息卡显示 OpenHarmony 6.1.0.135(SP9C00E120R3P5) + ① 纯色背景滑块(value=30,colors [0xFF2196F3, 0xFFE3F2FD])+ ② 渐变背景(顶部)② 渐变背景悬浮 label 50.0,thumb 颜色沿渐变插值;③ 范围选择双 thumb(values=[20, 60],跨度 40)
截图来源:docs/evidence/01_first_screen.jpeg截图来源:docs/evidence/02_scrolled.jpeg
底部(继续滚动)
④ 自定义样式(value=100,thumb #FF9C27B0SliderStyle(height: 18, radius: 9))+ 操作事件卡(最近 8 条)
截图来源:docs/evidence/03_bottom.jpeg

3 张截图均保留在仓库的 docs/evidence/ 目录下,便于评审时核对。SliderGradient 是纯 Dart 组件,没有额外的性能热点;OHOS 上运行效果与 Android / iOS 上等价,差异主要来自 Flutter 引擎本身。


KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页 KeyHash 示例页

我先读取这 6 张截图,确认每张内容后再统一整理成表格。

6 张截图均为同一个 SliderGradient 示例页(OpenHarmony TLR-AL00,6.1.0.135(SP9C00E120R3P5))在不同拖动状态下的快照。整理成两张表:

表 1 · 页面模块与配置说明

模块关键配置预期表现
平台信息MethodChannel("slider_gradient").invokeMethod("getPlatformVersion")展示运行平台版本、通道/方法名,提供「重新查询」按钮
① 纯色背景isGradientBg=false轨道为主色/辅色双色填充,下方展示当前值、百分比、Thumb 颜色
② 渐变背景(悬浮 label)isGradientBg=true(默认)、isShowLabel=true蓝→青→绿→黄渐变轨道,thumb 上方悬浮 label 实时显示数值
③ 范围选择(双 thumb)isRange=true红→绿渐变轨道,双 thumb;拖动时自动选中距离更近的 thumb,展示左/右端值、跨度、左右 Thumb 颜色
④ 自定义样式thumb 16×32、圆角 8;轨道高度 18;三色渐变橙→红→紫三色渐变轨道,自定义圆角 thumb,悬浮 label 显示数值
操作事件(最近 8 条)滚动记录滑块结果、拖动开始、通道调用等事件,最新在最前

表 2 · 各截图实测状态快照

截图时刻① 纯色背景(当前值 / 百分比 / Thumb 颜色)② 渐变背景(当前值 / 百分比 / label / Thumb 颜色)③ 范围选择(左 / 右 / 跨度 / 左色 / 右色)④ 自定义样式(当前值 / 百分比 / label)操作事件记录
22:0230 / 30.0% / -50.00 / 50.0% / 50.0 / -未滚到未滚到
22:03(a)75 / 75.0% / #FF2196F350.00 / 50.0% / 50.0 / -未滚到未滚到
22:03(b)未显示50.00 / 50.0% / 50.0 / -20 / 60 / 40 / - / -70.00 / - / 70.0
22:04未显示未显示20 / 60 / 40 / - / -70.00 / 70.0% / 70.01. 纯色滑块:结果 75(75.0%);2. 纯色滑块:拖动开始;3. 通道调用成功:OpenHarmony TLR-AL00 6.1.0.135(SP9C00E120R3P5)
22:05未显示53.00 / 53.0% / 53.0 / #FF5BE2BB18 / 83 / 65 / #FFDF6352 / #FF6A9F5056.00 / - / 56.0
22:0640 / 40.0% / #FF2196F353.00 / 53.0% / 53.0 / #FF5BE2BB35 / 83 / 48 / #FFC07351 / -未滚到

走查结论:四个示例卡片渲染正常;拖动过程中当前值、百分比、悬浮 label、Thumb 颜色(随轨道位置自动取色)均实时联动更新;范围滑块双 thumb 可独立拖动、跨度随动;MethodChannel 通道调用成功返回平台版本;操作事件按顺序落盘,符合预期。

以下是操作的视屏,可以参考一下:

Example 启动授权 Example 启动授权 Example 启动授权



九、FAQ:适配过程与使用问题

9.1 OHOS 上需要写原生 ArkTS 代码吗?

要写一个约 77 行的通道桩,复刻 getPlatformVersion 契约;但组件本体无需任何原生逻辑。

SliderGradient 本身是纯 Dart 组件(lib/slider_gradient.dart 中没有任何 MethodChannel / Platform / invokeMethod 调用),所有交互(GestureDetector / AnimationController / 自定义布局 _ModalSliderLayout / Color.lerp)都在 Flutter 框架内完成。因此 OHOS 上只要 Flutter 引擎能跑起来,组件就自然可用。

但为了保持与 Android / iOS 端完全一致的通道契约(通道名 slider_gradient、方法 getPlatformVersion),本次适配补全了:

  • pubspec.yamlflutter.plugin.platforms 新增 ohos: pluginClass: SliderGradientPlugin(同时移除原 web: 平台);
  • 根目录 ohos/src/main/ets/components/plugin/SliderGradientPlugin.ets(77 行),注册同名通道,读取 deviceInfo.displayVersion 返回 `OpenHarmony ${deviceInfo.displayVersion}`,其他方法统一 notImplemented()
  • ohos/index.ets 导出实现,GeneratedPluginRegistrant.ets 自动注入注册代码。

如果未来插件作者在 OHOS 上提供真正的平台差异(例如读取特定传感器),只需把 notImplemented 替换为实际实现并保持通道契约即可。

9.2 为什么必须做空安全迁移?

SliderGradient 上游代码是 pre-null-safety 写法,而 OHOS 版 Flutter 3.44 自带 Dart 3.12,强制 null-safety。旧语法在编译期直接报错(undefined_named_parameter.keynon_nullable_field_not_initialized 等)。"Dart 层零改动"在此场景不可行。

迁移的最小化原则:

  • Key keyKey? key@requiredrequired
  • 回调 / 数值 / 颜色字段改可空(SliderChangeCallback?double?Color?String?List<Color>?);
  • initData 中给 _beginColor / _endColorlate 字段显式赋值;
  • widget.colors 实例字段无法参与类型提升,改用局部变量 final colors = widget.colors
  • _doubleToInt 删除 dead null check,直接 int.parse(val.toString().split('.')[0])
  • pubspec.yamlsdk: ">=2.7.0 <3.0.0"sdk: ">=2.12.0 <4.0.0"

API 名称、语义、默认值全部保持不变,对 Flutter 2.12+ 用户透明;Flutter 2.12 以下用户继续使用 pub.dev 原版本。

9.3 TAG 不一致问题的说明与修复建议

这是本次适配最重要的遗留问题。 README.OpenHarmony_CN.mdCHANGELOG.OpenHarmony.md 声明的 TAG 名是 0.2.0-ohos-1.0.0-beta.1,但仓库中根本不存在这个名字的 TAG:

git tag -l
# => 0.1.0
# => 0.1.1
# => 0.2.0
# => 0.2.1   ← 实际指向 OHOS 适配提交 b9d2948

如果用户按 README.OpenHarmony_CN.md 的示例配置 ref: 0.2.0-ohos-1.0.0-beta.1 来依赖,flutter pub get 会因为找不到该 TAG 而报错;正确做法是使用真实存在的 ref: 0.2.1 或直接 ref: main

修复建议:

  • 短期:保留现 0.2.1 TAG 不动,把 README.OpenHarmony_CN.md / README.OpenHarmony.md / CHANGELOG.OpenHarmony.md 中所有 0.2.0-ohos-1.0.0-beta.1 替换为 0.2.1,并补一行注释说明 “TAG 命名规则为 原库版本-ohos-版本号,本次实际以 0.2.1 落地”。
  • 长期:按 原库版本-ohos-版本号(README 中声明的规则)重新打 TAG,并在合并请求中追加 git tag -f 0.2.0-ohos-1.0.0-beta.1 b9d2948,避免历史消费者继续踩坑。

本仓库的 pubspec.yamlversion 也仍是 0.2.0未随 OHOS 适配 bump(见 9.7)。

9.4 签名材料入库问题

example/ohos/build-profile.json5 中包含本地调试签名材料,已随本次提交进入公开仓库:

signingConfigs[*].material.certpath = /Users/david/.ohos/config/default_ohos_y-fnKD-R1pebw874YSsfbf9rxqrjqGGjJn2J0eUmMRM=.cer
signingConfigs[*].material.storeFile = /Users/david/.ohos/config/default_ohos_y-fnKD-R1pebw874YSsfbf9rxqrjqGGjJn2J0eUmMRM=.p12
signingConfigs[*].material.profile  = /Users/david/.ohos/config/default_ohos_y-fnKD-R1pebw874YSsfbf9rxqrjqGGjJn2J0eUmMRM=.p7b
keyPassword  = 0000001A23A16F8C0D152EA655E138164E57BCD4FDC31B51C2FF81C27D77ABA03B4BE433A7E4F9FE07D2
storePassword = 0000001A11F982339A5D0733FA9B3FC8CDD82F3696BDD34E8BE5E185B6D733F29BA486FE96FBA4C9776F

certpath / storeFile / profile 是本机绝对路径,离开本机就找不到文件;keyPassword / storePassword 是调试签名口令(Base64 字符串),一旦泄露需要轮换证书。

处理顺序:

  1. 本地立即轮换调试证书:在 DevEco Studio 删除现有自动签名,重新生成一份;

  2. 剥离仓库中的敏感配置:把 example/ohos/build-profile.json5signingConfigs 替换为通用占位:

    {
      "app": {
        "signingConfigs": [
          { "name": "default", "type": "HarmonyOS" }
        ],
        "products": [
          { "name": "default", "signingConfig": "default",
            "compatibleSdkVersion": "5.1.0(18)", "runtimeOS": "HarmonyOS" }
        ]
      },
      "modules": [
        { "name": "entry", "srcPath": "./entry",
          "targets": [{ "name": "default", "applyToProducts": ["default"] }] }
      ]
    }
    
  3. 提交一个清理 PR:在 PR 描述中说明 git diff -- example/ohos/build-profile.json5 已不再包含绝对路径或口令;

  4. 避免再次写入:使用 DevEco Studio 时关闭"保存签名到项目",或把 example/ohos/build-profile.json5 加入本地 .gitignore(注意:这会影响其他贡献者,需要在 README 中说明)。

9.5 通道 getPlatformVersion 在真机上返回什么

ArkTS 端读取 deviceInfo.displayVersion,再拼上 OpenHarmony 前缀返回:

const version = `OpenHarmony ${deviceInfo.displayVersion}`;
result.success(version);

在 HUAWEI nova 14(TLR-AL00,OpenHarmony-6.1.1.120 ROM)上的实测返回:

OpenHarmony 6.1.0.135(SP9C00E120R3P5)

注意 deviceInfo.displayVersion 包含完整的 ROM build 号(SP9C00E120R3P5),不是 OpenHarmony-6.1.1.120 这种"用户视角"的系统版本号。README.OpenHarmony_CN.md 中"ROM: OpenHarmony-6.1.1.120"指的是设备 ROM 版本,而 getPlatformVersion 实际返回的是底层 displayVersion 字符串,两者差异属正常现象。

如果业务需要的是"用户视角"的版本号,应该在 Dart 侧用正则从返回值里抽取主版本号,或在 ArkTS 端改用其他字段(如 deviceInfo.majorVersion + deviceInfo.minorVersion 自行拼接)。

9.6 华为设备上 label 高度计算偏小

lib/slider_gradient.dartlabelTextHeightTextPainter 显式传入了 locale: Localizations.localeOf(context),并在源码中保留了一条注释:

//AUTO:华为手机如果不指定locale的时候,该方法算出来的文字高度是比系统计算偏小的。
TextPainter painter = TextPainter(
  locale: Localizations.localeOf(context),
  ...
);

迁移到 null-safety 后,方法签名从 String value 改为 String? value,并在函数开头加 if (value == null || value.length == 0) return 0;。OHOS 真机(同样存在类似机型差异)上保留这一行为;不要把 locale 改成 LOCALES.first,否则华为设备上 label 会被裁剪。

9.7 pubspec.yamlversion 仍然是 0.2.0

本次适配没有 bump pubspec.yamlversion 字段(仍是 0.2.0),但实际 git TAG 已经打在 0.2.1,README 中又声称 TAG 是 0.2.0-ohos-1.0.0-beta.1,三者目前是不一致的。

上游 0.2.0 是 OHOS 适配前的最后一个 TAG,OHOS 适配没有引入新功能(Dart 迁移 + 通道桩属于"适配工作"),从语义上是否 bump 都可以。但如果按 TAG 命名规则 原库版本-ohos-版本号 打 TAG(见 9.3),通常需要把 pubspec.yamlversion 也调整为 0.2.0-ohos-1.0.0-beta.1 这种带后缀的形式——而 Dart pub 规范不支持带 - 后缀的版本。

建议:

  • 若不准备上游发布:保持 pubspec.yamlversion: 0.2.0 不动,TAG 用真实存在的 0.2.1(或后续 bump 到 0.2.2),README 中如实写明"基于 slider_gradient 0.2.0 适配";
  • 若准备上游发布:先在 PR 中与上游作者协商 bump 策略(如 0.2.1 兼容 OHOS + null-safety 迁移),再合并。

9.8 LICENSE 与 oh-package.json5license 不一致

LICENSE 文件声明的是 MIT,但 ohos/oh-package.json5license 字段写的是 "Apache-2.0"

{
  "name": "slider_gradient",
  "version": "1.0.0",
  "license": "Apache-2.0",
  "dependencies": {}
}

更奇怪的是,LICENSE 文件的 copyright 行写着 Radoslav Vitanov <radoslav.vitanov@icloud.com> (https://github.com/Sh1d0w)——这看起来像是从其它项目拷贝过来的模板残留,并不是 slider_gradient 的真实作者(GitHub 上 dilireba521)。

本文仅记录该不一致,不作修改。建议:

  • 保留 LICENSE 不动:插件作者在适配前已通过 git log / LICENSE 内容确认这是上游的现状;
  • oh-package.json5license 改为 "MIT":与上游保持一致;如果上游未来调整为 Apache-2.0,再同步修改;
  • LICENSE 的 copyright 行:提一个上游 issue,让作者确认是否需要替换为真实作者(dilireba521);如果替换,需要在 PR 中同步更新。

9.9 编译成功但安装失败

常见原因包括:

  • HAP 未签名或使用了错误的 Profile;
  • 设备未加入调试设备列表;
  • 包名与签名 Profile 不匹配(AppScope/app.json5 中的 bundleName 必须与 Profile 中的 bundle-name 一致,本例已确认为 com.example.slider_gradient_example);
  • 安装包的 compatibleSdkVersion 高于设备 API;
  • 手机上已经安装了使用不同证书签名的同包名应用。

根据安装错误码区分签名、版本和包名冲突,再处理对应配置。hvigor 在 SignHap 阶段强校验 hap 内 bundleName 与签名 profile 中的 bundle-name 一致(错误码 00303074),调试 profile 只能由 DevEco Studio 用已实名认证的华为账号按 bundleName 申请生成,无法跨 bundle 复用。

9.10 MissingPluginException

MissingPluginException 通常表示 Dart 通道找不到已注册的原生插件。新增原生插件后需要重新构建应用:

cd example
flutter clean
flutter pub get
flutter run -d <device-id>

如果仍然出现,检查:

  • example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets 是否包含 import SliderGradientPlugin from 'slider_gradient';flutterEngine.getPlugins()?.add(new SliderGradientPlugin());
  • pubspec.yamlflutter.plugin.platforms.ohos.pluginClass 是不是 SliderGradientPlugin
  • ohos/index.ets 是否正确 export default SliderGradientPlugin
  • 插件的 ohos/oh-package.json5name 字段是不是 slider_gradient(与 GeneratedPluginRegistrant 中的 import ... from 'slider_gradient' 对应)。

由于组件本体不经过通道,仅在 demo 调用 getPlatformVersion 时需要插件注册;其他场景不会触发 MissingPluginException

9.11 flutter create 不认识 ohos,或包名不合法

先执行 flutter --version,确认使用的是 OHOS 版工具链,环境配置回到第二节的社区链接核对。包名报错时,确认当前目录包含目标插件的 pubspec.yaml,并显式传入 --project-name slider_gradient;仓库名 slider_gradient 已经是合法 Dart 包名,直接使用即可。生成后检查 diff,再补充 ArkTS 业务实现(本例仅替换 SliderGradientPlugin.ets 为 77 行通道桩)。

如果 flutter create . 在仓库根目录崩溃(Xcode 12.5 过旧会导致 xcodebuild 探测抛 ProcessException),可在干净临时目录用 flutter create slider_gradient --template=plugin --platforms=ohos --no-pub 生成,再把 ohos/example/ohos/ 拷入仓库。


相关链接

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐