@ohos-cpf/3rdloop 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/vendor/Server/Routes/controllers/LoopEngineController.js +36 -3
- package/vendor/Server/Routes/controllers/OrchestratorController.js +193 -9
- package/vendor/Server/Skills/flutter-build-test/SKILL.md +252 -0
- package/vendor/Server/Skills/flutter-build-test/assets/BUILDENV_TEMPLATE.md +42 -0
- package/vendor/Server/Skills/flutter-build-test/assets/BUILD_REPORT_TEMPLATE.md +64 -0
- package/vendor/Server/Skills/flutter-build-test/assets/README_SECTION_TEMPLATE.md +78 -0
- package/vendor/Server/Skills/flutter-build-test/references/BUILD_TROUBLESHOOTING.md +128 -0
- package/vendor/Server/Skills/flutter-build-test/references/DOC_UPDATE_GUIDE.md +119 -0
- package/vendor/Server/Skills/flutter-build-test/references/FLVM_GUIDE.md +89 -0
- package/vendor/Server/Skills/flutter-build-test/scripts/build-matrix.cjs +374 -0
- package/vendor/Server/Skills/flutter-build-test/scripts/locate-example.cjs +189 -0
- package/vendor/Server/Skills/flutter-build-test/scripts/update-buildenv.cjs +171 -0
- package/vendor/Server/Skills/flutter-code-use/SKILL.md +316 -0
- package/vendor/Server/Skills/flutter-code-use/references/event-channel.md +440 -0
- package/vendor/Server/Skills/flutter-code-use/references/federated.md +295 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-binding-translate.md +130 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-compile-from-source.md +169 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-fetch-at-build.md +161 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-prebuilt-bundle.md +175 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-rhttp-guide.md +235 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi-rust-cross-compile.md +514 -0
- package/vendor/Server/Skills/flutter-code-use/references/ffi.md +220 -0
- package/vendor/Server/Skills/flutter-code-use/references/method-channel.md +643 -0
- package/vendor/Server/Skills/flutter-code-use/references/monorepo.md +188 -0
- package/vendor/Server/Skills/flutter-code-use/references/ohos-api-pitfalls.md +717 -0
- package/vendor/Server/Skills/flutter-code-use/references/platform-view.md +448 -0
- package/vendor/Server/Skills/flutter-code-use/references/pure-dart.md +180 -0
- package/vendor/Server/Skills/flutter-code-use/references/texture.md +459 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/SKILL.md +270 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/assets/PAGE_TEMPLATES.md +544 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/references/CODE_STANDARDS.md +328 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/references/DEMO_DOC_PARSING.md +126 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/references/EXAMPLES.md +629 -0
- package/vendor/Server/Skills/flutter-demo-code-generator/scripts/validate-flutter-demo.cjs +268 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/SKILL.md +227 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/assets/DEMO_DOC_TEMPLATE.md +78 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/COVERAGE_REPORT_PARSING.md +174 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/EXAMPLES.md +162 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/MCP_TOOL_GUIDE.md +123 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/OUTPUT_FORMAT.md +190 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/references/QUALITY_CHECKLIST.md +83 -0
- package/vendor/Server/Skills/flutter-demo-doc-generator/scripts/validate-skill.cjs +259 -0
- package/vendor/Server/Skills/flutter-library-demo-coverage/SKILL.md +175 -0
- package/vendor/VERSION +3 -3
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# MethodChannel 插件鸿蒙适配
|
|
2
|
+
|
|
3
|
+
## 适用条件
|
|
4
|
+
|
|
5
|
+
- 插件主要通过 `MethodChannel` 实现 Dart ↔ 原生通信
|
|
6
|
+
- 插件架构为 standalone(独立插件)
|
|
7
|
+
- 可能同时包含 EventChannel(混合模式),但以 MethodChannel 为主
|
|
8
|
+
|
|
9
|
+
> `type-method-channel` 只表示**主通信模式**,不表示实现只能有单个 `Plugin.ets`。如果主方案还依赖页面/容器、PlatformView/XComponent/Texture、UIAbility、辅助组件、桥接层或额外 Dart/ETS 文件,仍应在此基础上扩展完整结构,并补充读取对应的辅助类型指导文件。辅助承载层、预览层、纹理层、桥接层本身不等于公开 API 必然变化;若可被封装在插件内部,应优先保持原公开调用方式与行为兼容。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 第一部分:工程配置
|
|
14
|
+
|
|
15
|
+
> `ohos/` 目录由 `flutter create -t plugin --platforms ohos .` 自动生成(见 prompt 步骤 3.1)。本部分只描述对生成结果的自定义配置。**不要手动创建 `build-profile.json5`、`hvigorfile.ts`、`module.json5` 等配置文件。**
|
|
16
|
+
|
|
17
|
+
### pubspec.yaml — 添加 ohos 平台声明(必须)
|
|
18
|
+
|
|
19
|
+
`flutter create` 不会自动修改 `pubspec.yaml`,需要手动添加 ohos 平台:
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
flutter:
|
|
23
|
+
plugin:
|
|
24
|
+
platforms:
|
|
25
|
+
ohos:
|
|
26
|
+
package: com.example.xxx
|
|
27
|
+
pluginClass: XxxPlugin
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### oh-package.json5 — 添加三方依赖(按需)
|
|
31
|
+
|
|
32
|
+
如果插件需要额外的 ohpm 三方包(来自 `02-planning.json` 的 `native_dependency_mapping`),在 `flutter create` 生成的 `ohos/oh-package.json5` 的 `dependencies` 中追加:
|
|
33
|
+
|
|
34
|
+
```json5
|
|
35
|
+
{
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@ohos/some_package": "^1.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> `@ohos/flutter_ohos` 依赖由 Flutter 构建工具自动注入,无需手动添加。
|
|
43
|
+
|
|
44
|
+
### build-profile.json5 — Bytecode HAR 与 SDK 版本配置(按需)
|
|
45
|
+
|
|
46
|
+
如果添加的 ohpm 依赖使用 **Bytecode HAR** 格式(如 `@ohos/rive`、`@ohos/lottie` 等预编译库),或依赖声明了最低 SDK 版本要求,需要在 **Example 工程级** `example/ohos/build-profile.json5` 中做以下配置:
|
|
47
|
+
|
|
48
|
+
#### Bytecode HAR 支持
|
|
49
|
+
|
|
50
|
+
Bytecode HAR 包需要启用 `useNormalizedOHMUrl`,否则编译报错 `"Bytecode HARs: [@ohos/xxx] not supported when useNormalizedOHMUrl is not true"`:
|
|
51
|
+
|
|
52
|
+
```json5
|
|
53
|
+
{
|
|
54
|
+
"app": {
|
|
55
|
+
"products": [
|
|
56
|
+
{
|
|
57
|
+
"name": "default",
|
|
58
|
+
"signingConfig": "default",
|
|
59
|
+
"compatibleSdkVersion": "5.0.0(12)",
|
|
60
|
+
"buildOption": {
|
|
61
|
+
"strictMode": {
|
|
62
|
+
"useNormalizedOHMUrl": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
> **注意**:`useNormalizedOHMUrl` 必须在 `app.products[].buildOption.strictMode` 下,而非直接放在 products 层级。
|
|
72
|
+
|
|
73
|
+
#### compatibleSdkVersion 升级
|
|
74
|
+
|
|
75
|
+
如果 ohpm 依赖要求更高的 SDK 版本(编译报错 `"The project's compatibleSdkVersion: XXX cannot be lower than the minimum compatible version YYY"`),需要在 `app.products[].compatibleSdkVersion` 中升级版本号。
|
|
76
|
+
|
|
77
|
+
**检查步骤**:
|
|
78
|
+
1. 查看依赖包的 README 或 `oh-package.json5` 确认最低版本要求
|
|
79
|
+
2. 在 `build-profile.json5` 中升级 `compatibleSdkVersion`(如 `"5.0.0(12)"` → `"5.0.5(17)"`)
|
|
80
|
+
3. 升级后重新执行 `ohpm install` 和编译
|
|
81
|
+
|
|
82
|
+
> HarmonyOS 版本号格式:`"M.S.F(API Level)"`,如 `"5.0.5(17)"` 表示 API Level 17。
|
|
83
|
+
|
|
84
|
+
### modelVersion 一致性检查(必须)
|
|
85
|
+
|
|
86
|
+
OHOS 构建系统要求 ohpm 配置与 hvigor 配置的 `modelVersion` 完全一致,不一致时 hvigor 构建直接阻断,报错:"ohpm配置的modelVersion与hvigor配置的modelVersion不一致"。
|
|
87
|
+
|
|
88
|
+
**检查范围**(以下文件的 `modelVersion` 字段必须相同):
|
|
89
|
+
1. `example/ohos/hvigor/hvigor-config.json5` — 以此为基准值
|
|
90
|
+
2. `example/ohos/oh-package.json5` — 项目根目录
|
|
91
|
+
3. `example/ohos/entry/oh-package.json5` — entry 模块
|
|
92
|
+
|
|
93
|
+
**修复方式**:如果 `oh-package.json5` 中缺少 `modelVersion` 字段或值与 `hvigor-config.json5` 不一致,添加或修正为一致的值(范围 5.0.0 ~ 6.1.0)。
|
|
94
|
+
|
|
95
|
+
### module.json5 — 添加权限声明(按需)
|
|
96
|
+
|
|
97
|
+
如果插件需要系统权限,在 `flutter create` 生成的 `ohos/src/main/module.json5` 的 `module` 下追加 `requestPermissions`:
|
|
98
|
+
|
|
99
|
+
```json5
|
|
100
|
+
{
|
|
101
|
+
"module": {
|
|
102
|
+
"name": "xxx_plugin",
|
|
103
|
+
"type": "har",
|
|
104
|
+
"deviceTypes": ["default", "tablet"],
|
|
105
|
+
"requestPermissions": [
|
|
106
|
+
{
|
|
107
|
+
"name": "ohos.permission.INTERNET",
|
|
108
|
+
"reason": "$string:internet_reason",
|
|
109
|
+
"usedScene": { "abilities": ["EntryAbility"], "when": "always" }
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> **`user_grant` 权限必须运行时申请**:HarmonyOS 权限分为 `system_grant`(安装时自动授予,如 `INTERNET`)和 `user_grant`(需用户弹窗确认,如 `READ_PASTEBOARD`、`CAMERA`、`MICROPHONE`、`READ_MEDIA`、`ACCESS_BLUETOOTH`)。仅在 `module.json5` 中声明 `user_grant` 权限**不会**自动授予——首次调用受保护 API 时,系统会直接拒绝(返回空数据、错误码或异常),且**不会**自动弹出授权弹窗。
|
|
117
|
+
>
|
|
118
|
+
> **规则**:凡使用 `user_grant` 权限的插件,**必须**:
|
|
119
|
+
> 1. 实现 `AbilityAware` 接口获取 `UIAbilityContext`
|
|
120
|
+
> 2. 在首次调用受保护 API 前,调用 `abilityAccessCtrl.createAtManager().requestPermissionsFromUser(context, permissions)` 申请权限
|
|
121
|
+
> 3. 处理用户拒绝的情况(返回有意义的错误,而非静默失败)
|
|
122
|
+
>
|
|
123
|
+
> **禁止**:仅调用 `verifyAccessToken()` 检查权限状态而不调用 `requestPermissionsFromUser()` 申请权限。`verifyAccessToken` 是检查(check),不是申请(request)。
|
|
124
|
+
>
|
|
125
|
+
> ```ets
|
|
126
|
+
> import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
|
|
127
|
+
>
|
|
128
|
+
> private async ensurePermission(permission: Permissions): Promise<boolean> {
|
|
129
|
+
> const atManager = abilityAccessCtrl.createAtManager();
|
|
130
|
+
> const bundleInfo = await bundleManager.getBundleInfoForSelf(
|
|
131
|
+
> bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
|
|
132
|
+
> );
|
|
133
|
+
> const tokenId = bundleInfo.appInfo.accessTokenId;
|
|
134
|
+
> const status = atManager.verifyAccessTokenSync(tokenId, permission);
|
|
135
|
+
> if (status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
|
|
136
|
+
> return true;
|
|
137
|
+
> }
|
|
138
|
+
> // 未授予 → 弹窗申请
|
|
139
|
+
> if (this.context === null) {
|
|
140
|
+
> return false; // 无 UIAbilityContext,无法申请
|
|
141
|
+
> }
|
|
142
|
+
> const result = await atManager.requestPermissionsFromUser(this.context, [permission]);
|
|
143
|
+
> return result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
|
|
144
|
+
> }
|
|
145
|
+
> ```
|
|
146
|
+
>
|
|
147
|
+
> **常见 `user_grant` 权限参考**:
|
|
148
|
+
>
|
|
149
|
+
> | 权限 | 场景 |
|
|
150
|
+
> |------|------|
|
|
151
|
+
> | `ohos.permission.CAMERA` | 相机 |
|
|
152
|
+
> | `ohos.permission.MICROPHONE` | 录音 |
|
|
153
|
+
> | `ohos.permission.READ_PASTEBOARD` | 读取剪贴板(API 12+) |
|
|
154
|
+
> | `ohos.permission.ACCESS_BLUETOOTH` | 蓝牙 |
|
|
155
|
+
> | `ohos.permission.APPROXIMATELY_LOCATION` | 模糊定位 |
|
|
156
|
+
> | `ohos.permission.LOCATION` | 精确定位 |
|
|
157
|
+
> | `ohos.permission.READ_IMAGEVIDEO` / `WRITE_IMAGEVIDEO` | 图库读写 |
|
|
158
|
+
> | `ohos.permission.READ_AUDIO` / `WRITE_AUDIO` | 音频文件读写 |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 第二部分:编码实现
|
|
163
|
+
|
|
164
|
+
### 插件基本结构
|
|
165
|
+
|
|
166
|
+
> `flutter create` 会在 `ohos/src/main/ets/components/plugin/` 下生成一个基础插件模板。在此模板基础上扩展业务逻辑即可。
|
|
167
|
+
> 该模板只是起点,不是实现上限。不要因为当前类型是 `type-method-channel`,就默认判定“插件不能包含辅助页面/组件/承载层”或“需要宿主配合即不可实现”。若主方案需要额外承载能力,应继续新增对应 ETS/Dart 文件和桥接逻辑。
|
|
168
|
+
|
|
169
|
+
```ets
|
|
170
|
+
import {
|
|
171
|
+
FlutterPlugin,
|
|
172
|
+
FlutterPluginBinding,
|
|
173
|
+
MethodCall,
|
|
174
|
+
MethodCallHandler,
|
|
175
|
+
MethodChannel,
|
|
176
|
+
MethodResult,
|
|
177
|
+
} from '@ohos/flutter_ohos';
|
|
178
|
+
|
|
179
|
+
export default class XxxPlugin implements FlutterPlugin, MethodCallHandler {
|
|
180
|
+
private channel: MethodChannel | null = null;
|
|
181
|
+
|
|
182
|
+
getUniqueClassName(): string {
|
|
183
|
+
return "XxxPlugin";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onAttachedToEngine(binding: FlutterPluginBinding): void {
|
|
187
|
+
this.channel = new MethodChannel(binding.getBinaryMessenger(), "channel_name");
|
|
188
|
+
this.channel.setMethodCallHandler(this);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onDetachedFromEngine(binding: FlutterPluginBinding): void {
|
|
192
|
+
if (this.channel != null) {
|
|
193
|
+
this.channel.setMethodCallHandler(null);
|
|
194
|
+
}
|
|
195
|
+
this.channel = null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
onMethodCall(call: MethodCall, result: MethodResult): void {
|
|
199
|
+
switch (call.method) {
|
|
200
|
+
case "getPlatformVersion":
|
|
201
|
+
result.success("OpenHarmony");
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
result.notImplemented();
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 获取 Context
|
|
212
|
+
|
|
213
|
+
许多鸿蒙 API 需要 Context 参数,通过 `FlutterPluginBinding` 获取:
|
|
214
|
+
|
|
215
|
+
```ets
|
|
216
|
+
import {
|
|
217
|
+
FlutterPlugin,
|
|
218
|
+
FlutterPluginBinding,
|
|
219
|
+
MethodCall,
|
|
220
|
+
MethodCallHandler,
|
|
221
|
+
MethodChannel,
|
|
222
|
+
MethodResult,
|
|
223
|
+
} from '@ohos/flutter_ohos';
|
|
224
|
+
import common from '@ohos.app.ability.common';
|
|
225
|
+
|
|
226
|
+
export default class XxxPlugin implements FlutterPlugin, MethodCallHandler {
|
|
227
|
+
private channel: MethodChannel | null = null;
|
|
228
|
+
private context: common.Context | null = null;
|
|
229
|
+
|
|
230
|
+
getUniqueClassName(): string {
|
|
231
|
+
return "XxxPlugin";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onAttachedToEngine(binding: FlutterPluginBinding): void {
|
|
235
|
+
this.context = binding.getApplicationContext();
|
|
236
|
+
this.channel = new MethodChannel(binding.getBinaryMessenger(), "channel_name");
|
|
237
|
+
this.channel.setMethodCallHandler(this);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
onDetachedFromEngine(binding: FlutterPluginBinding): void {
|
|
241
|
+
this.context = null;
|
|
242
|
+
if (this.channel != null) {
|
|
243
|
+
this.channel.setMethodCallHandler(null);
|
|
244
|
+
}
|
|
245
|
+
this.channel = null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
onMethodCall(call: MethodCall, result: MethodResult): void { /* ... */ }
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
如需 UIAbilityContext(用于权限申请、启动 Ability 等),插件必须额外实现 `AbilityAware` 接口。**`FlutterPluginBinding` 上没有 `getAbility()` 方法**,`getAbility()` 属于 `AbilityPluginBinding`,只能在 `onAttachedToAbility` 回调中获取:
|
|
253
|
+
|
|
254
|
+
```ets
|
|
255
|
+
import {
|
|
256
|
+
FlutterPlugin,
|
|
257
|
+
FlutterPluginBinding,
|
|
258
|
+
MethodCall,
|
|
259
|
+
MethodCallHandler,
|
|
260
|
+
MethodChannel,
|
|
261
|
+
MethodResult,
|
|
262
|
+
AbilityAware,
|
|
263
|
+
AbilityPluginBinding,
|
|
264
|
+
} from '@ohos/flutter_ohos';
|
|
265
|
+
import { common } from '@kit.AbilityKit';
|
|
266
|
+
|
|
267
|
+
export default class XxxPlugin implements FlutterPlugin, MethodCallHandler, AbilityAware {
|
|
268
|
+
private context: common.UIAbilityContext | null = null;
|
|
269
|
+
|
|
270
|
+
getUniqueClassName(): string {
|
|
271
|
+
return "XxxPlugin";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// FlutterPlugin 生命周期
|
|
275
|
+
onAttachedToEngine(binding: FlutterPluginBinding): void {
|
|
276
|
+
// binding 上只有 getApplicationContext()、getBinaryMessenger() 等
|
|
277
|
+
// 没有 getAbility()!
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
onDetachedFromEngine(binding: FlutterPluginBinding): void { }
|
|
281
|
+
|
|
282
|
+
onMethodCall(call: MethodCall, result: MethodResult): void { /* ... */ }
|
|
283
|
+
|
|
284
|
+
// AbilityAware 生命周期 — 在这里获取 UIAbility
|
|
285
|
+
onAttachedToAbility(binding: AbilityPluginBinding): void {
|
|
286
|
+
const ability = binding.getAbility();
|
|
287
|
+
if (ability !== null && ability !== undefined) {
|
|
288
|
+
this.context = ability.context as common.UIAbilityContext;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
onDetachedFromAbility(): void {
|
|
293
|
+
this.context = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
> **注意**:不是所有插件都需要 `AbilityAware`。仅当插件需要 UIAbilityContext(如权限申请、跳转 Ability、窗口操作等)时才实现此接口。仅需 ApplicationContext(如文件读写、偏好设置等)的插件,直接用 `binding.getApplicationContext()` 即可。**但凡插件声明了 `user_grant` 权限(如 `CAMERA`、`READ_PASTEBOARD`、`ACCESS_BLUETOOTH` 等),就必须实现 `AbilityAware`** ——因为 `requestPermissionsFromUser()` 需要 `UIAbilityContext`。
|
|
299
|
+
|
|
300
|
+
### UIAbilityContext 依赖 API 清单
|
|
301
|
+
|
|
302
|
+
以下鸿蒙 API 需要 `UIAbilityContext` 作为参数。如果插件使用其中任何一个,**必须**实现 `AbilityAware` 接口:
|
|
303
|
+
|
|
304
|
+
| API / Kit | 方法签名 | 常见用途 |
|
|
305
|
+
|-----------|---------|---------|
|
|
306
|
+
| **Share Kit** (`@kit.ShareKit`) | `ShareController.show(context: UIAbilityContext, options)` | 系统分享面板 |
|
|
307
|
+
| **Ability 跳转** (`@kit.AbilityKit`) | `context.startAbility(want: Want)` | 启动其他应用、系统页面、设置页 |
|
|
308
|
+
| **Ability 跳转** | `context.startAbilityForResult(want: Want)` | 启动并等待返回结果 |
|
|
309
|
+
| **运行时权限** (`@kit.AbilityKit`) | `context.requestPermissionsFromUser(permissions)` | 动态权限申请 |
|
|
310
|
+
| **窗口操作** (`@kit.ArkUI`) | `window.getLastWindow(context: Context)` | 获取主窗口、状态栏、全屏 |
|
|
311
|
+
| **通知授权** (`@kit.NotificationKit`) | `notificationManager.requestEnableNotification(context)` | 通知权限 |
|
|
312
|
+
| **短信发送** (`@kit.TelephonyKit`) | 需要 UIAbilityContext 启动短信应用 | 跳转短信编辑页 |
|
|
313
|
+
|
|
314
|
+
> **判断方法**:在 `harmonyos-sdk-api-lookup` 查到的 API 签名中,如果第一个参数类型为 `common.UIAbilityContext` 或其父类 `common.Context`(且文档说明需要 Ability 级别 Context),则需要 `AbilityAware`。
|
|
315
|
+
|
|
316
|
+
### 外部可变状态能力
|
|
317
|
+
|
|
318
|
+
如果插件暴露系统/设备状态,且该状态可被应用外部改变(系统设置、控制中心、权限设置、硬件连接、系统服务、其他应用等),MethodChannel 实现不能只等 `stateChange`。必须提供能力级 `refreshCurrentState(reason)` 或等价方法:主动查询当前真实状态,并按 Dart 侧既有 method、Map key、enum index 推送状态。
|
|
319
|
+
|
|
320
|
+
至少在状态监听注册成功后、Flutter restart / engine reattach 后、`AbilityAware.onAttachedToAbility` / reattach 后、窗口重新获焦或应用回到前台后调用 `refreshCurrentState(reason)`。事件监听负责未来变化;刷新入口负责恢复时校准 Dart 缓存。
|
|
321
|
+
|
|
322
|
+
```ets
|
|
323
|
+
private refreshCurrentState(reason: string): void {
|
|
324
|
+
const state = systemApi.getState();
|
|
325
|
+
this.methodChannel?.invokeMethod('OnStateChanged', { 'state': this.convertState(state) });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
onWindowFocusChanged(hasFocus: boolean): void {
|
|
329
|
+
if (hasFocus) {
|
|
330
|
+
this.refreshCurrentState('windowFocusChanged');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 参数提取
|
|
336
|
+
|
|
337
|
+
> **ArkTS 类型安全要求**:ArkTS 禁止将 `ESObject`(`call.argument` / `call.args` 的返回类型)直接用 `as` 强转为具体业务类型(如 `number`、`boolean`、`string`、`Array`)。直接写 `call.argument as number` 或 `call.argument("key") as number` 都会触发编译错误 `arkts-no-any-unknown`。**必须**先转为 `Object`,再转为目标类型。
|
|
338
|
+
|
|
339
|
+
> **关键:`call.args` 是 `Map<string, Object>`,不是 `Record`**:Flutter OHOS 的 MethodChannel 传递 Dart Map 参数时,ETS 端收到的是 `Map<string, Object>` 类型。**必须**用 `.get('key')` 读取值,**绝对禁止**用 `Record<string, Object>` 加 `args['key']` 方括号索引——Map 不支持方括号索引,`args['key']` 会静默返回 `undefined`,导致所有参数丢失。
|
|
340
|
+
|
|
341
|
+
```ets
|
|
342
|
+
// ❌ 错误:直接强转 ESObject → 具体类型(编译失败)
|
|
343
|
+
const mapType = call.argument("mapType") as number;
|
|
344
|
+
|
|
345
|
+
// ❌ 严重错误:用 Record 接收 args 并用方括号访问(运行时所有参数为 undefined)
|
|
346
|
+
const args = call.args as Record<string, Object>; // 类型错误!实际是 Map
|
|
347
|
+
const msg = args['message']; // 永远是 undefined
|
|
348
|
+
|
|
349
|
+
// ✅ 正确:先转 Object,再转目标类型
|
|
350
|
+
const mapType = (call.argument("mapType") as Object) as number;
|
|
351
|
+
const enabled = (call.argument("enabled") as Object) as boolean;
|
|
352
|
+
const name = (call.argument("name") as Object) as string;
|
|
353
|
+
|
|
354
|
+
// ✅ 正确:Map 参数提取(唯一正确方式)
|
|
355
|
+
const args = call.args as Map<string, Object>;
|
|
356
|
+
const name = args.get("name") as string;
|
|
357
|
+
const count = args.get("count") as number;
|
|
358
|
+
|
|
359
|
+
// ✅ 正确:Dart 侧传 null 的参数,Map.get() 返回 undefined 而非 null
|
|
360
|
+
// 必须同时检查 null 和 undefined
|
|
361
|
+
const optionalPath = args.get("filePath");
|
|
362
|
+
if (optionalPath === null || optionalPath === undefined) {
|
|
363
|
+
// filePath 未传或为 null
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
> **禁止模式汇总**:
|
|
368
|
+
> - `call.argument as number` / `call.argument("key") as number`:ESObject 直接强转,编译失败
|
|
369
|
+
> - `call.args as Record<string, Object>` + `args['key']`:**类型用错**,运行时参数全部丢失
|
|
370
|
+
> - 只检查 `=== null` 不检查 `=== undefined`:Dart null 经 Map.get() 可能为 undefined
|
|
371
|
+
|
|
372
|
+
> **Dart `int` 整数语义要求**:Dart API 类型为 `int`,或语义是整数存储、id、时间戳、文件大小、计数器、bit mask、rowId 等时,OHOS 端不得和 `double` 共用 `number` 链路。必须单独实现 int 提取、存储和返回链路,优先使用 `bigint` / int64 保真。只有明确证明取值不会超过安全小整数范围时,才允许降级为 `number`,并需要在实现说明中写明依据。
|
|
373
|
+
|
|
374
|
+
以下示例是 Pigeon/数组参数场景的核心写法;普通 MethodChannel 的 Map 参数不要照搬 `args[1]`,但必须保持同样的 int64/bigint 语义。
|
|
375
|
+
|
|
376
|
+
```ets
|
|
377
|
+
// ❌ 错误:setInt 和 setDouble 共用 number,可能丢失整数语义
|
|
378
|
+
const valueArg = args[1] as number;
|
|
379
|
+
api.setInt(keyArg, valueArg, optionsArg);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
```ets
|
|
383
|
+
// ✅ 正确:setInt 入口按 int64/bigint 承接
|
|
384
|
+
const valueArg = BigInt(args[1] as bigint);
|
|
385
|
+
api.setInt(keyArg, valueArg, optionsArg);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
```ets
|
|
389
|
+
// ✅ setInt/getInt 接口和实现都保持 bigint 语义,double 才使用 number
|
|
390
|
+
async setInt(key: string, value: bigint): Promise<void> {
|
|
391
|
+
return this.put(key, value);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async getInt(key: string, options: SharedPreferencesPigeonOptions): Promise<bigint | null> {
|
|
395
|
+
const value = await this.plugin.preferences.get(key, BigInt(0)) as ExtendedPreferenceValue;
|
|
396
|
+
if (typeof value === 'bigint' || typeof value === 'number') {
|
|
397
|
+
return BigInt(value);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
编码前必须阅读 Dart 侧实际 `invokeMethod` 与结果解析代码;不要因为 ETS 端更方便,就擅自改 `wire shape`。例如 Dart 端若期待 `List<String>`、或固定的 `Map key`,ETS 端就必须按该格式返回;若确需改成 `List<Map>` 等新结构,必须同步修改 Dart OHOS 分支,不得只改一侧。
|
|
405
|
+
|
|
406
|
+
### 异步方法处理
|
|
407
|
+
|
|
408
|
+
```ets
|
|
409
|
+
onMethodCall(call: MethodCall, result: MethodResult): void {
|
|
410
|
+
switch (call.method) {
|
|
411
|
+
case "asyncMethod":
|
|
412
|
+
this.handleAsyncMethod(call, result);
|
|
413
|
+
break;
|
|
414
|
+
default:
|
|
415
|
+
result.notImplemented();
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private async handleAsyncMethod(call: MethodCall, result: MethodResult): Promise<void> {
|
|
421
|
+
try {
|
|
422
|
+
const data = await someAsyncOperation();
|
|
423
|
+
result.success(data);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
result.error("ERROR_CODE", (err as Error).message, null);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### 混合模式(同时使用 MethodChannel + EventChannel)
|
|
431
|
+
|
|
432
|
+
部分 MethodChannel 插件同时含有 EventChannel(如网络状态插件:MethodChannel 控制 + EventChannel 推送),需同时实现 `MethodCallHandler` 和 `StreamHandler`:
|
|
433
|
+
|
|
434
|
+
```ets
|
|
435
|
+
import {
|
|
436
|
+
FlutterPlugin,
|
|
437
|
+
FlutterPluginBinding,
|
|
438
|
+
MethodCall,
|
|
439
|
+
MethodCallHandler,
|
|
440
|
+
MethodChannel,
|
|
441
|
+
MethodResult,
|
|
442
|
+
EventChannel,
|
|
443
|
+
EventSink,
|
|
444
|
+
StreamHandler,
|
|
445
|
+
} from '@ohos/flutter_ohos';
|
|
446
|
+
|
|
447
|
+
export default class XxxPlugin implements FlutterPlugin, MethodCallHandler, StreamHandler {
|
|
448
|
+
private methodChannel: MethodChannel | null = null;
|
|
449
|
+
private eventChannel: EventChannel | null = null;
|
|
450
|
+
private eventSink: EventSink | null = null;
|
|
451
|
+
|
|
452
|
+
getUniqueClassName(): string {
|
|
453
|
+
return "XxxPlugin";
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
onAttachedToEngine(binding: FlutterPluginBinding): void {
|
|
457
|
+
this.methodChannel = new MethodChannel(binding.getBinaryMessenger(), "method_channel_name");
|
|
458
|
+
this.methodChannel.setMethodCallHandler(this);
|
|
459
|
+
|
|
460
|
+
this.eventChannel = new EventChannel(binding.getBinaryMessenger(), "event_channel_name");
|
|
461
|
+
this.eventChannel.setStreamHandler(this);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
onDetachedFromEngine(binding: FlutterPluginBinding): void {
|
|
465
|
+
this.methodChannel?.setMethodCallHandler(null);
|
|
466
|
+
this.methodChannel = null;
|
|
467
|
+
this.eventChannel?.setStreamHandler(null);
|
|
468
|
+
this.eventChannel = null;
|
|
469
|
+
this.eventSink = null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
onMethodCall(call: MethodCall, result: MethodResult): void {
|
|
473
|
+
// MethodChannel 方法处理
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
onListen(args: Object, events: EventSink): void {
|
|
477
|
+
this.eventSink = events;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
onCancel(args: Object): void {
|
|
481
|
+
this.eventSink = null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### 返回复杂数据类型
|
|
487
|
+
|
|
488
|
+
```ets
|
|
489
|
+
// 返回 Map
|
|
490
|
+
const resultMap = new Map<string, Object>();
|
|
491
|
+
resultMap.set("status", "success");
|
|
492
|
+
resultMap.set("code", 200);
|
|
493
|
+
result.success(resultMap);
|
|
494
|
+
|
|
495
|
+
// 返回 List
|
|
496
|
+
const resultList: Array<Object> = [];
|
|
497
|
+
resultList.push("item1");
|
|
498
|
+
resultList.push(42);
|
|
499
|
+
result.success(resultList);
|
|
500
|
+
|
|
501
|
+
// 返回嵌套 Map(Dart 端收到 Map<String, dynamic>)
|
|
502
|
+
const outer = new Map<string, Object>();
|
|
503
|
+
const inner = new Map<string, Object>();
|
|
504
|
+
inner.set("lat", 39.9);
|
|
505
|
+
inner.set("lng", 116.3);
|
|
506
|
+
outer.set("location", inner);
|
|
507
|
+
result.success(outer);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
返回值的顶层类型、列表元素类型、Map key、字符串拼接格式和空值语义必须与 Dart 侧现有解析保持一致。
|
|
511
|
+
|
|
512
|
+
### 返回值类型安全(重要)
|
|
513
|
+
|
|
514
|
+
> **ETS `result.success()` 类型必须与 Dart `invokeMethod<T>` 泛型严格匹配。**
|
|
515
|
+
|
|
516
|
+
| Dart 期望 | ETS 正确返回 | 常见错误 |
|
|
517
|
+
|----------|-------------|---------|
|
|
518
|
+
| `bool` | `true` / `false` | ❌ `'true'` / `1` |
|
|
519
|
+
| `String?` 空值 | `null` | ❌ `'null'` |
|
|
520
|
+
| `int` | `123` | ❌ `'123'` |
|
|
521
|
+
|
|
522
|
+
**编码前必须读取 Dart 侧 `invokeMethod` 调用,确认泛型或赋值类型推断。**
|
|
523
|
+
|
|
524
|
+
### Dart ↔ ETS 类型映射表
|
|
525
|
+
|
|
526
|
+
| Dart 类型 | ETS 类型 | 说明 |
|
|
527
|
+
|-----------|----------|------|
|
|
528
|
+
| `String` | `string` | |
|
|
529
|
+
| `int` | `bigint` / int64 优先;小范围整数才可用 `number` | 不默认等同于 `number`;整数存储、id、时间戳、文件大小、计数器、bit mask、rowId 等必须走独立整数链路 |
|
|
530
|
+
| `double` | `number` | |
|
|
531
|
+
| `bool` | `boolean` | |
|
|
532
|
+
| `List` | `Array<Object>` | |
|
|
533
|
+
| `Map` | `Map<string, Object>` | |
|
|
534
|
+
| `Uint8List` | `Uint8Array` | Dart 侧**禁止**对 `Uint8List` 调用 `.toList()` 再传入 Channel——`toList()` 产生 `List<int>`,编码为 INT32 数组,ETS 侧收到 `Array<number>` 而非 `Uint8Array`,丢失 `.buffer` 属性 |
|
|
535
|
+
| `null` | `null` | |
|
|
536
|
+
|
|
537
|
+
> **Dart 侧接收数值的类型安全**:上表中 `double → number` 是 Dart→ETS 方向。反方向(ETS→Dart)存在陷阱:ETS 的 `number` 经 `StandardMessageCodec` 编码后,Dart 侧收到的 `dynamic` 可能是 `int`(整数值)或 `double`(浮点值)。Dart null-safe 不允许 `int` 隐式转 `double`。
|
|
538
|
+
>
|
|
539
|
+
> **规则**:Dart 侧从 `invokeMethod` 返回的 `Map` / `List` 中提取数值字段,目标类型为 `double` 时**必须**用 `(value as num).toDouble()`,目标类型为 `int` 时用 `(value as num).toInt()`。`invokeMethod<double>` 直接返回顶层 `double` 时框架自动处理,但嵌套在 `Map`/`List` 内的值仍为 `dynamic`,必须手动转换。
|
|
540
|
+
|
|
541
|
+
> **Dart 侧二进制数据传输安全**:Dart 向 ETS 传递二进制数据时,**必须**保持 `Uint8List` 类型直接传入 Channel,**禁止**先调用 `.toList()` 转为 `List<int>`。`StandardMessageCodec` 对 `Uint8List` 编码为字节数组(type byte 72),ETS 侧解码为 `Uint8Array`(有 `.buffer` 属性);但 `List<int>` 编码为 INT32 列表(type byte 12),ETS 侧解码为 `Array<number>`(无 `.buffer`),导致依赖 `ArrayBuffer` 的 API(如 `image.createImageSource`)崩溃。
|
|
542
|
+
>
|
|
543
|
+
> ```dart
|
|
544
|
+
> // ❌ 错误:toList() 将 Uint8List 转为 List<int>,破坏二进制编码
|
|
545
|
+
> channel.invokeMethod('copyImage', {'imageBytes': imageBytes.toList()});
|
|
546
|
+
>
|
|
547
|
+
> // ✅ 正确:直接传 Uint8List
|
|
548
|
+
> channel.invokeMethod('copyImage', {'imageBytes': imageBytes});
|
|
549
|
+
> ```
|
|
550
|
+
>
|
|
551
|
+
> ETS 侧如果无法确定收到的是 `Uint8Array` 还是 `Array<number>`(如需兼容旧 Dart 代码),应做防御性转换:
|
|
552
|
+
>
|
|
553
|
+
> ```ets
|
|
554
|
+
> // 防御性处理:兼容 Array<number> 和 Uint8Array
|
|
555
|
+
> let bytes: Uint8Array;
|
|
556
|
+
> if (rawData instanceof Uint8Array) {
|
|
557
|
+
> bytes = rawData;
|
|
558
|
+
> } else if (Array.isArray(rawData)) {
|
|
559
|
+
> bytes = new Uint8Array(rawData as number[]);
|
|
560
|
+
> }
|
|
561
|
+
> const buffer = bytes.buffer;
|
|
562
|
+
> ```
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 第三部分:常见编译错误与修复
|
|
567
|
+
|
|
568
|
+
### 1. `Cannot find module '@ohos.xxx'`
|
|
569
|
+
|
|
570
|
+
**原因**:导入了不存在的模块或模块名拼写错误。
|
|
571
|
+
|
|
572
|
+
**修复**:
|
|
573
|
+
- 确认模块名正确(检查 SDK `.d.ts` 文件)
|
|
574
|
+
- 部分 API 已迁移到 Kit 导入方式:`import { xxx } from '@kit.XxxKit'`
|
|
575
|
+
- 示例:`@ohos.net.http` → 可能需要 `import { http } from '@kit.NetworkKit'`
|
|
576
|
+
|
|
577
|
+
### 2. `Type 'xxx' is not assignable to type 'yyy'`
|
|
578
|
+
|
|
579
|
+
**原因**:ArkTS 严格类型检查,类型不匹配。
|
|
580
|
+
|
|
581
|
+
**修复**:
|
|
582
|
+
- 使用正确的类型断言:`call.argument("key") as string`
|
|
583
|
+
- Map 类型统一使用 `Map<string, Object>` 而非 `Record<string, any>`
|
|
584
|
+
- 可空类型需显式声明:`string | null`
|
|
585
|
+
|
|
586
|
+
### 3. `Property 'xxx' does not exist on type 'FlutterPluginBinding'`
|
|
587
|
+
|
|
588
|
+
**原因**:使用了不存在的 `FlutterPluginBinding` 方法。
|
|
589
|
+
|
|
590
|
+
**`FlutterPluginBinding` 上仅有以下方法**(无其他):
|
|
591
|
+
- `getApplicationContext()` — 获取 ApplicationContext
|
|
592
|
+
- `getBinaryMessenger()` — 获取 BinaryMessenger
|
|
593
|
+
- `getFlutterAssets()` — 获取 FlutterAssets
|
|
594
|
+
- `getFlutterEngine()` — 获取 FlutterEngine
|
|
595
|
+
- `getTextureRegistry()` — 获取 TextureRegistry
|
|
596
|
+
- `getPlatformViewRegistry()` — 获取 PlatformViewRegistry
|
|
597
|
+
|
|
598
|
+
**`FlutterPluginBinding` 上没有 `getAbility()`!** 如需获取 UIAbility,必须实现 `AbilityAware` 接口,在 `onAttachedToAbility(binding: AbilityPluginBinding)` 中通过 `binding.getAbility()` 获取。详见上方「获取 Context」一节。
|
|
599
|
+
|
|
600
|
+
### 4. `An object literal cannot have multiple properties with the same name`
|
|
601
|
+
|
|
602
|
+
**原因**:ETS 对象字面量中有重复的 key。
|
|
603
|
+
|
|
604
|
+
**修复**:检查代码中是否有重复的属性定义。
|
|
605
|
+
|
|
606
|
+
### 5. `MethodResult` 调用错误
|
|
607
|
+
|
|
608
|
+
**原因**:`result.success()` / `result.error()` 参数不对。
|
|
609
|
+
|
|
610
|
+
**修复**:
|
|
611
|
+
- `result.success(value)` — value 可以是任意可序列化类型或 null
|
|
612
|
+
- `result.error(errorCode, errorMessage, errorDetails)` — 三个参数,errorDetails 可为 null
|
|
613
|
+
- `result.notImplemented()` — 无参数
|
|
614
|
+
- 每个 `onMethodCall` 分支必须调用 result 的某个方法,否则 Dart 端会一直等待
|
|
615
|
+
|
|
616
|
+
### 6. `'async' modifier cannot be used here`
|
|
617
|
+
|
|
618
|
+
**原因**:ArkTS 中 interface 方法签名不能直接声明 async。
|
|
619
|
+
|
|
620
|
+
**修复**:
|
|
621
|
+
- `onMethodCall` 方法本身不要声明为 async
|
|
622
|
+
- 异步逻辑提取到独立的 async 方法中调用:
|
|
623
|
+
```ets
|
|
624
|
+
onMethodCall(call: MethodCall, result: MethodResult): void {
|
|
625
|
+
// 不要在这里用 async,委托给独立方法
|
|
626
|
+
this.handleAsync(call, result);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private async handleAsync(call: MethodCall, result: MethodResult): Promise<void> {
|
|
630
|
+
// 这里可以用 async/await
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### 7. `Cannot find name 'xxx'` (未导入的鸿蒙 API)
|
|
635
|
+
|
|
636
|
+
**原因**:使用了鸿蒙 API 但未导入。
|
|
637
|
+
|
|
638
|
+
**修复**:每个 `@ohos.xxx` API 都需要显式 import:
|
|
639
|
+
```ets
|
|
640
|
+
import wifi from '@ohos.wifiManager';
|
|
641
|
+
import audio from '@ohos.multimedia.audio';
|
|
642
|
+
import camera from '@ohos.multimedia.camera';
|
|
643
|
+
```
|