前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
OhosProductName 类是apple_product_name库在OpenHarmony平台的核心API类 ,封装了所有与设备名称查询相关的功能。本文将从类的定义、设计模式、方法签名、返回值处理、异常机制等多个维度对该类进行全面解析,帮助开发者不仅学会如何使用这些API,更能理解其背后的设计理念和实现原理。
本文是apple_product_name OpenHarmony适配系列的第4篇,共30篇,深入剖析OhosProductName类的完整API设计。
一、类的定义与导入
1.1 导入方式
dart
import 'package:apple_product_name/apple_product_name_ohos.dart';
OhosProductName类定义在apple_product_name_ohos.dart文件中,这是OpenHarmony平台专用的API入口。注意与Apple设备的导入路径区分:
| 平台 | 导入路径 | 核心类 |
|---|---|---|
| OpenHarmony | apple_product_name_ohos.dart |
OhosProductName |
| iOS/macOS | apple_product_name.dart |
AppleProductName |
1.2 完整源码
以下是lib/apple_product_name_ohos.dart的完整真实源码:
dart
/// HarmonyOS platform support for apple_product_name.
library apple_product_name_ohos;
import 'package:flutter/services.dart';
/// 鸿蒙平台设备名称获取工具类
class OhosProductName {
static const MethodChannel _channel = MethodChannel('apple_product_name');
static final _instance = OhosProductName._();
OhosProductName._();
/// Returns the singleton instance of [OhosProductName].
factory OhosProductName() => _instance;
/// 获取设备型号标识符,例如: "ALN-AL00"
Future<String> getMachineId() async {
final String? machineId = await _channel.invokeMethod('getMachineId');
return machineId ?? 'Unknown';
}
/// 获取设备产品名称,例如: "HUAWEI Mate 60 Pro"
Future<String> getProductName() async {
final String? productName = await _channel.invokeMethod('getProductName');
return productName ?? 'Unknown';
}
/// 根据型号标识符查找产品名称
Future<String> lookup(String machineId) async {
final String? productName = await _channel.invokeMethod('lookup', {
'machineId': machineId,
});
return productName ?? machineId;
}
/// 返回产品名称,如果未找到则返回 null
Future<String?> lookupOrNull(String machineId) async {
final String? productName = await _channel.invokeMethod('lookup', {
'machineId': machineId,
});
return productName;
}
}
整个文件只有一个类,代码非常简洁。四个公开方法 + 一个单例构造,总共不到40行有效代码,但覆盖了设备名称查询的所有场景。
二、单例模式设计
2.1 单例实现
dart
class OhosProductName {
static final _instance = OhosProductName._();
OhosProductName._();
factory OhosProductName() => _instance;
}
该类采用Dart语言中经典的单例模式设计,通过三个关键要素实现:
- 私有命名构造函数
OhosProductName._():阻止外部直接创建实例 - 静态final实例
_instance:类加载时创建唯一实例,不可替换 - 工厂构造函数
factory OhosProductName():始终返回同一个实例
2.2 单例模式的优势
| 优势 | 说明 |
|---|---|
| 内存高效 | 整个应用生命周期只存在一个实例 |
| 通道唯一 | 保证MethodChannel通信通道的一致性 |
| 使用简单 | 无需手动管理实例的创建和销毁 |
| 线程安全 | Dart单线程模型下天然线程安全 |
设计理念 :无论在应用的任何位置调用
OhosProductName(),返回的都是同一个实例,内部的MethodChannel也始终是同一个通道。
2.3 与Apple平台的对比
Apple平台的AppleProductName类也采用了相同的单例模式:
dart
class AppleProductName {
static final _instance = AppleProductName._();
AppleProductName._();
factory AppleProductName() => _instance;
String lookup(String machineId) => _lookup(machineId) ?? machineId;
String? lookupOrNull(String machineId) => _lookup(machineId);
}
两者的关键区别:
- AppleProductName :
lookup是同步方法,映射表在Dart层(自动生成的.g.dart文件) - OhosProductName :
lookup是异步方法,映射表在ArkTS原生层
三、MethodChannel通信机制
3.1 通道声明
dart
static const MethodChannel _channel = MethodChannel('apple_product_name');
MethodChannel是Flutter插件体系中最核心的跨平台通信机制 。通道名称'apple_product_name'是Dart侧和原生侧的唯一匹配标识,两端必须完全一致。
3.2 原生侧通道创建
在AppleProductNamePlugin.ets中,原生侧创建了同名通道:
typescript
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "apple_product_name");
this.channel.setMethodCallHandler(this);
}
通道名称"apple_product_name"与Dart侧完全一致,这是通信能够建立的前提。setMethodCallHandler(this)将当前插件实例注册为消息处理器。
3.3 通信流程
Dart层调用 _channel.invokeMethod('getProductName')
→ Flutter引擎序列化方法名和参数
→ 通过BinaryMessenger传递到原生层
→ 原生层MethodChannel接收消息
→ onMethodCall() 路由到对应方法
→ 执行原生逻辑,调用result.success()
→ 结果序列化返回Dart层
→ Future<String> 完成
整个通信过程是异步的,通常在毫秒级完成。
四、getMachineId方法详解
4.1 方法签名与实现
dart
/// 获取设备型号标识符,例如: "ALN-AL00"
Future<String> getMachineId() async {
final String? machineId = await _channel.invokeMethod('getMachineId');
return machineId ?? 'Unknown';
}
getMachineId()用于获取当前设备的原始型号标识符 。返回值类型为Future<String>,是异步操作。内部对null值做了安全处理,返回'Unknown'兜底。
4.2 原生侧实现
typescript
private getMachineId(result: MethodResult): void {
try {
result.success(deviceInfo.productModel);
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
result.error("GET_MACHINE_ID_ERROR", errorMsg, null);
}
}
原生侧直接调用deviceInfo.productModel系统API获取型号标识符。try-catch确保异常时返回结构化错误而非崩溃。
4.3 返回值示例
| 设备 | getMachineId()返回值 |
|---|---|
| HUAWEI Mate 70 | "CFR-AN00" |
| HUAWEI Mate 60 Pro | "ALN-AL00" |
| HUAWEI Pura 70 Ultra | "HBK-AL00" |
| Honor Magic6 Pro | "PGT-AN00" |
| HUAWEI MatePad Pro 13.2 | "GROK-W09" |
五、getProductName方法详解
5.1 方法签名与实现
dart
/// 获取设备产品名称,例如: "HUAWEI Mate 60 Pro"
Future<String> getProductName() async {
final String? productName = await _channel.invokeMethod('getProductName');
return productName ?? 'Unknown';
}
getProductName()是该类中最常用的方法,直接返回当前设备的用户友好产品名称。
5.2 原生侧两级查找策略
typescript
private getProductName(result: MethodResult): void {
try {
const model = deviceInfo.productModel;
// 先从映射表查找
let productName = HUAWEI_DEVICE_MAP[model];
// 如果映射表没有,使用系统的 marketName
if (!productName) {
productName = deviceInfo.marketName || model;
}
result.success(productName);
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
result.error("GET_PRODUCT_NAME_ERROR", errorMsg, null);
}
}
这是一个三级降级策略:
- 首先在
HUAWEI_DEVICE_MAP映射表中查找(最精确) - 映射表未收录时,使用系统
deviceInfo.marketName(次精确) - marketName也不可用时,返回原始型号
model(兜底)
设计亮点:这种降级策略确保即使遇到映射表尚未收录的新设备,也不会返回空值,用户始终能看到有意义的设备信息。
5.3 getProductName vs getMachineId
| 对比项 | getProductName() | getMachineId() |
|---|---|---|
| 返回值 | "HUAWEI Mate 60 Pro" | "ALN-AL00" |
| 可读性 | 高,用户友好 | 低,技术标识 |
| 数据来源 | 映射表 + marketName + model | deviceInfo.productModel |
| 典型场景 | UI展示、关于页面 | 日志记录、问题排查 |


上图展示了两个方法返回值在应用界面中的对比效果。
六、lookup方法详解
6.1 方法签名与实现
dart
/// 根据型号标识符查找产品名称
Future<String> lookup(String machineId) async {
final String? productName = await _channel.invokeMethod('lookup', {
'machineId': machineId,
});
return productName ?? machineId;
}
lookup()允许通过任意型号标识符查询对应的产品名称,不限于当前设备。参数通过Map传递到原生层。
6.2 原生侧实现
typescript
private lookup(call: MethodCall, result: MethodResult): void {
try {
const machineId = call.argument("machineId") as string;
if (!machineId) {
result.error("INVALID_ARGUMENT", "machineId is required", null);
return;
}
const productName = HUAWEI_DEVICE_MAP[machineId];
result.success(productName); // 如果没找到返回 null
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
result.error("LOOKUP_ERROR", errorMsg, null);
}
}
原生侧的lookup方法有参数校验 :如果machineId为空,直接返回INVALID_ARGUMENT错误。查找逻辑只在映射表中查找,不使用marketName降级(因为查询的不一定是当前设备)。
6.3 使用示例
dart
// 查询已知型号
final name1 = await OhosProductName().lookup('ALN-AL00');
print(name1); // "HUAWEI Mate 60 Pro"
// 查询未知型号 → 返回原始machineId
final name2 = await OhosProductName().lookup('XXX-XX00');
print(name2); // "XXX-XX00"
// 批量查询
final models = ['CFR-AN00', 'HBN-AL00', 'PGT-AN00'];
for (final model in models) {
final name = await OhosProductName().lookup(model);
print('$model → $name');
}
// CFR-AN00 → HUAWEI Mate 70
// HBN-AL00 → HUAWEI Pura 70
// PGT-AN00 → Honor Magic6 Pro
注意 :
lookup()未找到时返回原始machineId而非"Unknown",这是与getProductName()的重要区别。
七、lookupOrNull方法详解
7.1 方法签名与实现
dart
/// 返回产品名称,如果未找到则返回 null
Future<String?> lookupOrNull(String machineId) async {
final String? productName = await _channel.invokeMethod('lookup', {
'machineId': machineId,
});
return productName;
}
lookupOrNull()是lookup()的变体,返回类型为Future<String?>,未找到时返回null而非原始machineId。
7.2 lookup vs lookupOrNull
dart
// lookup: 未找到时返回原始machineId
final name1 = await OhosProductName().lookup('UNKNOWN-MODEL');
print(name1); // "UNKNOWN-MODEL"
print(name1 == null); // false
// lookupOrNull: 未找到时返回null
final name2 = await OhosProductName().lookupOrNull('UNKNOWN-MODEL');
print(name2); // null
print(name2 == null); // true
两者在原生侧调用的是同一个方法,区别仅在Dart层的null处理:
| 对比项 | lookup() | lookupOrNull() |
|---|---|---|
| 返回类型 | Future<String> |
Future<String?> |
| 未找到时 | 返回原始machineId | 返回null |
| 是否可空 | 永远非空 | 可能为空 |
| 适用场景 | UI展示(保证有值) | 逻辑判断(区分已知/未知) |
7.3 典型应用场景
dart
// 场景:筛选映射表未收录的设备
Future<List<String>> findUnknownDevices(List<String> models) async {
final unknown = <String>[];
final ohos = OhosProductName();
for (final model in models) {
final name = await ohos.lookupOrNull(model);
if (name == null) {
unknown.add(model);
}
}
return unknown;
}
// 使用
final unknowns = await findUnknownDevices(['ALN-AL00', 'XXX-XX00', 'YYY-YY00']);
print('未收录型号: $unknowns'); // ['XXX-XX00', 'YYY-YY00']
lookupOrNull在需要区分"已知设备"和"未知设备"的场景中非常有用,比如统计映射表覆盖率、上报未收录型号等。
八、四个方法完整对比
8.1 API速查表
dart
final ohos = OhosProductName();
// 1. 获取当前设备产品名称
final productName = await ohos.getProductName(); // "HUAWEI Mate 60 Pro"
// 2. 获取当前设备型号标识符
final machineId = await ohos.getMachineId(); // "ALN-AL00"
// 3. 查询任意型号(未找到返回原始id)
final name = await ohos.lookup('ALN-AL00'); // "HUAWEI Mate 60 Pro"
// 4. 查询任意型号(未找到返回null)
final nameOrNull = await ohos.lookupOrNull('ALN-AL00'); // "HUAWEI Mate 60 Pro"
8.2 完整对比表
| 方法 | 参数 | 返回类型 | 未找到时 | 数据来源 | 场景 |
|---|---|---|---|---|---|
getProductName() |
无 | Future<String> |
"Unknown" | 映射表→marketName→model | UI展示 |
getMachineId() |
无 | Future<String> |
"Unknown" | deviceInfo.productModel | 日志记录 |
lookup(id) |
machineId | Future<String> |
原始id | 映射表 | 批量转换 |
lookupOrNull(id) |
machineId | Future<String?> |
null | 映射表 | 覆盖率检测 |
8.3 选择决策树
选择哪个方法,可以按以下逻辑判断:
- 需要当前设备的友好名称?→ 用
getProductName() - 需要当前设备的原始型号?→ 用
getMachineId() - 需要查询其他型号且保证非空?→ 用
lookup() - 需要判断型号是否在映射表中?→ 用
lookupOrNull()
九、原生侧方法路由
9.1 onMethodCall路由逻辑
以下是AppleProductNamePlugin.ets中的方法路由真实源码:
typescript
onMethodCall(call: MethodCall, result: MethodResult): void {
switch (call.method) {
case "getMachineId":
this.getMachineId(result);
break;
case "getProductName":
this.getProductName(result);
break;
case "lookup":
this.lookup(call, result);
break;
default:
result.notImplemented();
break;
}
}
onMethodCall通过switch语句将Dart层的方法调用路由到对应的原生实现。call.method是Dart层invokeMethod的第一个参数(方法名字符串)。未识别的方法名会调用result.notImplemented()。
9.2 方法名映射关系
| Dart层调用 | call.method值 | 原生层方法 |
|---|---|---|
_channel.invokeMethod('getMachineId') |
"getMachineId" | this.getMachineId(result) |
_channel.invokeMethod('getProductName') |
"getProductName" | this.getProductName(result) |
_channel.invokeMethod('lookup', {...}) |
"lookup" | this.lookup(call, result) |
关键点 :
lookup方法需要传递参数,所以原生侧方法签名多了一个call参数,用于通过call.argument("machineId")提取传入的型号标识符。
十、异常处理与错误码
10.1 推荐的异常处理模式
dart
import 'package:flutter/services.dart';
import 'package:apple_product_name/apple_product_name_ohos.dart';
Future<String> safeGetProductName() async {
try {
return await OhosProductName().getProductName();
} on PlatformException catch (e) {
print('平台异常: ${e.code} - ${e.message}');
return 'Unknown Device';
} on MissingPluginException {
print('插件未注册,请检查GeneratedPluginRegistrant');
return 'Unknown Device';
} catch (e) {
print('未知异常: $e');
return 'Unknown Device';
}
}
推荐的异常处理层级:
- PlatformException :原生侧通过
result.error()返回的结构化异常 - MissingPluginException:插件未注册,MethodChannel找不到处理器
- 通用catch:兜底处理所有其他异常
10.2 错误码一览
| 错误码 | 触发方法 | 含义 |
|---|---|---|
| GET_MACHINE_ID_ERROR | getMachineId | 获取型号标识符失败 |
| GET_PRODUCT_NAME_ERROR | getProductName | 获取产品名称失败 |
| LOOKUP_ERROR | lookup | 查询映射表失败 |
| INVALID_ARGUMENT | lookup | machineId参数为空 |
10.3 MissingPluginException排查
如果遇到MissingPluginException,按以下步骤排查:
- 检查
GeneratedPluginRegistrant.ets是否导入了AppleProductNamePlugin - 检查
EntryAbility.ets是否调用了registerWith - 确认
oh-package.json5中的依赖配置正确 - 执行
flutter clean后重新构建
十一、完整使用示例
11.1 综合展示页面
dart
import 'package:flutter/material.dart';
import 'package:apple_product_name/apple_product_name_ohos.dart';
class OhosProductNameDemo extends StatefulWidget {
@override
_OhosProductNameDemoState createState() => _OhosProductNameDemoState();
}
class _OhosProductNameDemoState extends State<OhosProductNameDemo> {
final _ohos = OhosProductName();
String _productName = '加载中...';
String _machineId = '加载中...';
String _lookupResult = '加载中...';
String? _lookupOrNullResult;
@override
void initState() {
super.initState();
_loadAll();
}
Future<void> _loadAll() async {
try {
final productName = await _ohos.getProductName();
final machineId = await _ohos.getMachineId();
final lookupResult = await _ohos.lookup('CFR-AN00');
final lookupOrNullResult = await _ohos.lookupOrNull('XXX-XX00');
setState(() {
_productName = productName;
_machineId = machineId;
_lookupResult = lookupResult;
_lookupOrNullResult = lookupOrNullResult;
});
} catch (e) {
setState(() {
_productName = '获取失败: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('OhosProductName 详解')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow('getProductName()', _productName),
_buildInfoRow('getMachineId()', _machineId),
_buildInfoRow('lookup("CFR-AN00")', _lookupResult),
_buildInfoRow('lookupOrNull("XXX-XX00")',
_lookupOrNullResult ?? 'null'),
],
),
),
);
}
Widget _buildInfoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$label: ', style: const TextStyle(fontWeight: FontWeight.bold)),
Expanded(child: Text(value)),
],
),
);
}
}
这个页面综合展示了四个方法的调用结果,使用_buildInfoRow辅助方法统一构建每行的布局,代码更加整洁。
11.2 预期运行结果
| 方法调用 | 预期返回值 |
|---|---|
getProductName() |
当前设备的友好名称(如"HUAWEI Mate 60 Pro") |
getMachineId() |
当前设备的型号标识符(如"ALN-AL00") |
lookup("CFR-AN00") |
"HUAWEI Mate 70" |
lookupOrNull("XXX-XX00") |
null(映射表未收录) |
十二、设备映射表覆盖范围
12.1 映射表代码片段
typescript
const HUAWEI_DEVICE_MAP: Record<string, string> = {
// Mate 70 系列
"CFR-AN00": "HUAWEI Mate 70",
"CFS-AN00": "HUAWEI Mate 70 Pro",
"CFT-AN00": "HUAWEI Mate 70 Pro+",
"CFU-AN00": "HUAWEI Mate 70 RS 非凡大师",
// Mate 60 系列
"BRA-AL00": "HUAWEI Mate 60",
"ALN-AL00": "HUAWEI Mate 60 Pro",
"GGK-AL10": "HUAWEI Mate 60 Pro+",
// Pura 70 系列
"HBN-AL00": "HUAWEI Pura 70",
"DUA-AL00": "HUAWEI Pura 70 Pro",
"HBK-AL00": "HUAWEI Pura 70 Ultra",
// 荣耀系列
"PGT-AN00": "Honor Magic6 Pro",
"BVL-AN00": "Honor Magic6",
// ... 共90+个型号映射
};
12.2 设备系列覆盖统计
| 设备系列 | 代表型号 | 映射数量 | 说明 |
|---|---|---|---|
| Mate系列 | Mate 70/60/X5/X3 | 22个 | 旗舰手机+折叠屏 |
| Pura系列 | Pura 70/P60 | 12个 | 影像旗舰 |
| nova系列 | nova 13/12/11 | 14个 | 年轻时尚系列 |
| Pocket系列 | Pocket 2/S | 4个 | 小折叠屏 |
| MatePad系列 | MatePad Pro/Air | 10个 | 平板设备 |
| 荣耀Magic | Magic6/5 | 8个 | 荣耀旗舰 |
| 荣耀数字 | Honor 200/100 | 8个 | 荣耀中端 |
| 华为手表 | WATCH GT/Ultimate | 12个 | 智能手表 |
映射表会随新设备发布持续更新,社区开发者可以通过提交Pull Request贡献新设备数据。
十三、高级用法
13.1 缓存优化
由于每次调用都会经过MethodChannel通信,频繁调用可能影响性能。建议在应用启动时缓存结果:
dart
class DeviceInfoCache {
static String? _productName;
static String? _machineId;
static Future<String> getProductName() async {
_productName ??= await OhosProductName().getProductName();
return _productName!;
}
static Future<String> getMachineId() async {
_machineId ??= await OhosProductName().getMachineId();
return _machineId!;
}
}
// 使用:多次调用只会触发一次MethodChannel通信
final name1 = await DeviceInfoCache.getProductName(); // 第一次:走MethodChannel
final name2 = await DeviceInfoCache.getProductName(); // 第二次:直接返回缓存
??=运算符确保只在首次调用时执行异步操作,后续调用直接返回缓存值。
13.2 与FutureBuilder结合
dart
FutureBuilder<String>(
future: OhosProductName().getProductName(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('错误: ${snapshot.error}');
}
return Text(snapshot.data ?? 'Unknown');
},
)
FutureBuilder自动管理异步状态,适合简单的数据展示场景。
总结
OhosProductName 类通过单例模式 确保资源高效利用,通过MethodChannel 实现可靠的跨平台通信,通过多级降级策略 保证返回值的可用性。四个核心方法(getProductName、getMachineId、lookup、lookupOrNull)覆盖了从UI展示到数据分析的所有场景,API设计简洁而完备。
下一篇文章将深入介绍设备型号标识符转换原理,解析映射表的数据结构和查找算法。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony适配仓库:https://gitcode.com/oh-flutter/flutter_apple_product_name
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- 原始GitHub仓库:https://github.com/kyle-seongwoo-jun/flutter_apple_product_name
- Flutter OHOS仓库:https://gitee.com/openharmony-sig/flutter_flutter
- device_info_plus:https://pub.dev/packages/device_info_plus
- Flutter MethodChannel文档:https://flutter.dev/docs/development/platform-integration/platform-channels
- OpenHarmony官方文档:https://www.openharmony.cn
- Dart单例模式:https://dart.dev/language/constructors#factory-constructors