Flutter 鸿蒙实现精准碰一碰分享:tap_share_ohos 三方库打通真机传文件与传卡片

示例仓库:https://atomgit.com/dijun_520/flutter_tap_share_ohos

社区引导:框架源码与版本发布见 https://atomgit.com/CPF-Flutter/flutter_flutter/tree/oh-3.41.9-release

CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter


实测环境版本(写清版本是排查问题的第一步,本文所有结论均基于下表):

项目版本
开发工具DevEco Studio 26.0(内置 SDK:DevEco Studio/sdk)
系统 APIHarmonyOS 7.0.0 / API 26(compatibleSdkVersion: 26.0.0)
Flutter3.41.9
实测设备华为(nova 14)

一、真实痛点:Flutter 开发者为什么拿不到"碰一碰"

先说一个业务场景。做连锁门店系统的团队一定遇到过这类需求:店员用手机给顾客"碰一碰"发优惠券;仓库管理员拿设备碰一下货架标签,直接把盘点文件接力传到工作手机;展会现场碰一碰交换电子名片。原生鸿蒙开发者对这套能力并不陌生——NFC 前台分发感知"碰一碰"、Share Kit 的系统分享面板承接"华为分享接力",两段系统级管线拼起来就是全场景分享。

但对 Flutter 开发者来说,这条路在鸿蒙上是断的,断得很具体:

第一,pub.dev 上没有可用的鸿蒙碰一碰插件。 Flutter 官方不提供鸿蒙平台支持,鸿蒙化依赖社区维护的 flutter_flutter 分支。整个鸿蒙 Flutter 插件生态还处于"从零开始补库"的阶段,NFC、Share Kit、分布式这些系统能力的插件覆盖率几乎为零。想在 Flutter 业务里加一个"碰一碰发券"功能,没有轮子,只能自己造。

第二,造轮子的第一道坎是嵌入层 API 和文档对不上。 flutter_ohos 嵌入层(flutter.har)的公开文档相对滞后,比如 EventChannel 的 StreamHandler / EventSink 在当前版本并没有从主入口导出,照着官方 EventChannel 的写法 import 会直接编译失败;拿 UIAbility 上下文的 FlutterPluginBinding.getAbility() 也不存在,正确入口是 FlutterManager.getInstance().getUIAbility()。这些坑文档不会告诉你,只能解包 flutter.har 看 Index.ets 才能确认。

第三,系统分享的正确入口发生了代际切换。 网上能搜到的鸿蒙分享代码大多是 FA 模型时代的 ohos.want.action.sendData Want 写法,在 Stage 模型的真机上只会弹"暂无可用打开方式",分享面板根本拉不起来。API 12 之后的正规入口是 Share Kit 的 systemShare,两者的差异没有任何迁移提示。

第四,权限体系有一处和直觉相反的枚举定义。 abilityAccessCtrl 的 GrantStatus 是 0 = PERMISSION_GRANTED、-1 = PERMISSION_DENIED,不是 Android 开发者习惯的"1 表示已授予"。照习惯写出来的权限判断在模拟器上"看起来正常",真机一跑权限页全乱。

这四个断点叠加起来的结果就是:Flutter 业务方想上碰一碰,要自己踩完嵌入层、Share Kit、权限、分布式四个坑,成本极高。所以我们的做法不是在业务里写一次性代码,而是把这些系统能力封装成一个标准 Flutter 三方库 tap_share_ohos——业务侧只 import 一个门面类,调用 shareFiles / shareCard 两个方法就能跑通真机碰一碰,所有系统细节收敛在库的 ArkTS 实现里。

选题上刻意收窄:本文只覆盖"碰一碰分享"这一个能力点(传文件 + 传卡片),不写框架横评,不做 API 盘点。跨端调起(分布式 Want)涉及完全另一套 API 与坑位,值得单独成文。

二、先看效果:

按"先给效果、再讲原理"的顺序,先看真机上实际跑出来的样子,后面每一段原理都对应画面里的一个环节。

2.1 入口与能力探测

打开 example,主页能力面板由 getCapabilities() 真实上报设备能力,不是写死的标签:

图 1 · 真机主页:能力面板真实上报 NFC 可用 / 分布式 可用 / 分享面板 可用,下方四个场景入口

这份能力面板的意义在于让业务 UI 可以"按能力路由":真机上显示真实链路入口,模拟器上显示模拟通道入口,桌面上直接隐藏不可用功能。能力探测的实现后面第三节会拆开讲。

2.2 碰一碰传文件:从沙箱文件到系统分享面板

进入"碰一碰传文件"页,点"生成演示文件"在应用沙箱里生成一个 116890 字节的文本文件,再点"碰一碰发送":

图 2 · 传文件页:沙箱生成的 demo-8586.txt(116890 字节),路径 /data/storage/el2/base/haps/entry/cache/

点击发送后,系统分享面板从屏幕底部弹出,文件以卡片形式呈现,面板顶部提示"可碰一碰分享至 HarmonyOS 5 及以上版本手机"——这正是华为分享碰一碰接力的正式入口;面板里已经自动列出了附近的可信设备(鸿蒙 PC)和"华为分享":

图 3 · 系统分享面板:文件卡片 + 可信设备入口 + 华为分享,碰一碰接力从这里开始

这一步的动效(点击发送 → 面板滑入 → 关闭回落)看下面的 GIF,约 8 秒:

图 4 · 动效演示:点击"碰一碰发送"→ 系统分享面板弹出 → 关闭面板,整个链路无跳转无卡顿

2.3 碰一碰传卡片:结构化消息编码为 URI

传卡片页可以编辑名片(姓名/标题 + 描述),点"碰一碰发送"后,卡片被编码成 tapshare://contact?id=...&title=... 形式的 URI 文本,以文本记录进入同一个系统分享面板:

图 5 · 传卡片页:名片编辑(李四 / Flutter / 鸿蒙跨端开发工程师)

图 6 · 卡片分享面板:文本卡片"李四 · 碰一碰卡片",与文件共用同一条华为分享接力通道

为什么卡片要走 URI 编码而不是直接塞 JSON?

2.4 权限页:两级权限、两种授予方式

权限页展示碰一碰分享涉及的两个权限及其真实授权状态,NFC_TAG 是 normal 级(安装即授予),DISTRIBUTED_DATASYNC 是 user_grant 级(运行时弹窗申请):

图 7 · 权限页:两个权限均显示"已授权",底部说明两种权限的授予机制差异

2.5 关键日志

在真机上触发一次完整的"生成文件 → 碰一碰发送",用 hdc shell hilog | grep TapShareOhos 过滤出的插件专属日志如下(domain 0x0042 / tag TapShareOhos,插件内置打点):

11-10 18:06:40.143  I  A00042/.flutter_ohos_ai_agent_example/TapShareOhos:
  shareFiles: 1 个文件已转 file:// URI(transferId=tf-1794305200141), 拉起系统分享面板
11-10 18:06:40.310  I  A00042/.flutter_ohos_ai_agent_example/TapShareOhos:
  shareFiles: 用户完成分享操作(transferId=tf-1794305200141)

两行日志把"沙箱路径转 URI → 面板拉起 → 用户完成"三个节点串了起来。transferId 从 Dart 侧发起调用时就生成并贯穿始终,用于把后续 transferProgress 事件关联回这次调用。如果分享面板被用户取消,日志会打出"分享面板被取消/失败,回落模拟通道",并在 Dart 侧通过事件流收到模拟进度——这是刻意设计的降级路径,保证业务逻辑在任何环境都有确定性行为。

效果层面看完,值得强调一点:以上所有画面都没有写一行原生页面代码。分享面板是系统组件,Flutter 侧只是通过插件触发它;UI 上可见的"待发送文件 / 传输进度 / 收到的文件"三段式反馈,全部由事件流驱动。

三、核心实现:把系统管线封装成 Flutter 三方库

3.1 总体架构:一个门面、两个通道、一条降级链

图 8 · tap_share_ohos 架构:Dart 门面统一 API,鸿蒙侧经 MethodChannel/BasicMessageChannel 对接 NFC、Share Kit 与权限系统,最终落到系统分享面板与华为分享接力

库分三层:

  • Dart 门面层(lib/tap_share_ohos.dart):业务唯一入口,暴露 startTapSession、shareFiles、shareCard、onEvent、ensurePermissions 等方法;
  • 平台通道层:MethodChannel(tap_share_ohos/methods)承载方法调用,BasicMessageChannel(tap_share_ohos/events)承载事件推送,编解码统一用 JSONMessageCodec / StandardMessageCodec;
  • ArkTS 原生层(ohos/src/main/ets/components/plugin/TapShareOhosPlugin.ets):NFC 探测、权限三件套、Share Kit 分享面板、模拟通道四块实现,全部带异常处理和 hilog 打点。

业务侧的使用代码可以短到只有一个方法调用。下面是 example 里传文件页的核心逻辑,完整可运行(演示文件生成 + 调用门面 + 事件驱动 UI):

import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:tap_share_ohos/tap_share_ohos.dart';
import 'package:tap_share_ohos/src/models.dart';

/// 碰一碰传文件页:生成演示文件 → 碰一碰发送 → 事件流驱动进度与接收反馈
class FileSharePage extends StatefulWidget {
  const FileSharePage({super.key});
  
  State<FileSharePage> createState() => _FileSharePageState();
}

class _FileSharePageState extends State<FileSharePage> {
  TapShareFile? _file;            // 待发送文件
  double _progress = 0;           // 传输进度 0~1
  String? _receivedName;          // 收到的文件名
  StreamSubscription<TapEvent>? _sub;

  
  void initState() {
    super.initState();
    // 订阅碰一碰事件流:进度事件与接收事件都从这里来
    _sub = TapShare.onEvent.listen(_onEvent);
  }

  
  void dispose() {
    _sub?.cancel();               // 页面销毁必须取消订阅,防止内存泄漏
    super.dispose();
  }

  /// 事件分发:transferProgress 更新进度条,fileReceived 展示接收结果
  void _onEvent(TapEvent event) {
    if (!mounted) return;
    if (event.type == TapEvent.transferProgress && event.progress != null) {
      setState(() => _progress = event.progress!.progress);
    } else if (event.type == TapEvent.fileReceived && event.file != null) {
      setState(() => _receivedName = event.file!.name);
    }
  }

  /// 在应用沙箱 cache 目录生成一个约 114KB 的演示文本文件
  Future<void> _generateFile() async {
    final rnd = Random();
    final sb = StringBuffer();
    for (int i = 0; i < 12000; i++) {
      sb.write('碰一碰演示数据 ${rnd.nextInt(999999)}\n');
    }
    final dir = await Directory.systemTemp.createTemp('demo');
    final f = File('${dir.path}/demo-${rnd.nextInt(9999)}.txt');
    await f.writeAsString(sb.toString());
    setState(() {
      _file = TapShareFile(path: f.path, name: f.uri.pathSegments.last,
          mimeType: 'text/plain');
    });
  }

  /// 发起碰一碰传文件:真机弹出系统分享面板(华为分享接力),失败回落模拟通道
  Future<void> _send() async {
    if (_file == null) return;
    try {
      final transferId = await TapShare.shareFiles([_file!], title: '碰一碰传文件');
      debugPrint('transferId=$transferId');   // 用于关联回调事件
    } on TapShareException catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('发送失败: ${e.code} ${e.message ?? ''}')));
      }
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('碰一碰传文件')),
      body: ListView(padding: const EdgeInsets.all(16), children: [
        Row(children: [
          TextButton(onPressed: _generateFile, child: const Text('生成演示文件')),
          const SizedBox(width: 12),
          FilledButton(onPressed: _file == null ? null : _send,
              child: const Text('碰一碰发送')),
        ]),
        if (_file != null) Text('待发送: ${_file!.name}'),
        LinearProgressIndicator(value: _progress),
        Text(_receivedName == null
            ? '等待对端碰一碰…' : '收到文件: $_receivedName'),
      ]),
    );
  }
}

这段业务代码里没有任何平台判断——TapShare.shareFiles 在真机上走系统分享面板。

3.2 事件通道:为什么用 BasicMessageChannel 而不是 EventChannel

碰一碰的所有异步反馈(会话开启、卡片到达、传输进度)都要从 ArkTS 侧推回 Dart。按 Flutter 官方习惯,事件流应该用 EventChannel + StreamHandler。但在 flutter_ohos 当前版本(3.7.12-ohos 分支对应的 flutter.har)里,StreamHandler / EventSink 没有从 @ohos/flutter_ohos 主入口导出,import 直接编译失败。我们解包 flutter.har 确认 Index.ets 的导出清单后,改用已导出的 BasicMessageChannel:

ArkTS 侧的发送实现(完整方法,含空通道保护):

/** 信息级日志(%{public} 保证明文输出,hilog 默认对参数脱敏) */
private logInfo(msg: string): void {
  hilog.info(LOG_DOMAIN, LOG_TAG, '%{public}s', msg);
}

/** 异常级日志 */
private logError(msg: string): void {
  hilog.error(LOG_DOMAIN, LOG_TAG, '%{public}s', msg);
}

/**
 * 统一事件出口:所有原生事件都从这里推给 Dart 侧。
 * data 的键值会被平铺进事件 payload,Dart 侧按 TapEvent.fromMap 解析。
 */
private emit(name: string, data: Record<string, Object>): void {
  const channel: BasicMessageChannel<Object> | null = this.eventChannel;
  if (channel == null) {
    this.logError(`emit 失败: 事件通道未初始化(event=${name})`);
    return;
  }
  const event: Record<string, Object> = { 'event': name };
  const keys: Array<string> = Object.keys(data);
  keys.forEach((k: string) => {
    event[k] = data[k];
  });
  channel.send(event);
}

Dart 侧把 BasicMessageChannel 的消息接成广播流(完整实现):


Stream<TapEvent> get onEvent {
  _eventController ??= () {
    final controller = StreamController<TapEvent>.broadcast();
    controller.onListen = () {
      _events.setMessageHandler((msg) async {
        if (msg != null) {
          try {
            controller
                .add(TapEvent.fromMap((msg as Map).cast<dynamic, dynamic>()));
          } catch (_) {}
        }
        return null;
      });
    };
    controller.onCancel = () {
      _events.setMessageHandler(null);
    };
    return controller;
  }();
  return _eventController!.stream;
}

两个实现细节值得展开。其一,用 broadcast StreamController:进度事件可能同时被进度条 UI 和日志模块订阅,单订阅流会让第二个订阅者抛异常。其二,onCancel 里把 handler 置空:所有订阅者取消后不再处理消息,避免页面销毁后事件继续派发到已 dispose 的回调。TapEvent.fromMap 内部做了两种 payload 的兼容——原生侧进度事件是平铺字段(bytesSent / bytesTotal / state 直接在顶层),而部分演示事件把进度嵌在 progress 子对象里,fromMap 里两个分支都处理了,这是踩过"嵌套 Map 解码"的坑之后补的防御。

通道编解码的选型也有讲究。BasicMessageChannel 配 JSONMessageCodec,意味着两侧数据都走 JSON 可序列化结构——这是刻意的:JSON 结构在 ArkTS 侧天然是 Record,避开了 StandardMessageCodec 把嵌套 Dart Map 解码成 ArkTS Map 类型的那套差异(坑 5 的根源),收发两侧的心智模型一致,调试时 emit 的 payload 打出来就是合法 JSON。代价是数据量比二进制编解码略大,但碰一碰事件都是轻量结构(状态、进度、卡片 URI),最大的单条 payload 也就几百字节,性能不构成约束。方法调用通道(MethodChannel + StandardMessageCodec)则保留默认编解码,参数表浅层平铺,两侧约定好键名即可——两条通道一个偏稳、一个偏简,各取所长。

3.3 传文件:沙箱路径 → file:// URI → SharedData → 系统分享面板

这是整条链路的核心。Dart 侧传来的文件路径是应用沙箱路径(形如 /data/storage/el2/base/haps/entry/cache/demo-8586.txt),而 Share Kit 的 SharedRecord 要求 file:// 协议的 URI,必须用 fileUri.getUriFromPath 转换。ArkTS 侧完整实现如下:

private shareFiles(call: MethodCall, result: MethodResult): void {
  const files: Array<ESObject> = call.argument('files') as Array<ESObject> ?? [];
  if (files.length === 0) {
    this.logError('shareFiles: files 列表为空,拒绝发起');
    result.error('invalidArgs', 'files 不能为空', null);
    return;
  }
  const transferId: string = `tf-${Date.now()}`;
  const fileCount: number = files.length;
  const title: string = call.argument('title') as string ?? '碰一碰传文件';
  const names: Array<string> = [];
  files.forEach((f: ESObject) => {
    names.push(this.fieldValue(f, 'name') as string ?? 'file');
  });

  const context: common.UIAbilityContext | null = this.getAbilityContext();
  if (this.nfcSupported && context != null) {
    // 真实设备:走 ShareKit systemShare 拉起系统分享面板(华为分享支持碰一碰接力)
    try {
      const uris: Array<string> = [];
      files.forEach((f: ESObject) => {
        const path: string = this.fieldValue(f, 'path') as string ?? '';
        if (path.length > 0) {
          uris.push(fileUri.getUriFromPath(path));
        }
      });
      if (uris.length > 0) {
        this.logInfo(`shareFiles: ${fileCount} 个文件已转 file:// URI(transferId=${transferId}), 拉起系统分享面板`);
        const first: systemShare.SharedRecord = {
          utd: 'general.file-object',
          uri: uris[0],
          title: title,
        };
        const data: systemShare.SharedData = new systemShare.SharedData(first);
        for (let i = 1; i < uris.length; i++) {
          const rec: systemShare.SharedRecord = {
            utd: 'general.file-object',
            uri: uris[i],
            title: names[i],
          };
          data.addRecord(rec);
        }
        const controller: systemShare.ShareController = new systemShare.ShareController(data);
        controller.show(context, {
          selectionMode: uris.length > 1
            ? systemShare.SelectionMode.BATCH : systemShare.SelectionMode.SINGLE,
        }).then(() => {
          this.logInfo(`shareFiles: 用户完成分享操作(transferId=${transferId})`);
          const out: Record<string, Object> = { 'transferId': transferId, 'simulated': false };
          result.success(out);
        }).catch(() => {
          this.logError(`shareFiles: 分享面板被取消/失败,回落模拟通道(transferId=${transferId})`);
          this.simulateFileTransfer(transferId, fileCount, result);
        });
        return;
      }
    } catch (err) {
      this.logError(`shareFiles: systemShare 异常,落入模拟通道: ${err}`);
      // 落入模拟通道
    }
  }
  this.simulateFileTransfer(transferId, fileCount, result);
}

拆开讲几个关键决策:

整条链路一张图看懂。 下图把从 Flutter 调用到对端接收的完整路径串了起来,包括踩坑后修正过的部分(FA 时代 Want 写法的废弃路径)和事件回流通道:

图 9 · 碰一碰传文件完整链路:Flutter UI → MethodChannel → ArkTS 插件(沙箱转 file:// URI、构造 SharedData)→ 系统分享面板 → 华为分享接力 → 对端;transferProgress 事件经 BasicMessageChannel 回流

图中有一条容易被忽略的虚线:事件回流。分享面板弹出期间,Dart 侧 UI 并没有被挂起,进度条、状态文案都在实时刷新,靠的就是 transferProgress 事件沿 BasicMessageChannel 回流。实线是"调下去",虚线是"推上来",两个方向的通道各自独立,这也是 3.2 节用两条通道而不是一条的架构原因。

UTD 类型决定面板行为。 utd: 'general.file-object' 是统一类型描述符(Uniform Type Descriptor),告诉分享面板这是一条文件记录。面板据此渲染文件卡片、匹配接收方。传卡片用的是 general.plain-text,面板渲染成文本卡片。UTD 写错不会报错,但面板会显示"?"图标且无法接力——图 3 文件卡片左下角的问号小图标就是我们调试 UTD 时留下的真实痕迹。

多文件走 BATCH、单文件走 SINGLE。 SelectionMode 影响面板的预览布局,与文件数量一一对应,写错会导致面板预览异常。

controller.show 的 Promise 语义是"面板关闭"而非"传输完成"。 用户点分享面板右上角关闭,Promise 也会 resolve;真正把文件接力出去是系统行为(华为分享),插件无法感知传输结果。所以 result.success 返回的 transferId 只是任务标识,业务侧不应把它当成"传输完成"的信号——需要传输结果感知时订阅 transferProgress 事件(模拟通道)/ 系统通知(真实接力由华为分享负责,见第四节边界说明)。

异常路径有四层兜底:参数为空 → result.error;路径转换失败或 URI 为空 → 落入模拟通道;systemShare 抛异常 → 落入模拟通道;用户取消面板 → 回落模拟通道。任何一条路径 Dart 侧都能拿到确定结果或确定事件,不会悬挂。

3.4 传卡片:为什么编码成 tapshare:// URI

卡片(名片、优惠券)和文件的本质区别是卡片是结构化消息,需要在接收端还原成对象。我们把卡片编码为:

private shareCard(call: MethodCall, result: MethodResult): void {
  const id: string = call.argument('id') as string ?? `card-${Date.now()}`;
  const cardTitle: string = call.argument('title') as string ?? '碰一碰卡片';
  const cardType: string = call.argument('cardType') as string ?? 'generic';
  const transferId: string = `tc-${Date.now()}`;

  // NFC 真实链路:卡片编码为 tapshare:// URI(NDEF 自定义记录承载)
  const uri: string = `tapshare://${cardType}?id=${encodeURIComponent(id)}&title=${encodeURIComponent(cardTitle)}`;
  this.logInfo(`shareCard: 卡片编码完成 ${uri}(transferId=${transferId})`);
  const context: common.UIAbilityContext | null = this.getAbilityContext();
  if (this.nfcSupported && context != null) {
    // 真实设备:卡片以 tapshare:// URI 文本形式进入系统分享面板
    try {
      const record: systemShare.SharedRecord = {
        utd: 'general.plain-text',
        content: uri,
        title: cardTitle,
        description: '碰一碰卡片',
      };
      const data: systemShare.SharedData = new systemShare.SharedData(record);
      const controller: systemShare.ShareController = new systemShare.ShareController(data);
      controller.show(context, {
        selectionMode: systemShare.SelectionMode.SINGLE,
        previewMode: systemShare.SharePreviewMode.DEFAULT,
      }).then(() => {
        const out: Record<string, Object> = { 'transferId': transferId, 'simulated': false };
        result.success(out);
      }).catch(() => {
        const out: Record<string, Object> = { 'transferId': transferId, 'simulated': true };
        result.success(out);
      });
      return;
    } catch (err) {
      this.logError(`shareCard: systemShare 异常,落入模拟通道: ${err}`);
      // 落入模拟通道
    }
  }
  const out: Record<string, Object> = { 'transferId': transferId, 'simulated': true };
  result.success(out);
  const data: Record<string, Object> = {
    'transferId': transferId,
    'bytesSent': 1,
    'bytesTotal': 1,
    'state': 'done',
  };
  this.emit(TapShareEvent.transferProgress, data);
}

三个设计点:

URI 格式自定义 scheme + query 编码。 tapshare://<cardType>?id=xx&title=xx。自定义 scheme 的好处是接收端一眼可辨"这是碰一碰卡片协议"(对端应用解析文本时先匹配 scheme),query 里只放标识与标题这类轻量字段并用 encodeURIComponent 转义,避免特殊字符破坏 URI 结构。大块业务数据(payload)不应该进 URI——URI 会随分享面板预览明文展示,敏感信息放进去既不安全也会撑爆面板预览;正确姿势是 URI 只当"取件凭证",payload 走自家业务后端。

卡片和文件共用同一条系统管线。 卡片就是一条 general.plain-text 记录,接收端(华为分享接力到 PC、或另一台手机)拿到的是文本,对端插件解析文本时匹配 tapshare:// scheme 还原卡片对象并发 cardReceived 事件。这样一条 NFC + 分享管线同时服务两种内容,库的实现面收敛了一半。

previewMode 显式给默认值。 SharePreviewMode 不传时部分版本面板表现不一致,显式声明 DEFAULT 后真机表现稳定。

3.5 权限三件套:查询、申请、以及那个要命的枚举

权限层提供三个方法:checkPermissions(查状态)、requestPermissions(弹窗申请)、ensurePermissions(一步到位)。核心是状态查询的枚举映射(完整方法):

private mapPermissionStatus(state: number): string {
  // abilityAccessCtrl.GrantStatus: 0 = PERMISSION_GRANTED, -1 = PERMISSION_DENIED
  if (state === 0) {
    return 'granted';
  } else if (state === -1) {
    return 'denied';
  }
  return 'notDetermined';
}

private checkPermissions(): Record<string, string> {
  const out: Record<string, string> = {};
  const tokenId: number = this.getTokenId();
  try {
    const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
    PERMISSION_MAP.forEach((perm: string, key: string) => {
      try {
        const state: number = atManager.verifyAccessTokenSync(tokenId, perm as ESObject as Permissions);
        out[key] = this.mapPermissionStatus(state);
      } catch (err) {
        out[key] = 'unavailable';
      }
    });
  } catch (err) {
    PERMISSION_MAP.forEach((perm: string, key: string) => {
      out[key] = 'unavailable';
    });
  }
  return out;
}

注意两个细节。** tokenId 的获取走同步接口** getBundleInfoForSelfSync(GET_BUNDLE_INFO_WITH_APPLICATION),取 appInfo.accessTokenId;逐条权限单独 try-catch——某个权限在低版本系统上不存在时(比如设备无 NFC 芯片,NFC_TAG 接口异常),只降级该条为 unavailable,不让整页权限查询崩掉。requestPermissions 内部调用 requestPermissionsFromUser 弹系统授权窗,无论用户点允许还是拒绝,都回读 checkPermissions 返回真实状态,Dart 侧不需要自己区分"弹窗被拒"和"查询失败"。

权限声明是库的 module.json5 完成的事,example 里声明如下(业务接入方同样需要在自己的 module.json5 里声明这两条):

"requestPermissions": [
  { "name": "ohos.permission.NFC_TAG" },
  {
    "name": "ohos.permission.DISTRIBUTED_DATASYNC",
    "reason": "$string:datasync_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
  }
]

NFC_TAG 是 normal 级,声明即授予;DISTRIBUTED_DATASYNC 是 user_grant 级,必须带 reason 字符串资源,且 usedScene.when 要写 inuse,缺了这两项打包直接报错。

3.6 模拟通道:没有 NFC 的环境怎么联调

模拟器没有 NFC 硬件,nfcController.isNfcAvailable() 返回 false。库的策略是能力探测 + 自动降级:startTapSession 里检测 NFC 可用性,不可用则把 simulated: true 返回给 Dart,同时事件流里发一条"已进入模拟碰一碰模式"的 sessionStarted;业务调用 simulateTapReceive 可以主动注入一次模拟碰一碰(模拟对端发来卡片/文件),接收链路的解析逻辑与真实链路完全共用。


四、踩坑与边界条件

坑 1:GrantStatus 枚举映射反了,权限页"看起来全被拒了"

现象:真机权限页 NFC_TAG 显示"已拒绝",但它是 normal 级权限,安装即授予。

定位:hdc shell "atm dump -t -b com.example.flutter_ohos_ai_agent_example" 查真实授权状态,输出里 grantStatus: 0。

根因:SDK 的 GrantStatus 是 PERMISSION_DENIED = -1、PERMISSION_GRANTED = 0,不是 Android 习惯的"1 = granted"。照习惯写 state == 1 ? granted : denied,在模拟器上所有权限都返回 denied,“看起来只是权限没申请”,实际上是映射反了。修复见 3.5 节 mapPermissionStatus。经验:任何跨平台权限代码迁移到鸿蒙,先用 atm dump 对一次真实值。

坑 2:FA 时代的 sendData Want 拉不起分享面板

现象:点"碰一碰发送"后真机弹系统提示"暂无可用打开方式",分享面板不出现。

根因:早期资料里鸿蒙分享的写法是构造 ohos.want.action.sendData 的 Want 去隐式拉起接收方。这套写法是 FA 模型时代的,Stage 模型下参数结构变了(要传 ability.picker.fileNames 等键),且不带真实文件 URI,系统匹配不到任何接收方。

修复:换 Share Kit 的 systemShare(3.3 节完整代码),面板正常弹出并出现华为分享碰一碰入口。经验:Stage 模型下做"分享",systemShare 是唯一正规入口,不要再从 FA 时代博客里抄 Want 拼接。

坑 3:沙箱路径不转 URI,面板卡片是"?"

现象:直接把沙箱路径塞进 SharedRecord.uri,面板显示一个带问号的灰色卡片,无法接力。

根因:SharedRecord.uri 要求 file:// 协议 URI,沙箱绝对路径不是合法 URI。

修复:fileUri.getUriFromPath(path)(@kit.CoreFileKit)逐文件转换后再进 SharedData。

坑 4:EventChannel 的 StreamHandler 没导出,编译失败

现象:import { EventSink, StreamHandler } from '@ohos/flutter_ohos' 报找不到导出。

根因:flutter_ohos 当前版本的 flutter.har 主入口未导出事件流相关类型(解包看 Index.ets 确认)。

修复:改用 BasicMessageChannel + JSONMessageCodec(3.2 节),Dart 侧 receiveBroadcastStream 等价能力由 setMessageHandler 实现。顺带拿到 FlutterManager.getInstance().getUIAbility() 这个正确取上下文的姿势——文档里的 getAbility() 在 FlutterPluginBinding 上并不存在。

坑 5:嵌套 Map 在 ArkTS 侧用下标访问拿到 undefined

现象:模拟碰一碰的 payload 里嵌套的 card 对象在 ArkTS 侧读出来是 undefined,卡片接收页显示空白。

根因:Dart 的嵌套 Map 经 StandardMessageCodec 解码到 ArkTS 是 Map 类型而不是 Record/普通对象,obj['card'] 下标访问对 Map 无效,必须 map.get('card')。平铺字段不受影响。

修复:插件里统一走 fieldValue 兼容方法(下面是完整实现),对 Map 用 .get()、对普通对象用下标:

/** 兼容读取 StandardMessageCodec 解码出的 Map / 普通对象字段 */
private fieldValue(obj: ESObject, key: string): ESObject {
  if (obj == null) {
    return null;
  }
  if (obj instanceof Map) {
    return (obj as Map<string, Object>).get(key);
  }
  return obj[key];
}

总结:

本文基于 Flutter OHOS 3.41.9、鸿蒙 API 26,落地 tap_share_ohos 三方插件,打通了鸿蒙真机碰一碰分享链路,支持碰一碰传文件与结构化卡片。

Flutter 鸿蒙生态缺少现成碰一碰插件,同时存在多处适配卡点:嵌入层文档滞后、EventChannel 相关类型未导出、Stage 模型下旧的 FA 分享写法失效、权限枚举定义和跨平台习惯相反,这些问题叠加导致业务直接接入成本很高。这套方案的核心思路是封装一层 ArkTS 桥接层,对外收敛成简洁统一的 Dart API,业务只需要调用 shareFiles、shareCard,不用关心底层 NFC、Share Kit、权限、沙箱路径转换等系统细节。

插件采用 MethodChannel 承载方法调用,受导出限制改用 BasicMessageChannel 做事件推送,使用 JSONMessageCodec 简化两侧数据编解码;沙箱文件路径通过 fileUri.getUriFromPath 转为 file:// URI,构造 SharedData 调用 Share Kit 的 systemShare 拉起系统分享面板,复用华为分享的碰一碰接力通道;结构化卡片则编码为自定义 tapshare:// URI,和文件复用同一套分享管线,接收端解析 Scheme 还原卡片信息。

权限模块单独封装查询、申请逻辑,重点处理了 GrantStatus 0代表授权、-1代表拒绝这个特殊枚举;同时内置完整的能力探测和模拟通道,模拟器无NFC硬件时自动降级,保证开发联调的可用性。整套链路附带完善的异常兜底、传输事件回流、日志打点,通过 transferId 关联单次分享任务。


参考资料:

  • Share Kit(systemShare 官方文档):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/share-systemshare
  • 访问控制(权限开发指南):https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/access-token-overview

相关推荐

RN 鸿蒙安全三方库落地:react-native-digital-shield,密钥不出 TEE,AES-GCM/RSA-JWT 跨端身份打通

本文基于 RNOH 0.84 与 HarmonyOS API 26,自研数字盾库 react-native-digital-shield,把 ArkTS 专属的 HUKS、asset 能力经TurboModule 桥接给 JS:提供 AES-256-GCM 加解密与密钥托管、敏感资产存储、RSA-2048 签发校验 RS256 JWT,并以 JWK 公钥和 HS256 共享密钥实现跨端身份互认,密钥明文始终不出 TEE。文中还复盘 TurboModule 双侧注册、GCM 需拆分 16 字节

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 16

Flutter 修复鸿蒙 WebView Cookie 同名键丢失:离线回读与开发助手复核实录

修复了 Flutter 三方库 webview_cookie_manager 鸿蒙端因同名键误判导致的 Cookie 名称丢失问题,确保业务键如 path、domain 等在回读时正确保留。修复后,模拟器与真机测试均实现四组数据完整恢复,值中多等号也保持不变。补丁已提交至 CPF-Flutter 官方仓库,提升插件在鸿蒙平台的可靠性。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 26

Flutter 鸿蒙启动图撤销后编译失败:用 DevEco Code 复核三方库修复

本文针对 Flutter 鸿蒙启动图撤销时导致编译失败的问题,修复了 flutter_native_splash_ohos 库在移除启动图时误删仍被引用资源的缺陷。通过保留1×1透明图片并透传自定义模块路径,确保资源引用完整性,使工程可正常构建。修复已提交至官方仓库,支持幂等操作与多场景验证,提升开发体验。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 39

Flutter 接入鸿蒙 ArkWeb 预初始化:帮助页首开时机与主线程成本实测

为解决Flutter鸿蒙版webview_flutter无法在无视图状态下提前初始化引擎的问题,我在其原生实现中新增可选静态入口initializeWebEngine()。实测表明,在2秒预热窗口下,页面首屏渲染(FCP)耗时从中位数1791ms降至1019ms,显著提升首次打开体验;而在零延迟场景下,总路径耗时略有上升,需权衡预热时机与用户等待成本。该补丁已提交至CPF-Flutter官方仓库,支持社区协作优化。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 36

Flutter 优化鸿蒙 printing_ohos 多页 PDF 光栅化:原生资源释放与缓冲复用

针对鸿蒙端 printing_ohos 插件存在的页码映射错误与高内存占用问题,本文提出并实现三项优化:修正页码索引映射、引入原生资源分层释放机制、复用像素缓冲区。修复后,第三页显示正确,内存增长显著降低——在模拟器上PSS增量中位数下降65.8%,真机测试下降64.4%。完整补丁已提交至官方仓库,支持社区协作与问题反馈。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 30

React Native 鸿蒙权限审计实战:让每一项权限都有人认领

本文介绍一款纯 JS 编写的 React Native 鸿蒙权限审计工具,解决权限声明与使用对账难、最小权限集计算矛盾、审核必填字段缺失三大痛点。通过 module.json5 声明、使用台账与源码扫描三路输入对账,自动识别未使用权限、补全 reason/usedScene、生成可粘贴的最小清单,并支持 CI 卡控与风险分评估。工具内核独立,离线运行,兼顾准确性与安全性,助力应用高效通过审核。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 12

Cordova 鸿蒙 WebView 安全实战:能改的运行时拦,改不了的构建期审

本文通过 cordova-plugin-harmony-webguard 插件,解决 Cordova/HarmonyOS 应用中导航安全、引擎不可修改配置、JS 门面静默降级三大难题。基于运行时策略闸门(onPageStart + JS 预检)与构建期审计 CLI(扫描 MainPage.ets、config.xml 等),实现协议拦截、白名单校验、高危配置阻断,支持 CI 失败退出,将安全基线可视化、可测化,真正落地“能改的运行时拦,改不了的构建期审”。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 25

Flutter 鸿蒙供应链审计实战:让高危依赖卡在 CI 门口

本文介绍了一个基于纯 Dart CLI 与 Flutter Dashboard 的依赖审计工具,解决鸿蒙工程中三套锁文件(pubspec.lock、oh-package-lock.json5、package-lock.json)统一读取、离线快照与 OSV 在线情报选择、以及高危依赖通过退出码卡在 CI 的问题。工具以锁文件为事实依据,支持离线快照(4 条真实 GHSA)与在线查询,输出 SBOM(CycloneDX 1.5)、Markdown/JSON 报告,并通过退出码实现 CI 阻断,性能秒级

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 127

React Native 鸿蒙端侧 OCR 实战:TurboModule 与纯 TS 引擎的逐字契约

本文以鸿蒙真机上的 React Native 0.84 工程为例,详解三大难题:通过 TurboModule 接入 HarmonyOS Core Vision Kit 实现端侧离线 OCR;规避 Metro 预置头导致的 require 不存在问题;实现 npm 包与 HAR 双形态交付。项目采用 codegen 生成 ArkTS 接口,结合纯 TS 回退引擎,确保跨引擎逐字符一致,并通过 autolinking 支持无缝集成。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 88

Flutter 鸿蒙端侧 OCR 实战:让文字识别留在设备上

本文通过一个运行在鸿蒙真机上的 Flutter 插件工程,系统解决了三大核心问题:如何统一内存、文件、相册三类输入的识别链路;如何实现与 KMP 版本的 JSON 契约逐字节对齐;以及在系统不提供置信度时如何诚实处理。基于 HarmonyOS Core Vision Kit,插件实现端侧离线 OCR,全程无网络、无权限、无密钥,支持真实小票与截图识别,性能稳定(312~507ms),并通过 35 项单元测试与真机集成验证。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 147

跨端 RN 鸿蒙碰一碰分享库实战:基于 Share Kit 封装 react-native-ohos-knock-share,实现卡片、文件一碰传

本文介绍基于React Native的鸿蒙碰一碰分享库,解决RN开发者在鸿蒙生态中无法便捷使用Share Kit碰一碰功能的痛点。通过封装NFC/星闪触碰、3秒发送窗口、沙箱文件URI转换等核心逻辑,实现跨设备快速传递链接、文件、图片等内容。支持手机、平板、PC多端适配,提供回环演示与真机实测验证,显著降低接入门槛,提升跨端协作效率。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 179

React Native 鸿蒙实战:给应用接入小艺语音指令,跑通 Skill 声明到操作闭环

本文介绍如何在 React Native 鸿蒙(RNOH)工程中实现小艺技能闭环,通过开源库 react-native-ohos-ai-agent 封装技能声明、语音指令接收、端侧执行与结果回传全流程。针对三方应用权限限制,精准定位可操作边界,实现“被唤起→执行→反馈”链路贯通。结合五张真机截图验证:技能注册、指令匹配、意图上报、智能体调起及对话层弹出等关键节点均成功,完整打通语音交互闭环,助力应用无需入口即可被用户“叫”到。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 169

Flutter 鸿蒙实战:用三方库接入小艺语音指令与智能体,跑通 Skill 声明到操作闭环

本文基于 flutter_ohos_ai_agent 三方库,实现 Flutter 应用一键调起鸿蒙小艺智能体。针对跨端框架对接鸿蒙 AI 能力的碎片化、环境差异等痛点,通过插件封装系统 Kit 能力,提供统一 Dart API。实测显示,模拟器与真机均支持文本/语音指令闭环,真机可完整调起小艺面板并回传事件。核心原理为:利用 ArkUI 承载页桥接 FunctionComponent,通过 MethodChannel 与 EventChannel 实现 Flutter 与原生能力双向通信,完成从语音识别到

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 150

RNOH 原生内存泄漏定位与治理:借助 LeakyGuard 实现图片解码降采样、LRU 字节缓存、资源生命周期管控,长列表图片页面峰值内存优化

本文面向 RNOH 0.84 长列表图片场景,剖析滑动数分钟后进程被系统杀掉的根因:原图解码尺寸失控、缓存按条数限流、PixelMap未显式 release。基于自研LeakyGuar,TurboModule 库,给出 desiredSize 真降采样、字节预算 LRU 缓存(逐出必 release)、资源登记与快照 diff、卸载即释放 Hook 的闭环方案,实测单图内存降 98.6%,24 图峰值由 1098.6MB 降至15.8MB。并总结双侧注册、ArkTS 语法差异、vp 转像

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 245

Flutter 鸿蒙空间音频实战:让音乐播放器的声音“长出方位感“

本文通过一个真实运行的 Flutter 工程,系统解析了在鸿蒙系统上实现空间音频的三大核心问题:基于双耳时间差(ITD)、声级差(ILD)和频谱整形的方位感算法;耳机切换与音频焦点管理;以及播放器与音效的集成。项目使用自研插件 spatial_audio,结合原生 HarmonyOS 接口,实现了跨设备的空间音频体验,支持普通耳机,无需硬件依赖,并提供实时参数反馈与系统状态监听。附带完整代码仓库与实测效果展示,为 Flutter on HarmonyOS 的音频开发提供了可复用的技术方案。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 285

Flutter 鸿蒙 HiTrace 请求归因:图片压缩等待时,主线程在做什么

为定位鸿蒙平台图片压缩性能瓶颈,我在 flutter_native_image 插件中新增可选的 HiTrace 阶段追踪功能,支持按请求开启解码、缩放、编码与资源释放阶段的耗时分析。通过对比模拟器与真机(华为畅享 100 Pro Max)数据,精准识别出耗时主要集中在解码与编码阶段,显著提升诊断效率。相关代码已提交至官方仓库,助力开发者快速定位性能问题。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 适配鸿蒙折叠屏铰链角度按需订阅:支架调节页用完即停与重入实测

本文介绍对 Flutter 三方库 dual_screen 的优化:针对支架调节页等短时观察场景,保留原有缓存入口的同时,新增按需订阅机制。通过按需启停监听,实现页面退出后停止原生回调,避免无效数据更新。实测显示,无消费者时原生回调从3条降至0条,重进页面则等待新事件,更符合临时工具页的使用逻辑。完整代码已开源,支持鸿蒙折叠屏角度动态感知,适用于需精准控制生命周期的折叠屏应用开发。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 适配鸿蒙手表屏幕形状:让计时卡片按圆屏和方屏布局

本文基于 wear_plus 1.2.4 版本,为鸿蒙系统补全屏幕形状查询功能,使 WatchShape 能正确返回 round 等形状信息。通过在原 Dart 接口不变的前提下,新增鸿蒙平台处理器,实现与 Android 一致的异步查询逻辑。对照实验验证:适配后页面可从“查询未接入”变为“圆屏已确认”,日志成功接收系统回执,布局行为保持一致。完整代码开源至 oh-flutter/wear_plus,支持 Flutter 鸿蒙穿戴设备开发。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 优化鸿蒙剪贴板大图复制卡顿:从主线程同步阻塞到异步文件描述符直解

针对鸿蒙系统下 flutter_image_clipboard 插件复制高清图片时阻塞主线程的问题,本文提出三项优化:异步文件描述符直解、异步剪贴板写入及原生资源逆序释放。实测显示,模拟器事件循环最大间隔从300.32ms降至54.36ms(降幅81.9%),真机上更从133.20ms降至11.20ms(降幅91.59%),显著提升页面响应性。完整代码已开源,支持直接使用与社区交流。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Flutter 三方库 petalmaps_ohos 的 OpenHarmony 适配实战(0 → 1 全记录)

本文详细记录了从零构建 Flutter 三方库 petalmaps_ohos 并适配 OpenHarmony/HarmonyOS 平台的全过程,涵盖项目初始化、原生实现(基于 ArkTS 和 MethodCallHandler)、插件注册、功能验证及升级至 HarmonyOS SDK 26 的关键步骤。重点解析了为何需实现 AbilityAware 获取 UIAbilityContext 以确保地图调起成功,并通过分层架构、枚举封装与兼容性设计,实现功能完整、易用且低维护成本的“调起系”地图插件,

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 59

Flutter 三方库 holding 的 OpenHarmony 适配实战:从 0 到 1 开发一个鸿蒙感知插件

本文详述了从零实现开源 Flutter 插件 holding 并适配 OpenHarmony/HarmonyOS 的全过程。基于鸿蒙专属 MultimodalAwarenessKit,实现握持手与操作手状态感知,覆盖事件流订阅、状态查询及 API 26 新增接口,确保功能一致、性能高效、接口稳定。通过 Dart 层封装、ArkTS 原生桥接、真机验证与单元测试,完成插件工程化落地,并发布至 pub.dev,为同类系统能力插件的鸿蒙化提供可复用的技术路径。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 28

Flutter 三方库 gesturesshare 从 0 到 1:鸿蒙隔空传送分享插件的开发、升级与发布实战

本文详述了 Flutter for OpenHarmony 三方库 gesturesshare 从零开发到发布全过程:基于 HarmonyOS 隔空传送能力,实现跨设备图片与 App Linking 一键分享。涵盖双端代码设计、API 26 升级、真机验证及 Pub 发布流程,提供 Dart/ArkTS 对照与踩坑复盘。项目填补 Flutter 生态空白,推动鸿蒙多设备交互能力普及,助力开发者快速接入系统级功能。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 28

Qt 鸿蒙沉浸光感实战:让玻璃的每个像素自己发光

本文通过一个 QML 模块与 Qt for OpenHarmony 构建实践,完整解答了三个核心问题:如何用 81 行 GLSL 实现无图片依赖的动态玻璃材质(含光核漂移、视差、SDF 圆角与颗粒);如何在 Qt Quick 中通过 C++ 控制器实现弹层锚定与翻转;以及如何将 Qt 应用交叉编译并以 HAR 形式部署至鸿蒙系统。项目基于 Qt 5.12.12,利用 ShaderEffect 实现纯渲染级视觉效果,兼顾性能与跨平台兼容性,提供可复用的开源方案。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 23

开源鸿蒙平台 KMP_CMP 三方库「端侧语音转文字」适配全流程

本项目实现KMP/CMP在OpenHarmony鸿蒙平台的语音转文字(TTS)完整链路适配,通过Kotlin/Native共享模型、C ABI/N-API桥接与ArkTS交互,解决权限、音频格式、生命周期及跨平台契约等难题。支持中文、英文、日文多语言识别,提供统一结果模型与JSON序列化,实测于华为Mate 80 Pro Max模拟器,验证了JVM测试、Native动态库、HAP签名部署及真机识别全流程。项目结构清晰,分六阶段推进,确保代码复用与平台无关性,为KMP在鸿蒙生态落地提供可复用的技术范式。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 30

开源鸿蒙平台 KMP_CMP 三方库「端侧OCR」新特性适配

本项目实现Kotlin Multiplatform(KMP)在OpenHarmony平台的端侧OCR适配,基于HarmonyOS 7.0 SDK与Compose Multiplatform 1.9.2,成功打通JVM、Kotlin/Native与ArkTS间的数据交互。通过C ABI/N-API桥接层,实现KMP模型与Core Vision Kit识别结果的跨语言共享,支持中、英、日、韩四语识别。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 13

开源鸿蒙平台 KMP/CMP 三方库「智感握姿」新特性

本项目实现鸿蒙KMP/CMP平台下智感握姿能力的跨端适配,基于Kotlin Multiplatform构建共享状态模型,通过C ABI与ArkTS桥接,支持左手、右手、双手等五种握持状态的稳定传递。项目打通ohosArm64目标、Native动态库构建与DevEco Studio集成链路,提供统一API与真机验证示例,确保状态模型、事件序列与生命周期在多端一致。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 19

Electron 鸿蒙沉浸光感实战:把玻璃交给操作系统去发光

本文通过一个 Electron 库与 HarmonyOS 三方库,系统解决三大核心问题:跨平台材质路径对齐(macOS vibrancy、Windows acrylic、鸿蒙 backgroundBlurStyle)、菜单子窗口精准定位(坐标转换与工作区适配)、以及无脚本菜单文档的安全事件回传(自定义 URL scheme 与焦点管理)。基于 CPF-Electron 鸿蒙化分支,实现桌面级玻璃效果——材质由操作系统合成器采样,网页仅渲染薄色层与光斑,确保视觉一致性与性能优化。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 21

Flutter 修复 flutter_secure_storage 鸿蒙入口路由与前缀隔离:让多账号登录凭证在手机上独立读写与清理

本文针对 flutter_secure_storage 在鸿蒙平台的适配问题,修复了主包缺失 TargetPlatform.ohos 路由导致的 unsupported_platform 错误,并统一鸿蒙子包多账号前缀的读写与清理范围,解决键值覆盖、前缀错用及批量操作匹配过宽等问题。修复后,业务层可继续使用统一接口实现跨账号安全存储,保障数据隔离性。完整代码已开源至 abaoge/flutter_secure_storage。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 1万+

Cordova 鸿蒙沉浸光感实战:让弹层玻璃“喝到“背景的光

本文通过一个 HarmonyOS 模拟器上的 Cordova 项目,详解了在 WebView 中实现“玻璃拟态”效果的三大核心问题:利用 backdrop-filter 实现背景实时模糊与提亮,构建四层结构(采样、渐变底、光场、噪点)模拟真实玻璃质感;通过锚点定位与动态布局实现弹层贴边生长;并集成焦点管理、系统返回键及 inert 属性,确保交互礼仪完整。插件为纯 JS 无原生依赖,基于 ArkWeb 内核,兼顾性能与真实背景采样,适用于菜单、浮层等场景。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 28

CMP 鸿蒙透明玻璃实战:让悬浮导航栏真正透出光场

本文通过一个KMP/CMP工程,详解如何在鸿蒙上用Kotlin实现沉浸光感玻璃材质的逐像素渲染。核心解决三大问题:透明感源于环境光场与带alpha的柔光混合、玻璃“透光”依赖alpha保留的屏幕混合算法、跨平台一致性通过软件光栅器与原生桥接实现。项目以纯代码生成全屏光场与悬浮玻璃组件,无图片资源,支持动态切换调色板,实测模拟器下亚秒级重绘,完整复现官方“沉浸光感”视觉效果。仓库地址:https://atomgit.com/lqjmac/spatial-glass-cmp。

白天写代码,晚上追AI大模型,偶尔搞点八卦 开源动态|技术干货|实习机会|生态活动 我负责分享,你负责变强 28
上一篇: Flutter 鸿蒙实战:用三方库接入小艺语音指令与智能体,跑通 Skill 声明到操作闭环
下一篇: React Native 鸿蒙实战:给应用接入小艺语音指令,跑通 Skill 声明到操作闭环
AtomGit 代码君
AtomGit 代码君 企业官方账号 企业官方账号
博客等级 码龄6年 2万+粉丝 856原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值