Flutter 三方库 flutter_email_sender 的 OpenHarmony 鸿蒙化适配指南(mailto RFC 6068 + 静态 Context 转发实战)
Flutter 三方库 flutter_email_sender 的 OpenHarmony 鸿蒙化适配指南(mailto RFC 6068 + 静态 Context 转发实战)
Flutter 社区地址: https://atomgit.com/CPF-Flutter/flutter_flutter
github三方库地址:https://github.com/sidlatau/flutter_email_sender
pub地址:https://pub.dev/packages/flutter_email_sender
鸿蒙适配版:https://atomgit.com/oh-flutter/email_sender
库版本:flutter_email_sender v10.0.x(上游 master,pub workspace 联邦三包重构版)|验证环境:Flutter 鸿蒙 SDK 3.44.9(oh-3.44.9-dev)|DevEco Studio 26.0.0.821 | DevEco 模拟器 + 真机 HarmonyOS 7.0.0.105(API 26)
在 Flutter 应用里,发送邮件是高频基础能力——账号注册验证、订单反馈、客服会话一键发送、诊断日志回传都依赖它。flutter_email_sender 是 pub.dev 上专注于"调用系统邮件 composer"的 Flutter 插件(区别于 emailjs 这类走 SMTP 协议直发的方案),但它在 OpenHarmony 上没有任何官方实现。本文记录我把它完整迁移到鸿蒙的全过程。

这次适配的难点集中在一个很"底层"的差异上:Android 的 Intent 有一套 putExtra 体系,主题、正文、收件人可以挂在 extras 上由系统分发给邮件应用;而鸿蒙的 Want 跨应用传参最可靠的载体就是 uri 一个字符串。所以鸿蒙侧不能照搬 Android 的思路,必须把整封邮件"编码"进一个符合 RFC 6068 的 mailto: URI,再用 startAbility 的隐式 Want 拉起系统邮件应用。同时还有两个工程问题:插件在引擎侧拿不到 UIAbilityContext(startAbility 是它的专属能力),以及 Dart facade 侧的平台守卫会把 ohos 直接判成"无邮件能力"。下面逐层展开。


一、环境搭建
本章不重复展开,直接引用官方文档:Flutter OH 开发环境搭建指导。
本文实际使用版本:Flutter OH oh-3.44.9-dev、DevEco Studio 26.0.0.821、HarmonyOS SDK API 26,flutter doctor -v 相关两项 [√] 即可开工。
二、应用背景
2.1 场景与痛点
- 账号验证邮件:注册流程末尾"给客服发验证邮件",正文带用户名和时间戳
- 订单反馈:订单详情页"反馈问题"按钮,主题自动带上订单号
- 客服入口:设置页"联系我们",收件人固定、主题预填
- 邀请分享:邀请好友注册,正文带邀请码
- 一键诊断反馈:把设备信息、日志摘录拼进正文,用户点一下就进系统邮件 composer
痛点:这些场景的共同特征是应用自己不碰 SMTP——不收集用户的邮箱账号密码、不申请网络权限直发,只负责"把字段预填好,剩下的交给系统邮件应用"。自研方案要在鸿蒙上自己拼 Want、处理邮件应用不存在的兜底、处理 URI 编码,适配工作量集中在"把 Dart 侧的 Email 模型无损地搬进 Want"这一步。
2.2 为什么需要这个库
发邮件在 Flutter 生态里有两条路线:
| 路线 | 代表库 | 体验 | 适合 |
|---|---|---|---|
| SMTP 直发 | emailjs 等 | 应用内静默发送,但要用户配置邮箱账号/授权码,体验重 | 后台自动化发信 |
| 系统邮件 composer | flutter_email_sender | 拉起系统邮件应用预填字段,用户确认后发送,体验轻 | 用户可见的反馈/客服场景 |
flutter_email_sender 屏蔽了 Android ACTION_SENDTO / iOS MFMailComposeViewController / macOS 的平台差异,业务侧只构造一个 Email 对象(subject / body / recipients / cc / bcc / isHTML / attachmentPaths)然后 await FlutterEmailSender.send(email)。它还提供 getCapabilities() 让 UI 按平台能力动态降级(比如 macOS 不支持 cc/bcc)。库在上游已完成联邦化(federated plugin)重构,这对鸿蒙适配是好消息——只需在 flutter_email_sender_method_channel 包里新增 ohos 平台实现,app 面的 API 一个字都不用改。
2.3 解决什么问题
一句话总结:让 Flutter 应用在鸿蒙上以与 Android/iOS 完全一致的 Email 模型调用 FlutterEmailSender.send(),内部把整封邮件编码为 RFC 6068 mailto URI,通过 startAbility({action: viewData, uri}) 拉起系统邮件 composer,收件人/主题/正文/cc/bcc 全部预填。
能力映射表(对照 Android 原版实现逐项翻译):
| flutter_email_sender 能力 | Android 原版 | 鸿蒙适配 |
|---|---|---|
canSendMail 探测 | resolveActivity(ACTION_SENDTO, mailto:) | ArkTS getCapabilities 返回 canSend:true,探测兜底下沉到 send 失败路径 |
send(无附件) | ACTION_SENDTO + mailto: + EXTRA_SUBJECT/EXTRA_TEXT/EXTRA_EMAIL/... | startAbility + ohos.want.action.viewData + mailto URI(字段编进 query 参数) |
send(带附件) | ACTION_SEND + FileProvider URI + EXTRA_STREAM | 当前版本未透传(mailto 无附件语义,见 FAQ Q7) |
| HTML 正文 | HtmlCompat.fromHtml + EXTRA_HTML_TEXT | isHTML 时 \n → <br> 后编入 body= 参数 |
| Activity/Ability 注入 | ActivityAware.onAttachedToActivity | EntryAbility.onCreate 静态 setContext(this.context) 转发 |
| 发送结果回调 | startActivityForResult + onActivityResult | startAbility Promise then/catch |
三、接口分析(适配前必做)
3.1 联邦三包结构
上游 master 已是 pub workspace monorepo,根 pubspec.yaml 用 workspace: packages/* 聚合:
flutter_email_sender/ # 仓库根(workspace)
├── packages/
│ ├── flutter_email_sender/ # app 面主包
│ │ └── example/ # demo(含 ohos/ 调试工程)
│ ├── flutter_email_sender_platform_interface/ # 纯 Dart 接口包
│ │ └── lib/src/{email.dart, email_capabilities.dart, ...}
│ └── flutter_email_sender_method_channel/ # 平台实现包
│ ├── android/ ios/ macos/ ohos/ # ← ohos 是本次新增
│ └── lib/src/method_channel_flutter_email_sender.dart
└── pubspec.yaml # resolution: workspace
- platform_interface:定义
Email(不可变请求模型)、EmailCapabilities(能力集)、FlutterEmailSenderPlatform(抽象类 + instance 单例)。纯 Dart 不碰平台,无需改动。 - method_channel:各平台的 MethodChannel 实现,Android/iOS/macOS 原生实现都在这个包里,ohos 实现也放这里。
- 主包:只做
default_package声明,把平台路由到 method_channel 包。
3.2 MethodChannel 协议
method_channel_flutter_email_sender.dart 在名为 flutter_email_sender 的 MethodChannel 上暴露两个方法:
| 方法 | 入参 | 返回 | 说明 |
|---|---|---|---|
send | Email.toJson()(Map) | void | 拉起系统邮件 composer |
getCapabilities | — | Map<String, bool>(canSend 等) | 原生侧能力探测 |
Email.toJson() 的 key(注意是 snake_case):subject / body / recipients / cc / bcc / attachment_paths / is_html。
3.3 Email 与 EmailCapabilities 模型
适配前必须精读这两个模型,因为校验逻辑在 Dart 侧,鸿蒙实现的行为要跟它对齐:
// platform_interface/lib/src/email_capabilities.dart(节选)
void validateEmail(Email email, {required String platformName}) {
if (!canSend) {
throw PlatformException(code: 'not_available',
message: 'Email composer is unavailable on $platformName.');
}
final unsupported = unsupportedFeaturesFor(email);
if (unsupported.isEmpty) return;
throw PlatformException(code: 'unsupported',
message: 'The current platform does not support: ${unsupported.join(', ')}.');
}
send() 的完整链路是:先 getCapabilities() → validateEmail()(canSend 为 false 抛 not_available;带了平台不支持的 cc/HTML/附件等抛 unsupported)→ 通过后才 invokeMethod('send', email.toJson())。如果鸿蒙侧的 getCapabilities 没接上,业务代码第一次调用就会在 Dart 层被拦死,根本走不到 ArkTS——这就是为什么 Dart 平台补丁(4.6 节)是必做的。
3.4 Android 原版实现剖析(鸿蒙方案的分岔点)
android/.../FlutterEmailSenderPlugin.kt 的核心逻辑:
private fun canSendMail(): Boolean {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
return activity!!.packageManager.resolveActivity(intent, 0) != null
}
// send():
// 无附件 → action = ACTION_SENDTO; data = Uri.parse("mailto:")
// subject/body/recipients/cc/bcc 全走 putExtra(EXTRA_*)
// 有附件 → ACTION_SEND + FileProvider URI + EXTRA_STREAM + ClipData
// + selector = Intent(ACTION_SENDTO, mailto:) 用来筛选邮件类应用
读到这里分岔点就清楚了:Android 是"mailto: 只做应用筛选器,真正的字段挂在 extras 上"。而鸿蒙 Want 的 parameters 只在同签名应用间可靠传递,跨应用(我们 → 华为邮件)最通用的契约就是 action + uri + type 三元组的隐式 Want。结论:鸿蒙侧必须走"纯 mailto"路线,把 subject/body/cc/bcc 全部编进 URI 的 query 参数——这正是 RFC 6068 定义的语法,也是 iOS 生态通行的做法,华为邮件应用对 mailto: 的支持非常完整。
3.5 鸿蒙侧 API 盘点
UIAbilityContext.startAbility(want):隐式拉起,Want = {action: 'ohos.want.action.viewData', uri: 'mailto:...'},返回 Promise,能 catch 到"无应用响应"错误openLink(API 12+):语义更贴切但同样是 UIAbilityContext 专属,且默认带应用选择弹窗等行为差异,本文用更通用的startAbility- 插件侧
FlutterPluginBinding.getApplicationContext():没有startAbility——这是本库适配的核心矛盾,见 4.5 节 flutter_ohos的AbilityAware:理论上可在onAttachedToAbility拿 ability,但 FlutterAbility 派生链路下时序不稳,社区主流做法是宿主显式转发(async_wallpaper、flutter_sharing_intent 同款决策)
四、适配实现
4.1 依赖写法(federated 双包声明)
主包 packages/flutter_email_sender/pubspec.yaml 把 ohos 路由到 method_channel 包:
flutter:
plugin:
platforms:
android:
default_package: flutter_email_sender_method_channel
ios:
default_package: flutter_email_sender_method_channel
macos:
default_package: flutter_email_sender_method_channel
ohos: # ← 新增
default_package: flutter_email_sender_method_channel
web:
default_package: flutter_email_sender_web
mc 包 packages/flutter_email_sender_method_channel/pubspec.yaml 声明 ohos 原生插件类和 Dart 插件类:
flutter:
plugin:
implements: flutter_email_sender
platforms:
android:
package: com.sidlatau.flutteremailsender
pluginClass: FlutterEmailSenderPlugin
dartPluginClass: MethodChannelFlutterEmailSender
ios:
pluginClass: FlutterEmailSenderPlugin
dartPluginClass: MethodChannelFlutterEmailSender
ohos: # ← 新增
pluginClass: FlutterEmailSenderMethodChannelPlugin
dartPluginClass: MethodChannelFlutterEmailSender
macos:
pluginClass: FlutterEmailSenderPlugin
dartPluginClass: MethodChannelFlutterEmailSender
pluginClass 指向 ArkTS 插件类(ohos/index.ets default export 的那个),dartPluginClass 指向联邦 Dart 实现——这样 FlutterEmailSenderPlatform.instance 在 ohos 平台会自动实例化到我们的 MethodChannel 版本。
4.2 ohos/ 骨架
flutter_email_sender_method_channel/ohos/
├── BuildProfile.ets
├── index.ets # export default FlutterEmailSenderMethodChannelPlugin
├── oh-package.json5 # 无额外依赖(flutter_ohos HAR 由构建动态注入)
├── build-profile.json5
└── src/main/
├── module.json5 # type: "har",name 必须与包目录名一致
└── ets/components/plugin/
└── FlutterEmailSenderMethodChannelPlugin.ets # 全部实现,约 130 行
三个骨架细节:
- HAR 的
module.json5里name必须与包目录名完全一致(flutter_email_sender_method_channel),这是 flutter_ohos 构建时按名找 HAR 的硬规则,也是宿主import ... from 'flutter_email_sender_method_channel'的解析依据。 - HAR 的
module.json5不允许requestPermissions——本库也不需要任何权限(拉起邮件应用是隐式 Want,无需声明)。 oh-package.json5的 dependencies 留空即可,@ohos/flutter_ohos由 flutter_tools 构建期动态注入。
4.3 ArkTS 插件核心(完整实现)
ohos/src/main/ets/components/plugin/FlutterEmailSenderMethodChannelPlugin.ets:
import { FlutterPlugin, FlutterPluginBinding } from '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/FlutterPlugin';
import MethodChannel, { MethodCallHandler, MethodResult } from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodChannel';
import MethodCall from '@ohos/flutter_ohos/src/main/ets/plugin/common/MethodCall';
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';
export default class FlutterEmailSenderMethodChannelPlugin implements FlutterPlugin, MethodCallHandler {
private methodChannel?: MethodChannel;
private uiContext?: common.UIAbilityContext;
private static instance?: FlutterEmailSenderMethodChannelPlugin;
private static pendingContext?: common.UIAbilityContext;
getUniqueClassName(): string {
return 'FlutterEmailSenderMethodChannelPlugin'; // 与 pubspec pluginClass 一致
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.methodChannel = new MethodChannel(binding.getBinaryMessenger(), 'flutter_email_sender');
this.methodChannel.setMethodCallHandler(this);
FlutterEmailSenderMethodChannelPlugin.instance = this;
if (FlutterEmailSenderMethodChannelPlugin.pendingContext !== undefined) {
this.uiContext = FlutterEmailSenderMethodChannelPlugin.pendingContext; // 宿主先到:补领 context
}
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
this.methodChannel?.setMethodCallHandler(null);
FlutterEmailSenderMethodChannelPlugin.instance = undefined;
}
/// Called from the host UIAbility so the plugin can start abilities.
static setContext(ctx: common.UIAbilityContext): void {
FlutterEmailSenderMethodChannelPlugin.pendingContext = ctx;
const that = FlutterEmailSenderMethodChannelPlugin.instance;
if (that !== undefined) {
that.uiContext = ctx; // 插件先到:直接注入
}
}
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case 'send':
this.send(call, result);
break;
case 'getCapabilities':
result.success({ 'canSend': true }); // 探测下沉到 send 的失败路径
break;
default:
result.notImplemented();
break;
}
}
private send(call: MethodCall, result: MethodResult): void {
const args = call.args as Record<string, Object>;
const subject = this.str(args, 'subject');
const body = this.str(args, 'body');
const isHtml = args['is_html'] === true;
const recipients = this.strList(args, 'recipients');
const cc = this.strList(args, 'cc');
const bcc = this.strList(args, 'bcc');
if (recipients.length === 0) {
result.error('invalid', 'recipients must not be empty', null);
return;
}
const uri = this.buildMailto(recipients, cc, bcc, subject, body, isHtml);
const ctx = this.uiContext;
if (ctx === undefined) {
result.error('not_available', 'no UIAbility context forwarded', null);
return;
}
const want: Want = { action: 'ohos.want.action.viewData', uri: uri };
ctx.startAbility(want).then(() => {
result.success(null);
}).catch((e: Object) => {
result.error('not_available', 'no mail app found: ' + e, null); // 无邮件应用时的兜底
});
}
// buildMailto / enc / str / strList 见 4.4 节
}
参数解析的 ArkTS 严格模式要点:call.args 断言为 Record<string, Object>(绝不能写 any);str() 对非 string 值返回空串;strList() 先 instanceof Array 判定再 filter 收敛为 string 数组——这套类型防御保证 3.2 节协议里任何字段缺省或类型漂移都不会崩。
4.4 mailto 构造:把整封邮件压进一个 URI
这是鸿蒙侧最核心的 20 行,值得逐条讲清楚:
private buildMailto(recipients: string[], cc: string[], bcc: string[],
subject: string, body: string, isHtml: boolean): string {
const to = recipients.map((r: string): string => r.trim()).join(',');
const params: string[] = [];
if (cc.length > 0) { params.push('cc=' + this.enc(cc.join(','))); }
if (bcc.length > 0) { params.push('bcc=' + this.enc(bcc.join(','))); }
if (subject.length > 0) { params.push('subject=' + this.enc(subject)); }
const bodyText = isHtml ? body.replace(/\n/g, '<br>') : body;
if (bodyText.length > 0) { params.push('body=' + this.enc(bodyText)); }
const query = params.join('&');
return 'mailto:' + to + (query.length > 0 ? '?' + query : '');
}
private enc(v: string): string { return encodeURIComponent(v); }
对照 RFC 6068 的语法 mailto:addr[,addr]*[?key=value[&key=value]],几个刻意的设计:
- 收件人进 path、其余进 query:
mailto:后直接跟逗号分隔的收件人列表(trim 过滤空串),cc/bcc/subject/body 作为 hname=hvalue 对挂在?后——这是 RFC 6068 明确定义的四个标准 header 扩展,华为邮件、Gmail、Apple Mail 全部认。 - 全部
encodeURIComponent:中文主题(如 demo 里的"来自鸿蒙 Flutter 的问候")、&?=#这些会破坏 URI 结构的字符、换行,全部百分号编码后再拼。实测中文主题在华为邮件 composer 里完整还原,无乱码。 - 空字段不进 query:
cc/bcc/subject/body为空时干脆不 push,避免产出cc=&bcc=&这种把空值显式传给邮件客户端的 URI——有的客户端会把空cc=解析成一个空收件人行。 isHTML的换行转换:mailto 的body参数是单行文本,\n经过编码后多数客户端能还原,但 HTML 邮件里换行语义应该由<br>承担——所以isHTML为 true 时先做\n → <br>替换。HTML 标签是否被渲染成富文本由邮件客户端决定(与 Android 侧EXTRA_HTML_TEXT的行为一致,都是"尽力而为")。- 空收件人前置校验:
recipients为空在构造 URI 前就返回result.error('invalid', ...)——mailto:?subject=...(无收件人)虽然 RFC 允许,但业务上几乎必然是 bug,尽早失败比静默拉起一个空 composer 好。
最终产出的 URI 长这样(demo 默认表单值):
mailto:hello@example.com?subject=%E6%9D%A5%E8%87%AA%E9%B8%BF%E8%92%99...&body=%E8%BF%99%E5%B0%81%E9%82%AE%E4%BB%B6...
4.5 静态 Context 转发:UIAbilityContext 的时序问题
startAbility 是 UIAbilityContext 的方法,但插件的 onAttachedToEngine(binding) 只给 applicationContext——它没有拉起 Ability 的能力。所以必须让宿主 EntryAbility 把自己的 context"递"进来。
问题在于时序不确定:EntryAbility.onCreate 和 Flutter 引擎初始化后插件 onAttachedToEngine 的先后没有保证(冷启动路径下两个初始化链路并行)。解法是插件侧两个静态字段做双向握手:
// example/ohos/entry/src/main/ets/entryability/EntryAbility.ets
import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos';
import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant';
import Want from '@ohos.app.ability.Want';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import FlutterEmailSenderMethodChannelPlugin from 'flutter_email_sender_method_channel';
export default class EntryAbility extends FlutterAbility {
configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
GeneratedPluginRegistrant.registerWith(flutterEngine)
}
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onCreate(want, launchParam);
// Forward the UIAbility context so the plugin can start the mail app.
FlutterEmailSenderMethodChannelPlugin.setContext(this.context);
}
}
握手逻辑(两种时序都覆盖):
| 时序 | 发生了什么 | 结果 |
|---|---|---|
| 宿主先到 | setContext 时 instance 还是 undefined → context 存入 pendingContext;稍后 onAttachedToEngine 发现 pendingContext 有值 → 补领到 uiContext | 正常 |
| 插件先到 | onAttachedToEngine 时 pendingContext 为空 → 只记 instance;稍后 setContext 发现 instance 有值 → 直接注入 uiContext | 正常 |
无论哪条路,当 Dart 侧真正调 send 时(用户点按钮,必然晚于两个初始化),this.uiContext 一定是就绪的。send 里仍保留 ctx === undefined 的防御分支返回 not_available——万一是极早期的 Dart 调用,给调用方一个明确的错误码而不是崩溃。
4.6 Dart 平台补丁:两个 switch 都要加 ohos
这是本库区别于"只加 ohos 目录"型适配的关键一步。上游 method_channel_flutter_email_sender.dart 的 getCapabilities 是按 defaultTargetPlatform 分发的,ohos 不在名单里会直接返回 EmailCapabilities.none()(canSend:false)——结合 3.3 节的 validateEmail,业务侧调用会在 Dart 层被 not_available 拦死。补丁要打两处:
Future<EmailCapabilities> getCapabilities() async {
return switch (defaultTargetPlatform) {
TargetPlatform.android || TargetPlatform.iOS || TargetPlatform.macOS ||
TargetPlatform.ohos => // ← 补丁 1:ohos 走原生查询
_nativeCapabilities(),
_ => const EmailCapabilities.none(),
};
}
Future<EmailCapabilities> _nativeCapabilities() async {
final capabilities = await _channel.invokeMapMethod<String, bool>(
_getCapabilitiesMethod,
);
final defaults = switch (defaultTargetPlatform) {
TargetPlatform.android || TargetPlatform.iOS ||
TargetPlatform.ohos => _mobileCapabilities, // ← 补丁 2:ohos 享有移动端能力集
TargetPlatform.macOS => _macosCapabilities,
_ => const EmailCapabilities.none(),
};
return EmailCapabilities(
canSend: capabilities?['canSend'] ?? defaults.canSend, // 通道值优先
supportsCc: defaults.supportsCc,
supportsBcc: defaults.supportsBcc,
supportsSubject: defaults.supportsSubject,
supportsPlainTextBody: defaults.supportsPlainTextBody,
supportsHtmlBody: defaults.supportsHtmlBody,
supportsAttachments: defaults.supportsAttachments,
);
}
两个 switch 的语义不同,都不能省:
- 外层 switch 决定"要不要去问原生"——不加 ohos,通道调用根本不会发出。
- 内层 defaults switch 决定"原生只答了 canSend 时,其余能力字段兜底成什么"——ohos 对齐
_mobileCapabilities(cc/bcc/subject/HTML/attachments 全 true),与 Android/iOS 同一档。
合并逻辑也值得注意:只有 canSend 是"通道返回值优先、defaults 兜底",其余字段全取 defaults——因为 ArkTS 侧 getCapabilities 只返回 {canSend: true} 一个键(与 Android 原版对齐),能力明细由 Dart 侧按平台档位静态声明。
4.7 关键决策点
| 决策 | 理由 |
|---|---|
| 纯 mailto URI 而非 Want.parameters 传字段 | parameters 跨应用不可靠;mailto 是 RFC 6068 标准 + 华为邮件完整支持,且与 iOS 路线同构 |
startAbility 而非 openLink | openLink 同样是 UIAbilityContext 专属,且行为差异(应用选择器策略)在 PC/手机形态上不一致,viewData Want 是最通用的隐式启动契约 |
静态 setContext 双向握手(pendingContext + instance) | onCreate 与 onAttachedToEngine 时序不确定,两个静态字段覆盖两种先后 |
getCapabilities 固定返回 canSend:true | ArkTS 侧没有等价 resolveActivity 的轻量探测 API;"设备上有没有邮件应用"的兜底下沉到 startAbility 的 catch,错误码同为 not_available,对 Dart 调用方语义一致 |
不实现 AbilityAware | FlutterAbility 派生链下时序不稳;宿主显式转发是社区已验证模式(async_wallpaper、flutter_sharing_intent 同款) |
| 空字段不进 query | 避免空 cc= 被部分客户端解析成空收件人行 |
MethodCall 独立文件 import | flutter_ohos HAR 里 MethodCall 是独立 default export,不是 MethodChannel 的 named export(同 flutter_sharing_intent 踩过的坑) |
五、demo 验证与运行效果
5.1 demo 设计
example/lib/main.dart 设计成一个"全字段可编辑 + 事件流可观测"的验证台:顶部深绿色能力卡实时显示 getCapabilities 返回的 7 个能力位;中间是 to(逗号分隔)/cc/subject/body 四个输入框 + isHTML 开关;底部 send() 按钮和暗色事件流日志卡(每次通道调用打一条时间戳日志,保留最近 20 条)。默认表单值就是一组含中文主题、中文正文的完整邮件,方便直接点 send 验证编码链路。
5.2 模拟器(DevEco Emulator,无邮件应用)

getCapabilities 真实返回 canSend:true cc:true bcc:true subject:true html:true attachments:true(Dart 平台补丁生效)
首屏能力卡显示 canSend:true cc:true bcc:true subject:true html:true attachments:true——注意这是真实的通道返回链路:Dart 补丁让 ohos 走 _nativeCapabilities() → MethodChannel 到 ArkTS → result.success({'canSend': true}) → Dart 合并 _mobileCapabilities 明细。4.6 节两个 switch 缺任何一个,这里都会是全 false。

点 send 后绿色系统进度条出现(鸿蒙系统在解析 mailto 的隐式启动能力)
点 send() 后系统绿色进度条出现——这是鸿蒙在为 ohos.want.action.viewData + mailto: 的隐式 Want 做能力匹配。模拟器没有预装邮件应用,系统进入"查找可响应应用"的等待状态,随后 startAbility Promise reject,Dart 侧 catch 到 PlatformException(not_available, no mail app found: ...) 打进事件流。这条失败路径本身就是有效验证:mailto URI 被系统正确解析、编码无误、错误码语义与 Android 原版的 No email clients found! 对齐。
5.3 真机(HarmonyOS 7.0.0 MateBook Pro)
真机系统邮件应用(华为邮件)响应 mailto: 拉起 compose,预填主题正文收件人
真机上点 send(),华为邮件应用(com.huawei.hmos.email)被 viewData Want 拉起并进入 compose 界面,收件人、中文主题、中文正文全部预填就位——整条链路(Dart Email 模型 → toJson → MethodChannel → ArkTS buildMailto → startAbility → 系统邮件)端到端打通。
5.4 不进应用的独立验证(aa start)
适配期还可以绕过 Flutter 直接验证"这台设备认不认 mailto",在 hdc shell 里:
aa start -A ohos.want.action.viewData -U 'mailto:hello@example.com?subject=test'
真机上这条命令会直接拉起华为邮件的 compose——与插件内部行为完全同构,可以用来区分"设备问题"还是"插件问题"(aa start 用法:-A action -U uri -t mime-type,隐式启动不指定 -a ability 名)。
六、FAQ:适配过程遇到的问题与解决
Q1:编译报 module name 必须一致?
mc 包 ohos/src/main/module.json5 的 name 字段必须与包目录名完全一致(flutter_email_sender_method_channel),不要去除 _method_channel 后缀——flutter_tools 构建时按"包目录名"找 HAR,宿主 EntryAbility 的 import ... from 'flutter_email_sender_method_channel' 也按这个名字解析。
Q2:send 点击后没反应?
分两种情况:
- 模拟器:没有默认邮件应用时
startAbility进入"选择应用"等待(绿色进度条),随后 Promise reject 报not_available——这是正确行为,不是 bug。 - 真机:确认 EntryAbility.onCreate 里调了
FlutterEmailSenderMethodChannelPlugin.setContext(this.context);漏了这行则 ArkTS 侧uiContext为 undefined,send 直接返回no UIAbility context forwarded。
Q3:canSend:false / 能力卡全 false?
Dart 平台补丁未生效。确认 method_channel_flutter_email_sender.dart 的两个 switch 都加了 TargetPlatform.ohos:外层(决定走 _nativeCapabilities())和 _nativeCapabilities 内层(决定 defaults 用 _mobileCapabilities)。只加外层的话通道能通,但 cc/bcc 等明细字段全是 false,带 cc 的邮件会被 validateEmail 以 unsupported 拦截。
Q4:openLink 不存在?
openLink 在 UIAbilityContext 上,插件的 FlutterPluginBinding.getApplicationContext() 没有。用 UIAbilityContext.startAbility + 静态方法 setContext 转发(4.5 节),不要试图从 applicationContext 调启动类 API。
Q5:EntryAbility 一定要改吗?时序会踩坑吗?
一定要改(startAbility 的 context 来源只有这条路)。时序问题已被双向握手覆盖:setContext 和 onAttachedToEngine 谁先到都能对上,无需在 demo 里加任何 delay 或重试。
Q6:中文主题/正文乱码?
检查 buildMailto 里所有进 query 的字段是否都过了 this.enc()(encodeURIComponent)。收件人 path 段(邮箱地址本身是 ASCII)可以不编码,但 cc/bcc 里如果混入中文显示名也必须编码。
Q7:附件(attachmentPaths)怎么办?
当前版本未透传。原因:RFC 6068 的 mailto 语法没有附件语义——Android 原版是靠 ACTION_SEND + EXTRA_STREAM + FileProvider 绕开 mailto 限制的,而鸿蒙 Want 没有等价的跨应用附件通道(parameters 跨应用不可靠、文件 URI 权限授予机制不同)。带附件调用时字段会被忽略,主题/正文/收件人正常预填。后续版本计划走 sendData Want + 沙箱文件 URI 授权的路线补齐,这也是本库与 Android 版当前的能力差异点,选型时注意。
Q8:如何测试?
开发期(无需邮件应用配合):
- 模拟器验证 capabilities 链路 + send 的 not_available 失败路径(5.2 节)
- hdc shell 里
aa start -A ohos.want.action.viewData -U 'mailto:...'单独验证设备侧(5.4 节)
验收期:真机(预装华为邮件)上跑 demo 点 send,检查 compose 界面的收件人/主题/正文/cc 预填与表单一致,isHTML 开关下换行是否变 <br> 语义。
七、其他内容
7.1 总结
flutter_email_sender v10.0.x 鸿蒙适配完成:federated 双包声明 + Dart 双 switch 平台补丁 + ArkTS mailto 构造 + viewData Want 隐式启动 + 静态 Context 双向握手。选型上放弃照搬 Android 的 extras 体系,改为 RFC 6068 纯 URI 路线,与 iOS 同构、被华为邮件完整支持;getCapabilities 的探测下沉到 send 失败路径,错误码与 Android 原版语义对齐;中文主题/正文经 encodeURIComponent 无损预填,模拟器(capabilities 真实返回 + not_available 失败路径)与真机(华为邮件 compose 预填)双重验证通过。已知能力差异:附件未透传(mailto 无附件语义),规划用 sendData Want 路线补齐。
7.2 鸿蒙适配三件套清单(活动硬性要求)
-
ohos/骨架(oh-package.json5/build-profile.json5/module.json5/index.ets/src/main/ets/components/plugin/*.ets) -
example/ohos(独立可运行调试工程,EntryAbility 覆写 onCreate 转发 UIAbilityContext,build-profile 带 signingConfig: default) - README.OpenHarmony 说明(适配范围 + 依赖写法 + EntryAbility 接入步骤)
7.3 参考链接
欢迎加入 CPF-Flutter 鸿蒙社区,社区入口、环境搭建指南和三方库链接统一放在这里:
- CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
- Flutter OHOS 开发环境搭建指南:https://atomgit.com/CPF-Flutter/flutter_samples/blob/master/docs/ohos/getting-started/flutter-oh-env-setup.md
- flutter_email_sender 鸿蒙适配版(本次产出):(本地工程,不上传远程;由用户自行决定是否推 oh-flutter 组织)
- flutter_email_sender 原项目:https://github.com/sidlatau/flutter_email_sender
- flutter_email_sender pub.dev 包:https://pub.dev/packages/flutter_email_sender
- RFC 6068(The ‘mailto’ URI Scheme):https://datatracker.ietf.org/doc/html/rfc6068
- 鸿蒙 Want 文档:Want 与 Ability 跳转
更多推荐



所有评论(0)