前言

欢迎来到 Flutter三方库适配OpenHarmony 系列文章!本系列围绕 flutter_libphonenumber 这个 电话号码处理库 的鸿蒙平台适配,进行全面深入的技术分享。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

在这里插入图片描述

上一篇我们从全局视角介绍了 flutter_libphonenumber 的功能定位和鸿蒙适配成果。本篇将深入解析该库采用的 联合插件(Federated Plugin)架构,这是理解整个适配工作的基础。我们将逐层拆解 platform_interfaceMethodChannel、各平台实现包的协作关系,并详细分析鸿蒙平台是如何 无缝接入 这套架构的。

理解联合插件架构是进行任何 Flutter 三方库鸿蒙适配的 第一步,掌握了这套模式,你就能举一反三地适配其他库。


一、什么是联合插件(Federated Plugin)

1.1 传统插件的局限性

在 Flutter 早期,一个插件包通常将 所有平台的实现 放在同一个仓库中:

my_plugin/
├── lib/my_plugin.dart           # Dart API
├── android/                     # Android 实现
├── ios/                         # iOS 实现
└── pubspec.yaml

这种方式存在明显的问题:

  1. 耦合度高 — 添加新平台需要修改原始仓库
  2. 权限受限 — 第三方开发者无法独立贡献新平台支持
  3. 维护困难 — 所有平台代码混在一起,CI/CD 复杂度高
  4. 版本冲突 — 某个平台的 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;
  }
}

验证流程:

  1. _token 是一个 私有静态对象,只有 FlutterLibphonenumberPlatform 类内部可以访问
  2. 子类通过 super(token: _token) 将 token 传递给 PlatformInterface 基类
  3. verifyToken() 检查传入实例的 token 是否与预期的 _token 相同
  4. 如果不匹配,抛出 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. 应用用户自定义覆盖
  );
}

执行步骤:

  1. 调用 getAllSupportedRegions() 通过 MethodChannel 从原生侧获取所有国家数据
  2. 将数据传给 CountryManager().loadCountries() 进行缓存
  3. 后续的 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();
  }
}

这一行代码完成了三件事:

  1. 创建实例FlutterLibphonenumberOhos() 调用构造函数,自动通过 super() 传递 token
  2. 验证 Tokeninstance 的 setter 调用 PlatformInterface.verifyToken() 验证合法性
  3. 替换默认实现 — 将 _instanceMethodChannelFlutterLibphonenumber 替换为 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 是开发者直接使用的包,它的职责非常简单:

  1. Re-export 接口包中的数据类型
  2. 转发 所有 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() 都返回同一个 _instance
  • CountryManager._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 没有采用联合插件架构,要添加鸿蒙支持需要:

  1. Fork 原始仓库
  2. android/ios/ 同级目录下添加 ohos/ 目录
  3. 修改主包的 pubspec.yaml 添加 ohos 平台声明
  4. 修改 Dart 侧代码添加平台判断逻辑
  5. 提交 PR 等待原作者合并
  6. 等待原作者发布新版本到 pub.dev

12.2 使用联合插件架构

实际的鸿蒙适配只需要:

  1. 创建独立的 flutter_libphonenumber_ohos
  2. 继承 FlutterLibphonenumberPlatform 实现 4 个抽象方法
  3. 配置 pubspec.yamldartPluginClass
  4. 实现 ArkTS 原生侧逻辑
  5. 独立发布到 pub.dev

12.3 两种方案的对比

对比项 非联合方案 联合插件方案
是否需要修改原仓库 ✅ 需要 ❌ 不需要
是否依赖原作者 ✅ 依赖 ❌ 不依赖
发布独立性 ❌ 无法独立发布 ✅ 可独立发布
代码隔离性 ❌ 混在一起 ✅ 完全隔离
维护成本 高(需要同步上游) 低(只维护自己的包)
适配速度 慢(等待 PR 合并) 快(独立开发发布)

结论:联合插件架构是 Flutter 三方库鸿蒙适配的 最佳实践。它让适配工作可以完全独立进行,不受原始仓库的限制。


总结

本文深入解析了 flutter_libphonenumber 的联合插件(Federated Plugin)架构。关键要点回顾:

  1. 联合插件架构 将插件拆分为主包、接口包、平台包三层,各层职责清晰,支持独立开发和发布
  2. FlutterLibphonenumberPlatform 抽象类定义了 4 个抽象方法和 2 个具体方法,平台包只需实现抽象方法
  3. PlatformInterface.verifyToken 通过 token 机制确保只有合法的子类才能注册为平台实现
  4. dartPluginClass + registerWith() 实现了零配置的自动注册,开发者无需手动选择平台实现
  5. CountryManager 单例 缓存了从原生侧加载的 57 个国家数据,为同步格式化提供零延迟的数据访问
  6. 联合插件架构是鸿蒙适配的 最佳实践,让适配工作完全独立于原始仓库

下一篇我们将详细讲解鸿蒙平台插件包的创建过程,包括 pubspec.yaml 配置、ohos 目录结构、registerWith 机制的完整实现细节。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐