# Flutter三方库适配OpenHarmony【flutter_libphonenumber】——联合插件(Federated Plugin)架构解析
本文介绍了Flutter三方库flutter_libphonenumber适配OpenHarmony的技术方案,重点解析了联合插件(Federated Plugin)架构。该架构将插件分为主包、平台接口包和各平台实现包,解决了传统插件耦合度高、难以扩展的问题。文章详细阐述了架构中三种角色的职责划分,并通过flutter_libphonenumber的包结构展示了多包协作机制。特别说明了鸿蒙平台如何
前言
欢迎来到 Flutter三方库适配OpenHarmony 系列文章!本系列围绕 flutter_libphonenumber 这个 电话号码处理库 的鸿蒙平台适配,进行全面深入的技术分享。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

上一篇我们从全局视角介绍了 flutter_libphonenumber 的功能定位和鸿蒙适配成果。本篇将深入解析该库采用的 联合插件(Federated Plugin)架构,这是理解整个适配工作的基础。我们将逐层拆解 platform_interface、MethodChannel、各平台实现包的协作关系,并详细分析鸿蒙平台是如何 无缝接入 这套架构的。
理解联合插件架构是进行任何 Flutter 三方库鸿蒙适配的 第一步,掌握了这套模式,你就能举一反三地适配其他库。
一、什么是联合插件(Federated Plugin)
1.1 传统插件的局限性
在 Flutter 早期,一个插件包通常将 所有平台的实现 放在同一个仓库中:
my_plugin/
├── lib/my_plugin.dart # Dart API
├── android/ # Android 实现
├── ios/ # iOS 实现
└── pubspec.yaml
这种方式存在明显的问题:
- 耦合度高 — 添加新平台需要修改原始仓库
- 权限受限 — 第三方开发者无法独立贡献新平台支持
- 维护困难 — 所有平台代码混在一起,CI/CD 复杂度高
- 版本冲突 — 某个平台的 breaking change 会影响整个包的版本号
痛点:如果你想为一个已有的 Flutter 插件添加鸿蒙平台支持,但原作者不接受 PR 或者仓库已经不活跃,传统架构下你几乎无能为力。
1.2 联合插件的解决方案
Flutter 官方在 2020 年提出了联合插件架构,将一个插件拆分为 多个独立的包:
flutter_libphonenumber/ # 主包(App-facing package)
├── flutter_libphonenumber_platform_interface/ # 平台接口包(Platform interface)
├── flutter_libphonenumber_android/ # Android 平台包
├── flutter_libphonenumber_ios/ # iOS 平台包
├── flutter_libphonenumber_web/ # Web 平台包
└── flutter_libphonenumber_ohos/ # 🆕 鸿蒙平台包
1.3 三种角色的职责划分
联合插件架构定义了三种角色,每种角色有明确的职责:
| 角色 | 包名示例 | 职责 | 依赖关系 |
|---|---|---|---|
| 主包(App-facing) | flutter_libphonenumber |
对外暴露 API,开发者直接使用 | 依赖 platform_interface |
| 接口包(Platform interface) | flutter_libphonenumber_platform_interface |
定义抽象接口和数据模型 | 依赖 plugin_platform_interface |
| 平台包(Platform implementation) | flutter_libphonenumber_ohos |
各平台的具体实现 | 依赖 platform_interface |
核心优势:任何人都可以独立发布一个新的平台实现包,无需修改主包或接口包的任何代码。这正是鸿蒙适配能够顺利进行的架构基础。
1.4 联合插件 vs 传统插件对比
| 对比项 | 传统插件 | 联合插件 |
|---|---|---|
| 代码组织 | 单一仓库 | 多包分离 |
| 添加新平台 | 必须修改原仓库 | 独立创建新包 |
| 第三方贡献 | 需要原作者合并 PR | 可独立发布 |
| 版本管理 | 所有平台共享版本号 | 各包独立版本 |
| CI/CD | 所有平台一起构建 | 各包独立构建 |
| 接口约束 | 无统一约束 | PlatformInterface 强制约束 |
| 适合场景 | 简单插件 | 多平台复杂插件 |
二、flutter_libphonenumber 的包结构全景
2.1 Melos 工作区管理
flutter_libphonenumber 使用 Melos 管理多包工作区。根目录的 melos.yaml 定义了所有子包的位置:
name: flutter_libphonenumber_workspace
packages:
- packages/**
2.2 六个包的完整依赖关系
整个项目由 6 个包 组成,它们的依赖关系如下:
┌─────────────────────────────────────────────────────────────┐
│ 开发者应用代码 │
│ import flutter_libphonenumber │
└──────────────────────────┬──────────────────────────────────┘
│ 依赖
▼
┌─────────────────────────────────────────────────────────────┐
│ flutter_libphonenumber(主包) │
│ 对外暴露 API + re-export 数据类型 │
└──────────────────────────┬──────────────────────────────────┘
│ 依赖
▼
┌─────────────────────────────────────────────────────────────┐
│ flutter_libphonenumber_platform_interface(接口包) │
│ 抽象类 + 数据模型 + 同步格式化逻辑 + Token 验证 │
└───┬──────────┬──────────┬──────────┬────────────────────────┘
│ │ │ │ 各平台包都依赖接口包
▼ ▼ ▼ ▼
android ios web ohos 🆕
2.3 各包的 pubspec.yaml 版本信息
| 包名 | 版本 | SDK 要求 | 关键依赖 |
|---|---|---|---|
flutter_libphonenumber |
主包 | Dart >= 2.19.0 | platform_interface |
flutter_libphonenumber_platform_interface |
2.1.0 | Dart >= 2.19.0 | plugin_platform_interface: ^2.1.4 |
flutter_libphonenumber_android |
- | - | platform_interface |
flutter_libphonenumber_ios |
- | - | platform_interface |
flutter_libphonenumber_web |
- | - | platform_interface |
flutter_libphonenumber_ohos |
1.0.0 | Dart >= 2.19.0 | platform_interface: ^2.1.0 |
三、平台接口层(Platform Interface)深度解析
3.1 FlutterLibphonenumberPlatform 抽象类
平台接口层的核心是 FlutterLibphonenumberPlatform 抽象类,它继承自 Flutter 官方的 PlatformInterface:
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
abstract class FlutterLibphonenumberPlatform extends PlatformInterface {
/// 构造函数,传入 token 用于安全验证
FlutterLibphonenumberPlatform() : super(token: _token);
/// 私有 token 对象,用于 verifyToken 安全机制
static final Object _token = Object();
/// 默认实例,初始为 MethodChannel 实现
static FlutterLibphonenumberPlatform _instance =
MethodChannelFlutterLibphonenumber();
/// 获取当前平台实例
static FlutterLibphonenumberPlatform get instance => _instance;
/// 设置平台实例(各平台包在 registerWith 中调用)
static set instance(FlutterLibphonenumberPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
}
关键设计:
_instance的默认值是MethodChannelFlutterLibphonenumber(),这意味着如果没有任何平台包注册自己,系统会回退到 MethodChannel 默认实现。
3.2 抽象方法定义
该抽象类定义了 5 个核心抽象方法 和 2 个具体方法:
// ===== 抽象方法(各平台必须实现)=====
/// 获取所有支持的国家/地区数据
Future<Map<String, CountryWithPhoneCode>> getAllSupportedRegions() async {
throw UnimplementedError('getAllSupportedRegions() has not been implemented.');
}
/// 异步格式化电话号码
Future<Map<String, String>> format(String phone, String region) async {
throw UnimplementedError('format() has not been implemented.');
}
/// 解析电话号码,返回元数据
Future<Map<String, dynamic>> parse(String phone, {String? region}) async {
throw UnimplementedError('parse() has not been implemented.');
}
/// 初始化,加载国家数据
Future<void> init({
Map<String, CountryWithPhoneCode> overrides = const {},
}) async {
throw UnimplementedError('init() has not been implemented.');
}
3.3 具体方法(无需平台实现)
抽象类中还包含 2 个具体方法,它们的逻辑完全在 Dart 侧完成,各平台包 无需重写:
/// 同步格式化 — 纯 Dart 侧 mask 匹配,无需原生调用
String formatNumberSync(
String number, {
CountryWithPhoneCode? country,
PhoneNumberType phoneNumberType = PhoneNumberType.mobile,
PhoneNumberFormat phoneNumberFormat = PhoneNumberFormat.international,
bool removeCountryCodeFromResult = false,
bool inputContainsCountryCode = true,
}) {
final guessedCountry =
country ?? CountryWithPhoneCode.getCountryDataByPhone(number);
if (guessedCountry == null) return number;
var formatResult = PhoneMask(
mask: guessedCountry.getPhoneMask(
format: phoneNumberFormat,
type: phoneNumberType,
removeCountryCodeFromMask: !inputContainsCountryCode,
),
country: guessedCountry,
).apply(number);
if (removeCountryCodeFromResult && inputContainsCountryCode) {
formatResult = formatResult.substring(guessedCountry.phoneCode.length + 2);
}
return formatResult;
}
/// 异步格式化+验证 — 组合 parse() 结果
Future<FormatPhoneResult?> getFormattedParseResult(
String phoneNumber,
CountryWithPhoneCode country, {
PhoneNumberType phoneNumberType = PhoneNumberType.mobile,
PhoneNumberFormat phoneNumberFormat = PhoneNumberFormat.international,
}) async {
try {
final res = await parse(phoneNumber, region: country.countryCode);
late final String formattedNumber;
if (phoneNumberFormat == PhoneNumberFormat.international) {
formattedNumber = res['international'] ?? '';
} else if (phoneNumberFormat == PhoneNumberFormat.national) {
formattedNumber = res['national'] ?? '';
} else {
formattedNumber = '';
}
return FormatPhoneResult(
e164: res['e164'] ?? '',
formattedNumber: formattedNumber,
);
} catch (e) {
// 解析失败返回 null
}
return null;
}
3.4 方法分类总结
| 方法 | 类型 | 调用方式 | 是否需要平台实现 |
|---|---|---|---|
getAllSupportedRegions() |
抽象 | 异步 | ✅ 是 |
format() |
抽象 | 异步 | ✅ 是 |
parse() |
抽象 | 异步 | ✅ 是 |
init() |
抽象 | 异步 | ✅ 是 |
formatNumberSync() |
具体 | 同步 | ❌ 否 |
getFormattedParseResult() |
具体 | 异步 | ❌ 否 |
设计哲学:将 平台相关 的操作定义为抽象方法,将 纯 Dart 逻辑 定义为具体方法。这样各平台只需实现 4 个方法,而同步格式化和组合查询的逻辑由接口层统一提供,避免了重复实现。
四、PlatformInterface.verifyToken 安全机制
4.1 为什么需要 Token 验证
在联合插件架构中,instance 的 setter 是公开的,任何代码都可以调用:
FlutterLibphonenumberPlatform.instance = myCustomImplementation;
如果没有安全机制,恶意代码可以通过 继承(extends) 抽象类来替换平台实现,这可能导致安全问题。
4.2 Token 验证的工作原理
Flutter 官方的 plugin_platform_interface 包提供了 PlatformInterface 基类和 verifyToken 方法:
abstract class FlutterLibphonenumberPlatform extends PlatformInterface {
// 1. 创建一个私有的 token 对象
static final Object _token = Object();
// 2. 构造函数中将 token 传给父类
FlutterLibphonenumberPlatform() : super(token: _token);
// 3. 设置实例时验证 token
static set instance(FlutterLibphonenumberPlatform instance) {
PlatformInterface.verifyToken(instance, _token); // 验证!
_instance = instance;
}
}
验证流程:
_token是一个 私有静态对象,只有FlutterLibphonenumberPlatform类内部可以访问- 子类通过
super(token: _token)将 token 传递给PlatformInterface基类 verifyToken()检查传入实例的 token 是否与预期的_token相同- 如果不匹配,抛出
AssertionError
4.3 extends vs implements 的区别
| 方式 | Token 传递 | 验证结果 | 安全性 |
|---|---|---|---|
extends FlutterLibphonenumberPlatform |
✅ 自动通过 super() 传递 |
✅ 通过 | 安全 |
implements FlutterLibphonenumberPlatform |
❌ 不会调用 super() |
❌ 失败 | 被阻止 |
// ✅ 正确方式:extends(继承)
class FlutterLibphonenumberOhos extends FlutterLibphonenumberPlatform {
// 构造函数自动调用 super(token: _token)
}
// ❌ 错误方式:implements(实现接口)
class FakeImplementation implements FlutterLibphonenumberPlatform {
// 不会调用 super(),verifyToken 会失败
}
安全保障:这个机制确保了只有通过
extends正确继承的子类才能注册为平台实现,防止了通过implements绕过类型系统的攻击。
五、MethodChannel 默认实现
5.1 MethodChannelFlutterLibphonenumber 类
接口包中提供了一个基于 MethodChannel 的默认实现,作为 _instance 的初始值:
const _channel = MethodChannel('com.bottlepay/flutter_libphonenumber');
class MethodChannelFlutterLibphonenumber extends FlutterLibphonenumberPlatform {
MethodChannelFlutterLibphonenumber();
Future<Map<String, String>> format(String phone, String region) async {
return await _channel.invokeMapMethod<String, String>('format', {
'phone': phone,
'region': region,
}) ?? <String, String>{};
}
Future<Map<String, CountryWithPhoneCode>> getAllSupportedRegions() async {
final result = await _channel
.invokeMapMethod<String, dynamic>('get_all_supported_regions') ?? {};
final returnMap = <String, CountryWithPhoneCode>{};
result.forEach((k, v) => returnMap[k] = CountryWithPhoneCode(
countryName: v['countryName'] ?? '',
phoneCode: v['phoneCode'] ?? '',
countryCode: k,
exampleNumberMobileNational: v['exampleNumberMobileNational'] ?? '',
exampleNumberFixedLineNational: v['exampleNumberFixedLineNational'] ?? '',
phoneMaskMobileNational: v['phoneMaskMobileNational'] ?? '',
phoneMaskFixedLineNational: v['phoneMaskFixedLineNational'] ?? '',
exampleNumberMobileInternational: v['exampleNumberMobileInternational'] ?? '',
exampleNumberFixedLineInternational: v['exampleNumberFixedLineInternational'] ?? '',
phoneMaskMobileInternational: v['phoneMaskMobileInternational'] ?? '',
phoneMaskFixedLineInternational: v['phoneMaskFixedLineInternational'] ?? '',
));
return returnMap;
}
}
5.2 init() 的实现逻辑
init() 方法的实现揭示了一个重要的设计模式——先从原生侧获取数据,再在 Dart 侧缓存:
Future<void> init({
Map<String, CountryWithPhoneCode> overrides = const {},
}) async {
return CountryManager().loadCountries(
phoneCodesMap: await getAllSupportedRegions(), // 1. 从原生侧获取全量数据
overrides: overrides, // 2. 应用用户自定义覆盖
);
}
执行步骤:
- 调用
getAllSupportedRegions()通过 MethodChannel 从原生侧获取所有国家数据 - 将数据传给
CountryManager().loadCountries()进行缓存 - 后续的
formatNumberSync()直接从CountryManager读取缓存数据,无需再调用原生侧
5.3 MethodChannel 通道名称
| 包 | 通道名称 | 用途 |
|---|---|---|
| 默认实现(Android/iOS) | com.bottlepay/flutter_libphonenumber |
Android 和 iOS 平台 |
| 鸿蒙实现 | com.bottlepay/flutter_libphonenumber_ohos |
OpenHarmony 平台 |
注意:鸿蒙平台使用了 不同的通道名称(末尾加了
_ohos),这是因为鸿蒙平台包是独立注册的,需要避免与默认实现的通道名称冲突。
六、鸿蒙平台的注册机制
6.1 dartPluginClass 自动注册
鸿蒙平台包通过 pubspec.yaml 中的 dartPluginClass 配置实现 自动注册:
flutter:
plugin:
implements: flutter_libphonenumber
platforms:
ohos:
package: com.bottlepay.flutter_libphonenumber
pluginClass: FlutterLibphonenumberPlugin
dartPluginClass: FlutterLibphonenumberOhos
各字段含义:
| 字段 | 值 | 说明 |
|---|---|---|
implements |
flutter_libphonenumber |
声明本包实现了哪个插件 |
platforms.ohos |
- | 声明支持的平台 |
package |
com.bottlepay.flutter_libphonenumber |
原生包名 |
pluginClass |
FlutterLibphonenumberPlugin |
ArkTS 侧入口类 |
dartPluginClass |
FlutterLibphonenumberOhos |
Dart 侧入口类 |
6.2 registerWith() 静态方法
Flutter 框架在启动时会自动调用 dartPluginClass 指定类的 registerWith() 静态方法:
class FlutterLibphonenumberOhos extends FlutterLibphonenumberPlatform {
/// 注册为默认平台实现
static void registerWith() {
FlutterLibphonenumberPlatform.instance = FlutterLibphonenumberOhos();
}
}
这一行代码完成了三件事:
- 创建实例 —
FlutterLibphonenumberOhos()调用构造函数,自动通过super()传递 token - 验证 Token —
instance的 setter 调用PlatformInterface.verifyToken()验证合法性 - 替换默认实现 — 将
_instance从MethodChannelFlutterLibphonenumber替换为FlutterLibphonenumberOhos
6.3 注册时序图
完整的注册流程按以下时序执行:
Flutter 框架启动
│
├── 1. 扫描所有依赖包的 pubspec.yaml
│ 找到 dartPluginClass: FlutterLibphonenumberOhos
│
├── 2. 生成 GeneratedPluginRegistrant
│ 自动调用 FlutterLibphonenumberOhos.registerWith()
│
├── 3. registerWith() 执行
│ FlutterLibphonenumberPlatform.instance = FlutterLibphonenumberOhos()
│
├── 4. instance setter 执行
│ PlatformInterface.verifyToken(instance, _token) ✅ 通过
│ _instance = FlutterLibphonenumberOhos()
│
└── 5. 注册完成
后续所有 API 调用都路由到鸿蒙实现
零配置:开发者只需在
pubspec.yaml中添加flutter_libphonenumber依赖,Flutter 框架会自动检测当前运行平台,选择对应的平台实现包。鸿蒙设备上会自动使用flutter_libphonenumber_ohos。
七、鸿蒙平台 Dart 侧实现分析
7.1 FlutterLibphonenumberOhos 完整源码
鸿蒙平台的 Dart 侧实现位于 flutter_libphonenumber_ohos.dart,完整代码如下:
import 'package:flutter/services.dart';
import 'package:flutter_libphonenumber_platform_interface/flutter_libphonenumber_platform_interface.dart';
const _channel = MethodChannel('com.bottlepay/flutter_libphonenumber_ohos');
class FlutterLibphonenumberOhos extends FlutterLibphonenumberPlatform {
static void registerWith() {
FlutterLibphonenumberPlatform.instance = FlutterLibphonenumberOhos();
}
Future<Map<String, String>> format(String phone, String region) async {
return await _channel.invokeMapMethod<String, String>('format', {
'phone': phone,
'region': region,
}) ?? <String, String>{};
}
Future<Map<String, CountryWithPhoneCode>> getAllSupportedRegions() async {
final result = await _channel
.invokeMapMethod<String, dynamic>('get_all_supported_regions') ?? {};
final returnMap = <String, CountryWithPhoneCode>{};
result.forEach((k, v) => returnMap[k] = CountryWithPhoneCode(
countryName: v['countryName'] ?? '',
phoneCode: v['phoneCode'] ?? '',
countryCode: k,
exampleNumberMobileNational: v['exampleNumberMobileNational'] ?? '',
exampleNumberFixedLineNational: v['exampleNumberFixedLineNational'] ?? '',
phoneMaskMobileNational: v['phoneMaskMobileNational'] ?? '',
phoneMaskFixedLineNational: v['phoneMaskFixedLineNational'] ?? '',
exampleNumberMobileInternational: v['exampleNumberMobileInternational'] ?? '',
exampleNumberFixedLineInternational: v['exampleNumberFixedLineInternational'] ?? '',
phoneMaskMobileInternational: v['phoneMaskMobileInternational'] ?? '',
phoneMaskFixedLineInternational: v['phoneMaskFixedLineInternational'] ?? '',
));
return returnMap;
}
Future<Map<String, dynamic>> parse(String phone, {String? region}) async {
return await _channel.invokeMapMethod<String, dynamic>('parse', {
'phone': phone,
'region': region,
}) ?? <String, dynamic>{};
}
Future<void> init({
Map<String, CountryWithPhoneCode> overrides = const {},
}) async {
return CountryManager().loadCountries(
phoneCodesMap: await getAllSupportedRegions(),
overrides: overrides,
);
}
}
7.2 与默认 MethodChannel 实现的对比
鸿蒙实现与默认实现的代码结构几乎一致,关键差异在于:
| 对比项 | 默认实现 | 鸿蒙实现 |
|---|---|---|
| 类名 | MethodChannelFlutterLibphonenumber |
FlutterLibphonenumberOhos |
| 通道名 | com.bottlepay/flutter_libphonenumber |
com.bottlepay/flutter_libphonenumber_ohos |
| 注册方式 | 作为 _instance 默认值 |
通过 registerWith() 注册 |
| 原生侧 | Kotlin/Swift | ArkTS |
设计一致性:鸿蒙实现刻意保持了与默认实现相同的代码结构,这降低了维护成本,也方便其他开发者理解代码。
八、主包(App-facing Package)的转发机制
8.1 主包的角色
主包 flutter_libphonenumber 是开发者直接使用的包,它的职责非常简单:
- Re-export 接口包中的数据类型
- 转发 所有 API 调用到当前平台实例
8.2 Re-export 数据类型
export 'package:flutter_libphonenumber_platform_interface/flutter_libphonenumber_platform_interface.dart'
show
CountryManager,
CountryWithPhoneCode,
FormatPhoneResult,
LibPhonenumberTextFormatter,
PhoneMask,
PhoneNumberFormat,
PhoneNumberType;
通过 export ... show,开发者只需 import 'package:flutter_libphonenumber/flutter_libphonenumber.dart' 就能访问所有需要的类型,无需直接依赖 platform_interface 包。
8.3 API 转发实现
主包中的每个函数都是简单的 一行转发:
Future<Map<String, String>> format(String phone, String region) async {
return FlutterLibphonenumberPlatform.instance.format(phone, region);
}
Future<Map<String, CountryWithPhoneCode>> getAllSupportedRegions() async {
return FlutterLibphonenumberPlatform.instance.getAllSupportedRegions();
}
Future<Map<String, dynamic>> parse(String phone, {String? region}) async {
return FlutterLibphonenumberPlatform.instance.parse(phone, region: region);
}
Future<void> init({
Map<String, CountryWithPhoneCode> overrides = const {},
}) async {
return FlutterLibphonenumberPlatform.instance.init(overrides: overrides);
}
String formatNumberSync(String number, { /* 参数省略 */ }) {
return FlutterLibphonenumberPlatform.instance.formatNumberSync(number, /* ... */);
}
透明代理:主包就像一个透明代理,所有调用都通过
FlutterLibphonenumberPlatform.instance路由到当前平台的实现。开发者完全不需要知道底层是 Android、iOS 还是鸿蒙在处理请求。
九、数据流:从 ArkTS 到 Dart 的完整链路
9.1 getAllSupportedRegions() 数据流
以 init() 调用为例,数据从 ArkTS 原生侧流向 Dart 侧的完整链路:
步骤 1: App 调用 init()
│
▼
步骤 2: 主包转发 → FlutterLibphonenumberPlatform.instance.init()
│
▼
步骤 3: FlutterLibphonenumberOhos.init() 执行
│ 调用 getAllSupportedRegions()
│
▼
步骤 4: MethodChannel 发送 'get_all_supported_regions' 到 ArkTS
│
▼
步骤 5: FlutterLibphonenumberPlugin.ets 接收消息
│ 调用 handleGetAllSupportedRegions(result)
│
▼
步骤 6: PhoneNumberUtil.ets 遍历 57 个国家数据
│ 构建 Map<String, Object> 返回
│
▼
步骤 7: result.success(regionsMap) 通过 MethodChannel 返回
│
▼
步骤 8: Dart 侧接收 Map<String, dynamic>
│ 转换为 Map<String, CountryWithPhoneCode>
│
▼
步骤 9: CountryManager().loadCountries() 缓存数据
│
▼
步骤 10: init() 完成,后续 formatNumberSync() 直接读缓存
9.2 ArkTS 侧返回的数据结构
ArkTS 侧为每个国家返回以下结构的 Map:
// PhoneNumberUtil.ets 中构建的返回数据
const regionData: Record<string, Object> = {
'CN': {
'phoneCode': '86',
'countryName': 'China',
'exampleNumberMobileNational': '131 2345 6789',
'exampleNumberFixedLineNational': '010 1234 5678',
'phoneMaskMobileNational': '000 0000 0000',
'phoneMaskFixedLineNational': '000 0000 0000',
'exampleNumberMobileInternational': '+86 131 2345 6789',
'exampleNumberFixedLineInternational': '+86 10 1234 5678',
'phoneMaskMobileInternational': '+00 000 0000 0000',
'phoneMaskFixedLineInternational': '+00 00 0000 0000',
},
// ... 其他 56 个国家
};
9.3 Dart 侧的数据转换
Dart 侧接收到原始 Map 后,逐个转换为 CountryWithPhoneCode 对象:
result.forEach((k, v) => returnMap[k] = CountryWithPhoneCode(
countryName: v['countryName'] ?? '', // 'China'
phoneCode: v['phoneCode'] ?? '', // '86'
countryCode: k, // 'CN'(Map 的 key)
exampleNumberMobileNational: v['exampleNumberMobileNational'] ?? '',
// ... 其他 7 个字段
));
容错设计:每个字段都使用
?? ''提供默认空字符串,确保即使原生侧某个字段缺失,也不会导致空指针异常。
十、CountryManager 单例与数据缓存
10.1 单例模式实现
CountryManager 使用 Dart 的 工厂构造函数 实现单例模式:
class CountryManager {
factory CountryManager() => _instance;
CountryManager._internal();
static final CountryManager _instance = CountryManager._internal();
var _countries = <CountryWithPhoneCode>[];
var _initialized = false;
List<CountryWithPhoneCode> get countries => _countries;
}
关键设计点:
factory CountryManager()— 每次调用CountryManager()都返回同一个_instanceCountryManager._internal()— 私有命名构造函数,防止外部直接实例化_initialized— 标记是否已初始化,防止重复加载
10.2 loadCountries() 加载逻辑
Future<void> loadCountries({
required Map<String, CountryWithPhoneCode> phoneCodesMap,
Map<String, CountryWithPhoneCode> overrides = const {},
}) async {
if (_initialized) return; // 防止重复初始化
try {
// 应用用户自定义覆盖
overrides.forEach((key, value) {
phoneCodesMap[key] = value;
});
// 保存国家列表
_countries = phoneCodesMap.values.toList();
_initialized = true;
} catch (err) {
// 出错时使用 overrides 作为兜底数据
_countries = overrides.values.toList();
}
}
10.3 数据访问方式
初始化完成后,任何地方都可以通过 CountryManager().countries 访问国家数据:
// 获取所有国家列表
final countries = CountryManager().countries;
// 按国家代码查找
final china = countries.firstWhere((c) => c.countryCode == 'CN');
// 按电话区号查找
final us = CountryWithPhoneCode.getCountryDataByPhone('+12015550123');
性能优势:
CountryManager的单例缓存机制意味着 57 个国家的数据只需从原生侧加载 一次,后续所有的同步格式化操作都直接读取内存中的缓存数据,零延迟。
十一、接口包导出的完整类型清单
11.1 barrel 文件导出
flutter_libphonenumber_platform_interface.dart 作为 barrel 文件,导出了接口包中的所有公开类型:
export 'src/platform_interface/flutter_libphonenumber_platform.dart';
export 'src/types/country_manager.dart';
export 'src/types/country_with_phone_code.dart';
export 'src/types/format_phone_result.dart';
export 'src/types/input_formatter.dart';
export 'src/types/phone_mask.dart';
export 'src/types/phone_number_format.dart';
export 'src/types/phone_number_type.dart';
11.2 各类型的职责
| 类型 | 文件 | 职责 |
|---|---|---|
FlutterLibphonenumberPlatform |
flutter_libphonenumber_platform.dart |
抽象基类,定义平台接口 |
CountryManager |
country_manager.dart |
单例,管理国家数据缓存 |
CountryWithPhoneCode |
country_with_phone_code.dart |
国家数据模型(11 个字段) |
FormatPhoneResult |
format_phone_result.dart |
格式化结果(e164 + formattedNumber) |
LibPhonenumberTextFormatter |
input_formatter.dart |
TextField 实时格式化器 |
PhoneMask |
phone_mask.dart |
Mask 应用逻辑 |
PhoneNumberFormat |
phone_number_format.dart |
枚举:national / international |
PhoneNumberType |
phone_number_type.dart |
枚举:mobile / fixedLine |
11.3 类型依赖关系
FlutterLibphonenumberPlatform
├── 使用 CountryWithPhoneCode(参数和返回值)
├── 使用 FormatPhoneResult(getFormattedParseResult 返回值)
├── 使用 PhoneMask(formatNumberSync 内部)
├── 使用 PhoneNumberFormat(格式枚举)
└── 使用 PhoneNumberType(类型枚举)
CountryManager
└── 管理 List<CountryWithPhoneCode>
LibPhonenumberTextFormatter
├── 使用 CountryWithPhoneCode(国家数据)
├── 使用 PhoneMask(mask 应用)
├── 使用 PhoneNumberFormat
└── 使用 PhoneNumberType
十二、与非联合插件方案的对比
12.1 如果不用联合插件架构
假设 flutter_libphonenumber 没有采用联合插件架构,要添加鸿蒙支持需要:
- Fork 原始仓库
- 在
android/、ios/同级目录下添加ohos/目录 - 修改主包的
pubspec.yaml添加 ohos 平台声明 - 修改 Dart 侧代码添加平台判断逻辑
- 提交 PR 等待原作者合并
- 等待原作者发布新版本到 pub.dev
12.2 使用联合插件架构
实际的鸿蒙适配只需要:
- 创建独立的
flutter_libphonenumber_ohos包 - 继承
FlutterLibphonenumberPlatform实现 4 个抽象方法 - 配置
pubspec.yaml的dartPluginClass - 实现 ArkTS 原生侧逻辑
- 独立发布到 pub.dev
12.3 两种方案的对比
| 对比项 | 非联合方案 | 联合插件方案 |
|---|---|---|
| 是否需要修改原仓库 | ✅ 需要 | ❌ 不需要 |
| 是否依赖原作者 | ✅ 依赖 | ❌ 不依赖 |
| 发布独立性 | ❌ 无法独立发布 | ✅ 可独立发布 |
| 代码隔离性 | ❌ 混在一起 | ✅ 完全隔离 |
| 维护成本 | 高(需要同步上游) | 低(只维护自己的包) |
| 适配速度 | 慢(等待 PR 合并) | 快(独立开发发布) |
结论:联合插件架构是 Flutter 三方库鸿蒙适配的 最佳实践。它让适配工作可以完全独立进行,不受原始仓库的限制。
总结
本文深入解析了 flutter_libphonenumber 的联合插件(Federated Plugin)架构。关键要点回顾:
- 联合插件架构 将插件拆分为主包、接口包、平台包三层,各层职责清晰,支持独立开发和发布
- FlutterLibphonenumberPlatform 抽象类定义了 4 个抽象方法和 2 个具体方法,平台包只需实现抽象方法
- PlatformInterface.verifyToken 通过 token 机制确保只有合法的子类才能注册为平台实现
- dartPluginClass + registerWith() 实现了零配置的自动注册,开发者无需手动选择平台实现
- CountryManager 单例 缓存了从原生侧加载的 57 个国家数据,为同步格式化提供零延迟的数据访问
- 联合插件架构是鸿蒙适配的 最佳实践,让适配工作完全独立于原始仓库
下一篇我们将详细讲解鸿蒙平台插件包的创建过程,包括 pubspec.yaml 配置、ohos 目录结构、registerWith 机制的完整实现细节。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony 适配仓库:gitcode.com/oh-flutter/flutter_libphonenumber
- 开源鸿蒙跨平台社区:openharmonycrossplatform.csdn.net
- Flutter 联合插件官方文档:docs.flutter.dev - Federated plugins
- plugin_platform_interface 包:pub.dev/packages/plugin_platform_interface
- Google libphonenumber:github.com/google/libphonenumber
- Flutter MethodChannel 文档:docs.flutter.dev - Platform channels
- Flutter-OHOS 项目:gitee.com/openharmony-sig/flutter_flutter
- Melos 多包管理工具:melos.invertase.dev
- Dart 工厂构造函数文档:dart.dev - Factory constructors
- PhoneNumberKit(iOS):github.com/marmelroy/PhoneNumberKit
更多推荐

所有评论(0)