# Flutter三方库适配OpenHarmony【flutter_libphonenumber】——联合插件(Federated Plugin)架构解析
前言
欢迎来到 Flutter三方库适配OpenHarmony 系列文章!本系列围绕 flutter_libphonenumber 这个 电话号码处理库 的鸿蒙平台适配,进行全面深入的技术分享。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.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';abstractclassFlutterLibphonenumberPlatformextendsPlatformInterface{/// 构造函数,传入 token 用于安全验证FlutterLibphonenumberPlatform():super(token: _token);/// 私有 token 对象,用于 verifyToken 安全机制staticfinalObject _token =Object();/// 默认实例,初始为 MethodChannel 实现staticFlutterLibphonenumberPlatform _instance =MethodChannelFlutterLibphonenumber();/// 获取当前平台实例staticFlutterLibphonenumberPlatformget instance => _instance;/// 设置平台实例(各平台包在 registerWith 中调用)staticsetinstance(FlutterLibphonenumberPlatform instance){PlatformInterface.verifyToken(instance, _token); _instance = instance;}}关键设计:_instance的默认值是MethodChannelFlutterLibphonenumber(),这意味着如果没有任何平台包注册自己,系统会回退到 MethodChannel 默认实现。
3.2 抽象方法定义
该抽象类定义了 5 个核心抽象方法 和 2 个具体方法:
// ===== 抽象方法(各平台必须实现)=====/// 获取所有支持的国家/地区数据Future<Map<String,CountryWithPhoneCode>>getAllSupportedRegions()async{throwUnimplementedError('getAllSupportedRegions() has not been implemented.');}/// 异步格式化电话号码Future<Map<String,String>>format(String phone,String region)async{throwUnimplementedError('format() has not been implemented.');}/// 解析电话号码,返回元数据Future<Map<String,dynamic>>parse(String phone,{String? region})async{throwUnimplementedError('parse() has not been implemented.');}/// 初始化,加载国家数据Future<void>init({Map<String,CountryWithPhoneCode> overrides =const{},})async{throwUnimplementedError('init() has not been implemented.');}3.3 具体方法(无需平台实现)
抽象类中还包含 2 个具体方法,它们的逻辑完全在 Dart 侧完成,各平台包 无需重写:
/// 同步格式化 — 纯 Dart 侧 mask 匹配,无需原生调用StringformatNumberSync(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 =awaitparse(phoneNumber, region: country.countryCode); late finalString formattedNumber;if(phoneNumberFormat ==PhoneNumberFormat.international){ formattedNumber = res['international']??'';}elseif(phoneNumberFormat ==PhoneNumberFormat.national){ formattedNumber = res['national']??'';}else{ formattedNumber ='';}returnFormatPhoneResult( e164: res['e164']??'', formattedNumber: formattedNumber,);}catch(e){// 解析失败返回 null}returnnull;}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 方法:
abstractclassFlutterLibphonenumberPlatformextendsPlatformInterface{// 1. 创建一个私有的 token 对象staticfinalObject _token =Object();// 2. 构造函数中将 token 传给父类FlutterLibphonenumberPlatform():super(token: _token);// 3. 设置实例时验证 tokenstaticsetinstance(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(继承)classFlutterLibphonenumberOhosextendsFlutterLibphonenumberPlatform{// 构造函数自动调用 super(token: _token)}// ❌ 错误方式:implements(实现接口)classFakeImplementationimplementsFlutterLibphonenumberPlatform{// 不会调用 super(),verifyToken 会失败}安全保障:这个机制确保了只有通过extends正确继承的子类才能注册为平台实现,防止了通过implements绕过类型系统的攻击。
五、MethodChannel 默认实现
5.1 MethodChannelFlutterLibphonenumber 类
接口包中提供了一个基于 MethodChannel 的默认实现,作为 _instance 的初始值:
const _channel =MethodChannel('com.bottlepay/flutter_libphonenumber');classMethodChannelFlutterLibphonenumberextendsFlutterLibphonenumberPlatform{MethodChannelFlutterLibphonenumber();@overrideFuture<Map<String,String>>format(String phone,String region)async{returnawait _channel.invokeMapMethod<String,String>('format',{'phone': phone,'region': region,})??<String,String>{};}@overrideFuture<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 侧缓存:
@overrideFuture<void>init({Map<String,CountryWithPhoneCode> overrides =const{},})async{returnCountryManager().loadCountries( phoneCodesMap:awaitgetAllSupportedRegions(),// 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() 静态方法:
classFlutterLibphonenumberOhosextendsFlutterLibphonenumberPlatform{/// 注册为默认平台实现staticvoidregisterWith(){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');classFlutterLibphonenumberOhosextendsFlutterLibphonenumberPlatform{staticvoidregisterWith(){FlutterLibphonenumberPlatform.instance =FlutterLibphonenumberOhos();}@overrideFuture<Map<String,String>>format(String phone,String region)async{returnawait _channel.invokeMapMethod<String,String>('format',{'phone': phone,'region': region,})??<String,String>{};}@overrideFuture<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;}@overrideFuture<Map<String,dynamic>>parse(String phone,{String? region})async{returnawait _channel.invokeMapMethod<String,dynamic>('parse',{'phone': phone,'region': region,})??<String,dynamic>{};}@overrideFuture<void>init({Map<String,CountryWithPhoneCode> overrides =const{},})async{returnCountryManager().loadCountries( phoneCodesMap:awaitgetAllSupportedRegions(), 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'showCountryManager,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{returnFlutterLibphonenumberPlatform.instance.format(phone, region);}Future<Map<String,CountryWithPhoneCode>>getAllSupportedRegions()async{returnFlutterLibphonenumberPlatform.instance.getAllSupportedRegions();}Future<Map<String,dynamic>>parse(String phone,{String? region})async{returnFlutterLibphonenumberPlatform.instance.parse(phone, region: region);}Future<void>init({Map<String,CountryWithPhoneCode> overrides =const{},})async{returnFlutterLibphonenumberPlatform.instance.init(overrides: overrides);}StringformatNumberSync(String number,{/* 参数省略 */}){returnFlutterLibphonenumberPlatform.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 的 工厂构造函数 实现单例模式:
classCountryManager{factoryCountryManager()=> _instance;CountryManager._internal();staticfinalCountryManager _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.ZEEKLOG.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