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

设备信息展示 是 apple_product_name 库最直接的应用场景。无论是"关于手机"页面、客服支持界面还是应用内调试工具,都需要将设备型号标识符转换为用户可读的产品名称并直观展示。本文将从零构建一个应用级别 的设备信息展示页面,涵盖数据加载、UI 布局、交互设计、错误处理等完整流程,所有代码均基于 apple_product_name 库的真实 API。
先给出本文的核心要点:
- 三个 API 协同 :
getMachineId()获取型号、getProductName()获取名称、lookup()查询映射 - 并行加载 :使用
Future.wait同时获取多项设备信息,减少等待时间 - 应用级 UI:渐变 AppBar、设备卡片、系列标签、交互查询,贴近真实产品设计
提示:本文的演示页面代码可直接运行在 OpenHarmony 设备上,建议对照 源码仓库 阅读。
一、API 回顾与数据流设计
1.1 apple_product_name 提供的三个 API
在构建页面之前,先回顾 OhosProductName 类提供的三个核心方法:
dart
class OhosProductName {
/// 获取设备型号标识符,如 "ALN-AL00"
Future<String> getMachineId() async { ... }
/// 获取设备产品名称,如 "HUAWEI Mate 60 Pro"
Future<String> getProductName() async { ... }
/// 根据型号标识符查找产品名称
Future<String> lookup(String machineId) async { ... }
}
1.2 三个 API 的数据来源
| API | 原生层实现 | 数据来源 | 返回示例 |
|---|---|---|---|
getMachineId() |
deviceInfo.productModel |
系统硬件信息 | "ALN-AL00" |
getProductName() |
映射表 → marketName → productModel | 三级回退 | "HUAWEI Mate 60 Pro" |
lookup(id) |
HUAWEI_DEVICE_MAP[id] |
映射表查找 | "HUAWEI Mate 60 Pro" |
1.3 数据流设计
页面的数据流遵循单向数据流模式:
用户点击按钮
→ 调用 OhosProductName API
→ MethodChannel 跨平台通信
→ 原生层读取 deviceInfo / 查询映射表
→ result.success(data)
→ Future 完成
→ setState 更新 UI
这种设计的好处是数据流向清晰,每一步都可以独立调试和测试。
提示:
getMachineId()和getProductName()是独立的 MethodChannel 调用 ,可以使用Future.wait并行执行,减少总等待时间。
二、数据模型定义
2.1 设备信息数据类
为了在 UI 层方便传递和展示数据,定义一个简单的数据模型:
dart
class _InfoItem {
final IconData icon;
final String label;
final String value;
final Color color;
const _InfoItem({
required this.icon,
required this.label,
required this.value,
required this.color,
});
}
2.2 设备系列分类
根据产品名称中的关键词,将设备分为不同系列,每个系列有独立的图标和颜色:
dart
class _DeviceSeries {
final String name;
final IconData icon;
final Color color;
const _DeviceSeries(this.name, this.icon, this.color);
}
2.3 系列识别逻辑
dart
_DeviceSeries _getSeries(String name) {
if (name.contains('MatePad')) {
return _DeviceSeries('MatePad 系列', Icons.tablet_android, Color(0xFF00897B));
} else if (name.contains('Mate X') || name.contains('Pocket')) {
return _DeviceSeries('折叠屏系列', Icons.unfold_more, Color(0xFF8E24AA));
} else if (name.contains('Mate')) {
return _DeviceSeries('Mate 系列', Icons.phone_android, Color(0xFFC62828));
} else if (name.contains('Pura') || name.contains(' P6')) {
return _DeviceSeries('Pura 系列', Icons.camera_alt, Color(0xFFEF6C00));
} else if (name.contains('nova')) {
return _DeviceSeries('nova 系列', Icons.auto_awesome, Color(0xFFE91E63));
} else if (name.contains('WATCH')) {
return _DeviceSeries('智能手表', Icons.watch, Color(0xFF2E7D32));
} else if (name.contains('Honor') || name.contains('Magic')) {
return _DeviceSeries('荣耀系列', Icons.star, Color(0xFF1565C0));
}
return _DeviceSeries('其他设备', Icons.devices, Color(0xFF546E7A));
}
关键词匹配的顺序很重要------MatePad 必须在 Mate 之前判断,否则 "HUAWEI MatePad Pro" 会被错误归类为 Mate 系列。
| 关键词 | 系列名称 | 图标 | 颜色 | 匹配示例 |
|---|---|---|---|---|
MatePad |
MatePad 系列 | 平板 | 青色 | HUAWEI MatePad Pro 13.2 |
Mate X / Pocket |
折叠屏系列 | 展开 | 紫色 | HUAWEI Mate X5 |
Mate |
Mate 系列 | 手机 | 红色 | HUAWEI Mate 70 Pro |
Pura / P6 |
Pura 系列 | 相机 | 橙色 | HUAWEI Pura 70 Ultra |
nova |
nova 系列 | 星星 | 粉色 | HUAWEI nova 13 Pro |
WATCH |
智能手表 | 手表 | 绿色 | HUAWEI WATCH GT 4 |
Honor / Magic |
荣耀系列 | 星标 | 蓝色 | Honor Magic6 Pro |
注意:系列识别基于
getProductName()返回的名称字符串,而名称来自插件的HUAWEI_DEVICE_MAP映射表。如果映射表中没有收录当前设备,返回的是系统marketName,关键词匹配仍然有效。
三、页面状态管理
3.1 状态变量设计
页面需要管理以下状态:
dart
class _Article23DemoPageState extends State<Article23DemoPage> {
// 设备信息
String? _machineId;
String? _productName;
String? _lookupResult;
DateTime? _loadTime;
bool _isLoading = false;
String? _error;
// 查询功能
final _lookupController = TextEditingController();
String? _queryResult;
bool _isQuerying = false;
}
3.2 状态分类
| 状态变量 | 类型 | 初始值 | 用途 |
|---|---|---|---|
_machineId |
String? |
null |
设备型号标识符 |
_productName |
String? |
null |
设备产品名称 |
_lookupResult |
String? |
null |
映射查询结果 |
_loadTime |
DateTime? |
null |
数据获取时间 |
_isLoading |
bool |
false |
加载中状态 |
_error |
String? |
null |
错误信息 |
_queryResult |
String? |
null |
型号查询结果 |
_isQuerying |
bool |
false |
查询中状态 |
3.3 为什么不用 FutureBuilder
前面的 example 项目使用了 FutureBuilder,但本页面选择手动管理状态,原因如下:
- 页面有多个独立的异步操作(加载设备信息 + 型号查询)
- 需要支持重试 和刷新功能
- 需要在加载完成后保持数据,而非每次
setState都重新加载 FutureBuilder在setState时会重新触发 Future,导致不必要的重复请求
手动管理状态虽然代码量稍多,但控制力更强,适合复杂页面。
四、数据加载逻辑
4.1 并行加载实现
dart
Future<void> _loadDeviceInfo() async {
if (_isLoading) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final ohos = OhosProductName();
final results = await Future.wait([
ohos.getMachineId(),
ohos.getProductName(),
]);
final id = results[0];
final name = results[1];
final lookup = await ohos.lookup(id);
setState(() {
_machineId = id;
_productName = name;
_lookupResult = lookup;
_loadTime = DateTime.now();
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
4.2 加载流程分析
加载过程分为三步:
-
并行获取 :
Future.wait同时调用getMachineId()和getProductName() -
串行查询 :拿到
machineId后调用lookup(id)验证映射结果 -
更新状态 :一次性
setState更新所有数据_loadDeviceInfo()
├── Future.wait([getMachineId(), getProductName()]) // 并行
│ ├── getMachineId() → "ALN-AL00"
│ └── getProductName() → "HUAWEI Mate 60 Pro"
└── lookup("ALN-AL00") → "HUAWEI Mate 60 Pro" // 串行
4.3 防重复加载
dart
if (_isLoading) return; // 防止重复点击
这行代码防止用户在加载过程中重复点击按钮。_isLoading 同时控制按钮的 onPressed 属性------加载中时按钮变为禁用状态。
提示:
Future.wait的并行执行比串行调用快约 50% ,因为两个 MethodChannel 调用可以同时发送到原生层。原生层的onMethodCall是同步处理的,但 MethodChannel 的消息传递是异步的。
五、页面整体布局
5.1 CustomScrollView 架构
页面使用 CustomScrollView + SliverAppBar 实现可折叠的渐变顶栏:
dart
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: [
// 渐变 AppBar
SliverAppBar(
expandedHeight: 140,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('设备信息', style: TextStyle(fontSize: 18)),
background: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF3949AB), Color(0xFF1A237E)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
),
),
actions: [
if (_machineId != null)
IconButton(
icon: const Icon(Icons.share),
onPressed: _shareDeviceInfo,
tooltip: '分享设备信息',
),
],
),
// 内容区域
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([ ... ]),
),
),
],
),
);
}
5.2 布局层次
CustomScrollView
├── SliverAppBar(渐变顶栏 + 分享按钮)
└── SliverPadding
└── SliverList
├── 欢迎卡片(首次进入时显示)
├── 操作按钮(获取/刷新设备信息)
├── 错误提示(加载失败时显示)
├── 设备卡片(产品名称 + 系列标签 + 型号)
├── 信息列表(4 行详细信息,可点击复制)
└── 型号查询区域(快捷标签 + 输入框 + 结果)
5.3 为什么选择 CustomScrollView
与普通的 ListView 相比,CustomScrollView 的优势:
- 支持
SliverAppBar的折叠效果------向上滚动时顶栏自动收缩 - 可以混合不同类型的 Sliver 组件
pinned: true让标题在滚动时始终可见- 渐变背景在折叠过程中自然过渡
提示:
SliverAppBar的expandedHeight: 140设置了展开高度,pinned: true确保收缩后标题栏仍然固定在顶部。这种效果在 Material Design 中被称为 Collapsing Toolbar。
六、欢迎卡片设计
6.1 首次进入的引导
当用户首次打开页面时,显示一个欢迎卡片引导操作:
dart
Widget _buildWelcomeCard() {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 2),
)],
),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF3949AB).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.phone_android, size: 48, color: Color(0xFF3949AB)),
),
const SizedBox(height: 16),
const Text('设备信息展示',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('点击下方按钮获取当前设备的产品名称、型号标识符等信息',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, color: Colors.grey.shade600, height: 1.5)),
],
),
);
}
6.2 条件显示逻辑
欢迎卡片只在首次进入时显示,获取到设备信息后自动隐藏:
dart
if (_machineId == null && _error == null) ...[
_buildWelcomeCard(),
const SizedBox(height: 16),
],
这种条件渲染避免了页面在有数据时仍然显示引导信息,保持界面简洁。
七、设备信息卡片
7.1 卡片实现
设备卡片是页面的视觉焦点,展示产品名称、系列标签和型号标识符:
dart
Widget _buildDeviceCard() {
final series = _getSeries(_productName ?? '');
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
series.color.withValues(alpha: 0.08),
series.color.withValues(alpha: 0.02),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: series.color.withValues(alpha: 0.2)),
),
child: Column(
children: [
// 设备图标
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: series.color.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: Icon(series.icon, size: 40, color: series.color),
),
const SizedBox(height: 14),
// 产品名称
Text(_productName ?? 'Unknown',
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
textAlign: TextAlign.center),
const SizedBox(height: 8),
// 系列标签
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: series.color,
borderRadius: BorderRadius.circular(20),
),
child: Text(series.name,
style: const TextStyle(color: Colors.white, fontSize: 12)),
),
const SizedBox(height: 12),
// 型号标识
Text(_machineId ?? '',
style: TextStyle(fontSize: 14, color: Colors.grey.shade600,
fontFamily: 'monospace', letterSpacing: 1)),
],
),
);
}
7.2 动态配色方案
卡片的颜色随设备系列动态变化:
- Mate 系列 → 红色渐变背景
- Pura 系列 → 橙色渐变背景
- nova 系列 → 粉色渐变背景
- MatePad 系列 → 青色渐变背景
这种设计让不同设备的展示页面各具特色,增强了视觉辨识度。
7.3 卡片结构层次
Container(渐变背景 + 圆角边框)
├── 圆形图标容器(系列图标)
├── 产品名称(大号加粗文字)
├── 系列标签(彩色圆角标签)
└── 型号标识(等宽字体,灰色)
提示:
fontFamily: 'monospace'让型号标识符使用等宽字体显示,使 "ALN-AL00" 这样的编码更易读。letterSpacing: 1增加字符间距,进一步提升可读性。
八、信息列表与复制功能
8.1 信息列表构建
设备卡片下方是详细的信息列表,每行展示一项数据:
dart
Widget _buildInfoList() {
final items = [
_InfoItem(icon: Icons.phone_android, label: '产品名称',
value: _productName ?? '', color: const Color(0xFF3949AB)),
_InfoItem(icon: Icons.memory, label: '型号标识符',
value: _machineId ?? '', color: const Color(0xFF00897B)),
_InfoItem(icon: Icons.search, label: '映射查询结果',
value: _lookupResult ?? '', color: const Color(0xFFEF6C00)),
_InfoItem(icon: Icons.access_time, label: '获取时间',
value: _formatTime(_loadTime), color: const Color(0xFF546E7A)),
];
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10, offset: const Offset(0, 2),
)],
),
child: Column(
children: [
for (int i = 0; i < items.length; i++) ...[
_buildInfoRow(items[i]),
if (i < items.length - 1)
Divider(height: 1, indent: 56, color: Colors.grey.shade200),
],
],
),
);
}
8.2 单行信息组件
每行信息包含图标、标签、值和复制按钮:
dart
Widget _buildInfoRow(_InfoItem item) {
return InkWell(
onTap: () => _copyToClipboard(item.value, item.label),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: item.color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(item.icon, size: 20, color: item.color),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.label, style: TextStyle(fontSize: 12, color: Colors.grey.shade500)),
const SizedBox(height: 2),
Text(item.value, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500)),
],
),
),
Icon(Icons.copy, size: 16, color: Colors.grey.shade400),
],
),
),
);
}
8.3 一键复制实现
dart
void _copyToClipboard(String text, String label) {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已复制$label'),
duration: const Duration(seconds: 1),
behavior: SnackBarBehavior.floating,
),
);
}
复制功能使用 Flutter 内置的 Clipboard API,配合 SnackBar 提供即时反馈。SnackBarBehavior.floating 让提示条悬浮在底部,不会遮挡页面内容。
在客服场景中,用户可以点击任意一行快速复制型号或产品名称,粘贴到聊天窗口发送给客服人员。
提示:
Clipboard来自package:flutter/services.dart,是 Flutter 提供的跨平台剪贴板 API,在 OpenHarmony 上同样可用。详见 Flutter Services 文档。
九、分享设备信息
9.1 格式化分享文本
dart
void _shareDeviceInfo() {
if (_machineId == null || _productName == null) return;
final text = '📱 设备信息\n'
'━━━━━━━━━━━━━━━\n'
'产品名称: $_productName\n'
'型号标识: $_machineId\n'
'映射结果: $_lookupResult\n'
'获取时间: ${_loadTime?.toIso8601String() ?? "N/A"}\n'
'━━━━━━━━━━━━━━━\n'
'来自 apple_product_name 插件';
_copyToClipboard(text, '设备信息');
}
9.2 分享按钮位置
分享按钮放在 SliverAppBar 的 actions 中,只在有数据时显示:
dart
actions: [
if (_machineId != null)
IconButton(
icon: const Icon(Icons.share),
onPressed: _shareDeviceInfo,
tooltip: '分享设备信息',
),
],
9.3 分享文本格式设计
分享文本使用了结构化格式,方便接收方阅读:
| 行 | 内容 | 用途 |
|---|---|---|
| 1 | 📱 设备信息 | 标题,带 emoji 增加辨识度 |
| 2 | ━━━━━━━━━ | 分隔线 |
| 3 | 产品名称: xxx | 用户可读的设备名称 |
| 4 | 型号标识: xxx | 技术人员需要的型号编码 |
| 5 | 映射结果: xxx | 映射表查询结果 |
| 6 | 获取时间: xxx | ISO 8601 时间戳 |
| 7 | ━━━━━━━━━ | 分隔线 |
| 8 | 来自 apple_product_name | 来源标注 |
这种格式在微信、钉钉等聊天工具中粘贴后排版整齐,客服人员可以快速提取所需信息。
十、交互式型号查询
10.1 查询区域布局
页面底部提供了一个交互式查询区域,用户可以输入任意型号标识符查询对应的产品名称:
dart
Widget _buildLookupSection() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10, offset: const Offset(0, 2),
)],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(children: [
Icon(Icons.manage_search, color: Color(0xFF3949AB), size: 22),
SizedBox(width: 8),
Text('型号查询', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
]),
const SizedBox(height: 6),
Text('输入型号标识符查询对应的产品名称',
style: TextStyle(fontSize: 13, color: Colors.grey.shade500)),
const SizedBox(height: 14),
// 快捷标签
_buildPresetChips(),
const SizedBox(height: 14),
// 输入框 + 查询按钮
_buildSearchBar(),
// 查询结果
if (_queryResult != null) _buildQueryResult(),
],
),
);
}
10.2 快捷标签
预设了 6 个常见型号作为快捷查询入口:
dart
static const _presetModels = [
('CFR-AN00', 'Mate 70'),
('ALN-AL00', 'Mate 60 Pro'),
('HBK-AL00', 'Pura 70 Ultra'),
('FOA-AL00', 'nova 13'),
('GROK-W09', 'MatePad Pro'),
('PGT-AN00', 'Magic6 Pro'),
];
使用 Wrap + ActionChip 实现自动换行的标签列表:
dart
Wrap(
spacing: 8,
runSpacing: 8,
children: _presetModels.map((m) => ActionChip(
label: Text(m.$1, style: const TextStyle(fontSize: 12)),
avatar: Text(m.$2, style: TextStyle(fontSize: 9, color: Colors.grey.shade600)),
onPressed: () {
_lookupController.text = m.$1;
_doLookup(m.$1);
},
backgroundColor: const Color(0xFFF0F0FF),
side: BorderSide(color: Colors.indigo.shade100),
)).toList(),
)
点击标签会自动填充输入框并触发查询,省去手动输入的步骤。
10.3 查询逻辑
dart
Future<void> _doLookup(String modelId) async {
if (modelId.trim().isEmpty || _isQuerying) return;
setState(() => _isQuerying = true);
try {
final result = await OhosProductName().lookup(modelId.trim());
setState(() {
_queryResult = result;
_isQuerying = false;
});
} catch (e) {
setState(() {
_queryResult = '查询失败: $e';
_isQuerying = false;
});
}
}
lookup() 方法在原生层执行 HUAWEI_DEVICE_MAP[machineId] 键值查找,时间复杂度 O(1),响应几乎是即时的。如果映射表中没有收录该型号,返回原始输入字符串。
提示:
lookup()的底层实现是HUAWEI_DEVICE_MAP[machineId],如果查找不到会返回null,Dart 端的OhosProductName.lookup()会将null转换为原始machineId返回。详见 apple_product_name 源码。
十一、错误处理与重试
11.1 错误视图
加载失败时显示错误卡片:
dart
Widget _buildErrorCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red.shade700, size: 28),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('获取设备信息失败',
style: TextStyle(fontWeight: FontWeight.w600, color: Colors.red.shade800)),
const SizedBox(height: 4),
Text(_error!, style: TextStyle(fontSize: 12, color: Colors.red.shade600)),
],
),
),
],
),
);
}
11.2 错误来源分析
设备信息获取可能失败的原因:
| 错误类型 | 原因 | 原生层错误码 |
|---|---|---|
MissingPluginException |
插件未注册 | 无(框架层错误) |
PlatformException |
原生层异常 | GET_MACHINE_ID_ERROR 等 |
TimeoutException |
通信超时 | 无 |
11.3 重试机制
错误发生后,用户可以点击"获取设备信息"按钮重试。重试时会清除之前的错误状态:
dart
setState(() {
_isLoading = true;
_error = null; // 清除错误,隐藏错误卡片
});
十二、原生层数据获取流程
12.1 getMachineId 实现
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,这是 OpenHarmony 系统 API 提供的设备型号标识符。
12.2 getProductName 实现
typescript
private getProductName(result: MethodResult): void {
try {
const model = deviceInfo.productModel;
let productName = HUAWEI_DEVICE_MAP[model];
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映射表中查找 - 映射表没有则使用系统
marketName marketName也为空则返回原始productModel
12.3 lookup 实现
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);
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
result.error("LOOKUP_ERROR", errorMsg, null);
}
}
lookup 只查映射表,不访问系统 API,因此可以用于查询任意型号,不限于当前设备。
注意:
lookup返回null表示映射表中未收录该型号,Dart 端会将null转换为原始machineId。如果需要区分"已收录"和"未收录",可以使用lookupOrNull()方法。
十三、Dart 端 API 封装
13.1 OhosProductName 源码
dart
class OhosProductName {
static const MethodChannel _channel = MethodChannel('apple_product_name');
static final _instance = OhosProductName._();
OhosProductName._();
factory OhosProductName() => _instance;
Future<String> getMachineId() async {
final String? machineId = await _channel.invokeMethod('getMachineId');
return machineId ?? 'Unknown';
}
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;
}
}
13.2 设计要点
| 设计 | 实现 | 好处 |
|---|---|---|
| 单例模式 | factory OhosProductName() => _instance |
复用 MethodChannel 实例 |
| 空值处理 | machineId ?? 'Unknown' |
防止 null 传递到 UI 层 |
| 参数传递 | {'machineId': machineId} |
Map 格式,原生层通过 call.argument() 读取 |
提示:
OhosProductName使用工厂构造函数 实现单例,确保整个应用只有一个MethodChannel实例。多次OhosProductName()调用返回的是同一个对象。详见 Dart 工厂构造函数文档。
十四、完整通信链路
14.1 从按钮点击到 UI 更新
用户点击"获取设备信息"按钮
→ _loadDeviceInfo()
→ setState(_isLoading = true) // UI 显示加载状态
→ Future.wait([getMachineId(), getProductName()])
→ _channel.invokeMethod('getMachineId')
→ MethodChannel 编码消息
→ BinaryMessenger 跨平台传递
→ AppleProductNamePlugin.onMethodCall(call, result)
→ case "getMachineId": deviceInfo.productModel
→ result.success("ALN-AL00")
→ Future<String> 完成
→ lookup("ALN-AL00")
→ _channel.invokeMethod('lookup', {'machineId': 'ALN-AL00'})
→ HUAWEI_DEVICE_MAP["ALN-AL00"]
→ result.success("HUAWEI Mate 60 Pro")
→ setState(更新所有状态变量) // UI 显示设备信息
14.2 涉及的文件
| 层级 | 文件 | 职责 |
|---|---|---|
| UI 层 | example/lib/main.dart |
页面布局、状态管理、用户交互 |
| API 层 | lib/apple_product_name_ohos.dart |
Dart 端 API 封装、MethodChannel 定义 |
| 通信层 | Flutter 框架 | BinaryMessenger 消息编解码 |
| 插件层 | AppleProductNamePlugin.ets |
原生方法路由、业务逻辑 |
| 系统层 | @kit.BasicServicesKit |
deviceInfo 系统 API |
14.3 数据转换过程
deviceInfo.productModel (string)
→ result.success(string)
→ BinaryMessenger 编码为二进制
→ 跨平台传递
→ BinaryMessenger 解码为 Dart String
→ Future<String?> 完成
→ ?? 'Unknown' 空值处理
→ setState 更新 UI
十五、UI 细节与用户体验
15.1 加载状态反馈
按钮在加载过程中显示 CircularProgressIndicator,同时禁用点击:
dart
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _loadDeviceInfo,
icon: _isLoading
? const SizedBox(width: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
: Icon(_machineId != null ? Icons.refresh : Icons.phone_android),
label: Text(_isLoading ? '获取中...'
: (_machineId != null ? '刷新设备信息' : '获取设备信息')),
),
)
按钮文案随状态变化:
| 状态 | 图标 | 文案 |
|---|---|---|
| 首次进入 | 手机图标 | "获取设备信息" |
| 加载中 | 旋转指示器 | "获取中..." |
| 已有数据 | 刷新图标 | "刷新设备信息" |
15.2 颜色体系
页面使用了统一的 Indigo 色系作为主色调:
- AppBar 渐变:
#3949AB→#1A237E - 按钮背景:
#3949AB - 图标强调色:
#3949AB - 快捷标签背景:
#F0F0FF
设备卡片的颜色则根据系列动态变化,与主色调形成层次对比。
15.3 资源释放
dart
@override
void dispose() {
_lookupController.dispose();
super.dispose();
}
TextEditingController 在页面销毁时必须调用 dispose() 释放资源,否则会导致内存泄漏。这是 Flutter 开发中的基本规范。
提示:如果忘记
dispose,Flutter 的 debug 模式会在控制台输出警告信息。建议养成在dispose中释放所有 Controller 的习惯。
十六、应用场景扩展
16.1 "关于手机"页面
将设备卡片集成到应用的设置页面中:
dart
ListTile(
leading: const Icon(Icons.phone_android),
title: const Text('关于手机'),
subtitle: FutureBuilder<String>(
future: OhosProductName().getProductName(),
builder: (_, snap) => Text(snap.data ?? '加载中...'),
),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => const Article23DemoPage())),
)
16.2 客服工单自动填充
在用户提交工单时自动附带设备信息:
dart
Future<Map<String, String>> getDeviceContext() async {
final ohos = OhosProductName();
return {
'device_model': await ohos.getMachineId(),
'device_name': await ohos.getProductName(),
'timestamp': DateTime.now().toIso8601String(),
};
}
16.3 调试信息面板
在开发阶段,可以将设备信息展示为悬浮调试面板:
dart
// 在 MaterialApp 的 builder 中添加
builder: (context, child) {
return Banner(
message: 'DEBUG',
location: BannerLocation.topEnd,
child: child!,
);
}
这些扩展场景都基于 apple_product_name 提供的三个核心 API,只是 UI 呈现方式不同。
十七、性能考量
17.1 API 调用耗时
| API | 原生层操作 | 预估耗时 |
|---|---|---|
getMachineId() |
读取系统属性 | < 5ms |
getProductName() |
系统属性 + 映射表查找 | < 5ms |
lookup(id) |
映射表查找 | < 1ms |
| MethodChannel 通信开销 | 消息编解码 + 跨平台传递 | ~10ms |
总耗时约 15-20ms,用户几乎感知不到延迟。
17.2 优化建议
如果页面需要频繁刷新设备信息,可以考虑以下优化:
- 缓存结果:设备信息在运行期间不会变化,首次获取后缓存即可
- 减少调用次数 :
getProductName()内部已经包含了getMachineId()的逻辑,如果只需要产品名称,调用一次即可 - 避免在 build 中调用 :异步操作应在
initState或事件回调中触发,不要在build方法中调用
提示:
OhosProductName是单例模式,多次调用OhosProductName()不会创建新的MethodChannel实例,因此不存在通道重复创建的开销。
十八、与 example 原始代码的对比
18.1 原始 example 实现
项目自带的 example 使用了最简单的实现方式:
dart
// 原始 example --- 单一信息展示
FutureBuilder<String>(
future: _loadProductName(),
builder: (context, snapshot) {
final productName = snapshot.data ?? 'Loading...';
return Center(child: Text(productName));
},
)
18.2 本文实现的增强
| 维度 | 原始 example | 本文实现 |
|---|---|---|
| 信息量 | 仅产品名称 | 产品名称 + 型号 + 映射结果 + 时间 |
| 交互 | 无 | 复制、分享、查询 |
| 错误处理 | 返回 "Unknown" | 错误卡片 + 重试按钮 |
| 视觉设计 | 居中文字 | 渐变 AppBar + 设备卡片 + 系列标签 |
| 状态管理 | FutureBuilder | 手动 setState |
| 扩展性 | 低 | 高(可集成到任意页面) |
18.3 选择建议
- 快速原型 :使用原始 example 的
FutureBuilder方式,代码量最少 - 正式产品:使用本文的手动状态管理方式,控制力更强
- 复杂场景 :考虑引入状态管理框架(如 Riverpod 或 Bloc)
十九、常见问题排查
19.1 页面一直显示加载状态
排查步骤:
- 检查插件是否正确注册(
GeneratedPluginRegistrant.ets) - 检查通道名称是否一致(
"apple_product_name") - 在 DevEco Studio 的 Log 窗口中过滤
AppleProductNamePlugin查看日志 - 确认
onMethodCall中每个分支都调用了result.success()或result.error()
19.2 产品名称显示为 "Unknown"
可能原因:
- 当前设备型号未收录在
HUAWEI_DEVICE_MAP映射表中 - 系统
marketName为空 - 原生层
getProductName方法返回了null
解决方案:
- 使用
getMachineId()获取当前设备的型号标识符 - 检查该型号是否在映射表中
- 如果未收录,可以提交 PR 添加新型号
19.3 查询结果与产品名称不一致
getProductName() 使用三级回退 (映射表 → marketName → productModel),而 lookup() 只查映射表。如果当前设备不在映射表中,两者的返回值可能不同:
getProductName() → "HUAWEI xxx"(来自 marketName)
lookup(machineId) → "ALN-AL00"(映射表未收录,返回原始 ID)
提示:这种不一致是正常的,说明当前设备型号尚未被映射表收录。可以通过向 仓库 提交 PR 来添加新型号。
二十、完整演示代码
本文的完整演示代码位于 example/lib/main.dart,包含以下组件:
| 组件 | 职责 | 行数 |
|---|---|---|
Article23DemoPage |
页面入口,StatefulWidget | ~10 行 |
_Article23DemoPageState |
状态管理、数据加载、UI 构建 | ~250 行 |
_DeviceSeries |
设备系列数据类 | ~5 行 |
_InfoItem |
信息行数据类 | ~8 行 |
页面特色:
- 渐变
SliverAppBar替代普通 AppBar,视觉层次更丰富 - 设备卡片根据系列动态配色,每种设备都有独特的视觉风格
- 快捷标签让型号查询零输入即可体验
- 所有信息行可点击复制,贴近真实产品交互
完整代码请参考本文第十六章的演示代码部分,或直接查看 源码仓库 的 example/lib/main.dart 文件。
总结
本文从零构建了一个应用级别的设备信息展示页面,涵盖了 apple_product_name 库的三个核心 API(getMachineId、getProductName、lookup)的完整使用场景。页面采用渐变 AppBar + 动态配色设备卡片 + 交互式查询的设计,贴近真实产品的 UI 标准,可以直接集成到"关于手机"、客服支持等业务场景中。
下一篇文章将介绍用户反馈系统集成设备信息。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- OpenHarmony适配仓库:apple_product_name
- 开源鸿蒙跨平台社区:openharmonycrossplatform.csdn.net
- Flutter Cookbook 浮动 AppBar:flutter.dev
- Flutter Clipboard API:api.flutter.dev
- Dart 工厂构造函数:dart.dev
- Flutter 插件开发指南:flutter.dev
- Riverpod 状态管理:riverpod.dev
- Bloc 状态管理:bloclibrary.dev
- OpenHarmony Flutter 引擎:gitee.com