Flutter三方库适配OpenHarmony【apple_product_name】构建设备信息展示页面

前言

欢迎加入开源鸿蒙跨平台社区: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,但本页面选择手动管理状态,原因如下:

  1. 页面有多个独立的异步操作(加载设备信息 + 型号查询)
  2. 需要支持重试刷新功能
  3. 需要在加载完成后保持数据,而非每次 setState 都重新加载
  4. FutureBuildersetState 时会重新触发 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 加载流程分析

加载过程分为三步:

  1. 并行获取Future.wait 同时调用 getMachineId()getProductName()

  2. 串行查询 :拿到 machineId 后调用 lookup(id) 验证映射结果

  3. 更新状态 :一次性 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 让标题在滚动时始终可见
  • 渐变背景在折叠过程中自然过渡

提示:SliverAppBarexpandedHeight: 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 分享按钮位置

分享按钮放在 SliverAppBaractions 中,只在有数据时显示:

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);
  }
}

三级回退策略

  1. 先在 HUAWEI_DEVICE_MAP 映射表中查找
  2. 映射表没有则使用系统 marketName
  3. 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 优化建议

如果页面需要频繁刷新设备信息,可以考虑以下优化:

  1. 缓存结果:设备信息在运行期间不会变化,首次获取后缓存即可
  2. 减少调用次数getProductName() 内部已经包含了 getMachineId() 的逻辑,如果只需要产品名称,调用一次即可
  3. 避免在 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 方式,代码量最少
  • 正式产品:使用本文的手动状态管理方式,控制力更强
  • 复杂场景 :考虑引入状态管理框架(如 RiverpodBloc

十九、常见问题排查

19.1 页面一直显示加载状态

排查步骤:

  1. 检查插件是否正确注册(GeneratedPluginRegistrant.ets
  2. 检查通道名称是否一致("apple_product_name"
  3. 在 DevEco Studio 的 Log 窗口中过滤 AppleProductNamePlugin 查看日志
  4. 确认 onMethodCall 中每个分支都调用了 result.success()result.error()

19.2 产品名称显示为 "Unknown"

可能原因:

  • 当前设备型号未收录在 HUAWEI_DEVICE_MAP 映射表中
  • 系统 marketName 为空
  • 原生层 getProductName 方法返回了 null

解决方案:

  1. 使用 getMachineId() 获取当前设备的型号标识符
  2. 检查该型号是否在映射表中
  3. 如果未收录,可以提交 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(getMachineIdgetProductNamelookup)的完整使用场景。页面采用渐变 AppBar + 动态配色设备卡片 + 交互式查询的设计,贴近真实产品的 UI 标准,可以直接集成到"关于手机"、客服支持等业务场景中。

下一篇文章将介绍用户反馈系统集成设备信息。

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


相关资源:

相关推荐
lili-felicity2 小时前
基础入门 Flutter for OpenHarmony:第三方库实战 cryptography_flutter 加密解密详解
flutter
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_web_auth】— 深度链接(Deep Link)机制全解析
flutter
松叶似针2 小时前
Flutter三方库适配OpenHarmony【doc_text】— FlutterPlugin 接口实现与 MethodChannel 注册
flutter·harmonyos
lqj_本人2 小时前
Flutter三方库适配OpenHarmony【apple_product_name】插件架构设计解析
flutter
wangyang62752 小时前
Xcode 26 真机运行崩溃 EXC_BAD_ACCESS map_images_nolock 完美解决方案
flutter·ios
2601_949593652 小时前
Flutter for Harmony 跨平台开发实战:超形状与超椭圆——参数方程的形态边界
flutter
Swift社区3 小时前
Flutter 中如何优雅地处理复杂表单
前端·flutter·前端框架
不爱吃糖的程序媛3 小时前
Flutter-OH 三方库 devicelocale 鸿蒙适配
flutter·华为·harmonyos
2501_9219308311 小时前
进阶实战 Flutter for OpenHarmony:Isolate 多线程计算系统 - 并发任务处理实现
flutter