进阶实战 Flutter for OpenHarmony:mobile_device_identifier 第三方库实战 - 设备标识

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


🎯 一、组件概述与应用场景

📋 1.1 mobile_device_identifier 简介

在移动应用开发中,获取设备唯一标识符是一个非常常见的需求。无论是用户身份识别、设备绑定、数据统计还是安全验证,都需要一个稳定可靠的设备标识符。在 Flutter for OpenHarmony 应用开发中,mobile_device_identifier 插件正是为此而生的解决方案。

核心特性:

特性 说明
🔑 唯一性 生成的设备 ID 在全球范围内唯一
💾 持久性 应用卸载重装后 ID 保持不变
🌐 跨平台支持 支持 Android、iOS、OpenHarmony 等多个平台
🔓 无需权限 在大多数平台上不需要特殊权限即可获取
⚡ 简单易用 一行代码即可获取设备 ID
🛡️ 安全可靠 使用系统级 API 生成,保证安全性

💡 1.2 实际应用场景

用户身份识别:在某些应用场景中,需要识别用户所使用的设备。例如,免费试用应用可能限制每个设备只能试用一次。

设备绑定:对于安全敏感的应用,如银行应用、企业应用等,可能需要将用户账号与特定设备绑定。

数据统计与分析:在应用数据分析中,设备 ID 用于统计活跃用户数、留存率等指标。

推送通知:推送通知服务需要设备标识来定位目标设备。

防作弊与安全:在游戏、投票、抽奖等场景中,需要防止用户通过多次安装应用来作弊。

个性化设置同步:某些应用允许用户在设置中保存偏好设置,并在重新安装后恢复。

🏗️ 1.3 系统架构设计

复制代码
┌─────────────────────────────────────────────────────────┐
│                    UI 展示层                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │ 设备信息展示 │  │ 设备绑定管理 │  │ 设备统计面板 │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    业务逻辑层                            │
│  ┌─────────────────────────────────────────────────┐   │
│  │         DeviceIdentifierService 设备服务        │   │
│  │  • getDeviceId()  • bindDevice()  • unbind()   │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    平台适配层                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │ Android ID  │  │ IDFV (iOS)  │  │ OAID (OH)   │     │
│  └─────────────┘  └─────────────┘  └─────────────┘     │
└─────────────────────────────────────────────────────────┘

📦 二、项目配置与依赖安装

🔧 2.1 添加依赖配置

打开项目根目录下的 pubspec.yaml 文件,添加以下配置:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter

  # mobile_device_identifier - 设备唯一标识插件
  mobile_device_identifier:
    git:
      url: "https://gitcode.com/openharmony-sig/fluttertpc_mobile_device_identifier.git"

dev_dependencies:
  # mobile_device_identifier 鸿蒙平台支持
  mobile_device_identifier_ohos:
    git:
      url: "https://gitcode.com/openharmony-sig/fluttertpc_mobile_device_identifier.git"
      path: ./ohos

配置说明:

  • 使用 git 方式引用开源鸿蒙适配的仓库
  • mobile_device_identifier_ohos:鸿蒙平台的原生实现
  • 本项目基于 mobile_device_identifier@0.0.2 开发
  • 适配 Flutter 3.27.5-ohos-1.0.4

⚠️ 重要提示:对于 OpenHarmony 平台,必须使用 git 方式引用适配版本,不能直接使用 pub.dev 的版本号。

📥 2.2 下载依赖

配置完成后,在项目根目录执行以下命令:

bash 复制代码
flutter pub get

🔐 2.3 权限配置

在 OpenHarmony 平台上,获取设备 ID 需要配置相关权限。

ohos/entry/src/main/module.json5:

json 复制代码
{
  "module": {
    "requestPermissions": [
      {"name": "ohos.permission.INTERNET"},
      {
        "name": "ohos.permission.APP_TRACKING_CONSENT",
        "reason": "$string:reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

ohos/entry/src/main/resources/base/element/string.json:

json 复制代码
{
  "string": [
    {
      "name": "reason",
      "value": "应用信息读取"
    }
  ]
}

⚠️ 重要提示ohos.permission.APP_TRACKING_CONSENT 权限属于 system_basic 级别。需要参考官方文档修改应用等级。

📱 2.4 平台差异说明

平台 主要标识符 特点
Android Android ID / IMEI Android ID 在恢复出厂后重置
iOS identifierForVendor 同一开发商的应用 ID 相同
OpenHarmony OAID 用户可重置,匿名性强

🔧 三、核心功能详解

🎯 3.1 获取设备 ID

dart 复制代码
import 'package:flutter/services.dart';
import 'package:mobile_device_identifier_ohos/mobile_device_identifier_ohos.dart';

final _mobileDeviceIdentifierPlugin = MobileDeviceIdentifier();

Future<String?> getDeviceId() async {
  try {
    String? deviceId = await _mobileDeviceIdentifierPlugin.getDeviceId();
    return deviceId;
  } on PlatformException catch (e) {
    debugPrint('获取设备 ID 失败: ${e.message}');
    return null;
  }
}

🔐 3.2 设备绑定功能

dart 复制代码
class DeviceBindingService {
  static const String _keyDeviceId = 'bound_device_id';
  
  final SharedPreferences _prefs;
  
  DeviceBindingService(this._prefs);
  
  Future<bool> isCurrentDeviceBound() async {
    final boundId = _prefs.getString(_keyDeviceId);
    if (boundId == null) return false;
    
    final currentId = await MobileDeviceIdentifier().getDeviceId();
    return boundId == currentId;
  }
  
  Future<bool> bindCurrentDevice() async {
    final currentId = await MobileDeviceIdentifier().getDeviceId();
    if (currentId == null) return false;
    
    await _prefs.setString(_keyDeviceId, currentId);
    return true;
  }
  
  Future<void> unbindDevice() async {
    await _prefs.remove(_keyDeviceId);
  }
}

📊 3.3 设备数量限制

dart 复制代码
class DeviceLimitService {
  final int maxDevices;
  
  DeviceLimitService({this.maxDevices = 3});
  
  Future<bool> canAddNewDevice(String userId) async {
    final devices = await _getRegisteredDevices(userId);
    return devices.length < maxDevices;
  }
  
  Future<bool> registerCurrentDevice(String userId) async {
    final deviceId = await MobileDeviceIdentifier().getDeviceId();
    if (deviceId == null) return false;
    
    final devices = await _getRegisteredDevices(userId);
    
    if (devices.contains(deviceId)) return true;
    
    if (devices.length >= maxDevices) return false;
    
    devices.add(deviceId);
    await _saveRegisteredDevices(userId, devices);
    return true;
  }
  
  Future<List<String>> _getRegisteredDevices(String userId) async {
    return [];
  }
  
  Future<void> _saveRegisteredDevices(String userId, List<String> devices) async {}
}

📝 四、完整示例代码

下面是一个完整的智能设备标识管理系统示例:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobile_device_identifier_ohos/mobile_device_identifier_ohos.dart';

void main() {
  runApp(const DeviceIdentifierApp());
}

class DeviceIdentifierApp extends StatelessWidget {
  const DeviceIdentifierApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '智能设备标识管理系统',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const MainPage(),
    );
  }
}

class MainPage extends StatefulWidget {
  const MainPage({super.key});

  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = [
    const DeviceInfoPage(),
    const DeviceBindingPage(),
    const DeviceStatsPage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages[_currentIndex],
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (index) {
          setState(() => _currentIndex = index);
        },
        destinations: const [
          NavigationDestination(icon: Icon(Icons.devices), label: '设备信息'),
          NavigationDestination(icon: Icon(Icons.link), label: '设备绑定'),
          NavigationDestination(icon: Icon(Icons.analytics), label: '设备统计'),
        ],
      ),
    );
  }
}

// ============ 设备信息页面 ============

class DeviceInfoPage extends StatefulWidget {
  const DeviceInfoPage({super.key});

  @override
  State<DeviceInfoPage> createState() => _DeviceInfoPageState();
}

class _DeviceInfoPageState extends State<DeviceInfoPage> {
  String? _deviceId;
  bool _isLoading = true;
  String? _error;

  @override
  void initState() {
    super.initState();
    _loadDeviceId();
  }

  Future<void> _loadDeviceId() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final plugin = MobileDeviceIdentifier();
      final id = await plugin.getDeviceId();
      setState(() {
        _deviceId = id;
        _isLoading = false;
      });
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  Future<void> _copyToClipboard() async {
    if (_deviceId == null) return;

    await Clipboard.setData(ClipboardData(text: _deviceId!));
    
    if (mounted) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: const Text('设备 ID 已复制到剪贴板'),
          behavior: SnackBarBehavior.floating,
          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('设备信息'),
        centerTitle: true,
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _loadDeviceId,
          ),
        ],
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _buildContent(),
    );
  }

  Widget _buildContent() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          _buildDeviceCard(),
          const SizedBox(height: 24),
          _buildInfoSection(),
          const SizedBox(height: 24),
          _buildActionsSection(),
        ],
      ),
    );
  }

  Widget _buildDeviceCard() {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.indigo.shade400, Colors.indigo.shade600],
        ),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        children: [
          const Icon(Icons.devices, size: 48, color: Colors.white),
          const SizedBox(height: 16),
          const Text(
            '设备唯一标识',
            style: TextStyle(color: Colors.white70, fontSize: 14),
          ),
          const SizedBox(height: 8),
          SelectableText(
            _deviceId ?? '获取失败',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 16,
              fontWeight: FontWeight.bold,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),
          ElevatedButton.icon(
            onPressed: _deviceId != null ? _copyToClipboard : null,
            icon: const Icon(Icons.copy, size: 18),
            label: const Text('复制 ID'),
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.white,
              foregroundColor: Colors.indigo,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildInfoSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '关于设备标识',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            _buildInfoItem('唯一性', '每个设备拥有全球唯一的标识符'),
            _buildInfoItem('持久性', '应用卸载重装后标识符保持不变'),
            _buildInfoItem('安全性', '标识符不包含个人隐私信息'),
            _buildInfoItem('用途', '用于设备识别、数据同步等功能'),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoItem(String title, String description) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: 6,
            height: 6,
            margin: const EdgeInsets.only(top: 6, right: 12),
            decoration: BoxDecoration(
              color: Colors.indigo.shade400,
              shape: BoxShape.circle,
            ),
          ),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
                Text(
                  description,
                  style: TextStyle(color: Colors.grey.shade600, fontSize: 13),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildActionsSection() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '快捷操作',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            ListTile(
              leading: const Icon(Icons.copy),
              title: const Text('复制设备 ID'),
              subtitle: const Text('将设备 ID 复制到剪贴板'),
              trailing: const Icon(Icons.chevron_right),
              onTap: _copyToClipboard,
            ),
            ListTile(
              leading: const Icon(Icons.share),
              title: const Text('分享设备信息'),
              subtitle: const Text('通过其他应用分享设备 ID'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('分享功能开发中')),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

// ============ 设备绑定页面 ============

class DeviceBindingPage extends StatefulWidget {
  const DeviceBindingPage({super.key});

  @override
  State<DeviceBindingPage> createState() => _DeviceBindingPageState();
}

class _DeviceBindingPageState extends State<DeviceBindingPage> {
  String? _deviceId;
  bool _isBound = false;
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    final plugin = MobileDeviceIdentifier();
    final id = await plugin.getDeviceId();
    setState(() {
      _deviceId = id;
      _isLoading = false;
    });
  }

  void _toggleBinding() {
    setState(() => _isBound = !_isBound);
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(_isBound ? '设备已绑定' : '设备已解绑'),
        backgroundColor: _isBound ? Colors.green : Colors.orange,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('设备绑定'),
        centerTitle: true,
      ),
      body: _isLoading
          ? const Center(child: CircularProgressIndicator())
          : _buildContent(),
    );
  }

  Widget _buildContent() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(
        children: [
          _buildBindingStatusCard(),
          const SizedBox(height: 24),
          _buildBindingInfoCard(),
        ],
      ),
    );
  }

  Widget _buildBindingStatusCard() {
    return Container(
      padding: const EdgeInsets.all(24),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: _isBound
              ? [Colors.green.shade400, Colors.green.shade600]
              : [Colors.grey.shade400, Colors.grey.shade600],
        ),
        borderRadius: BorderRadius.circular(16),
      ),
      child: Column(
        children: [
          Icon(
            _isBound ? Icons.link : Icons.link_off,
            size: 48,
            color: Colors.white,
          ),
          const SizedBox(height: 16),
          Text(
            _isBound ? '设备已绑定' : '设备未绑定',
            style: const TextStyle(
              color: Colors.white,
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          const SizedBox(height: 8),
          Text(
            _deviceId ?? '',
            style: const TextStyle(color: Colors.white70, fontSize: 12),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: _toggleBinding,
            style: ElevatedButton.styleFrom(
              backgroundColor: Colors.white,
              foregroundColor: _isBound ? Colors.green : Colors.grey,
            ),
            child: Text(_isBound ? '解除绑定' : '绑定设备'),
          ),
        ],
      ),
    );
  }

  Widget _buildBindingInfoCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '设备绑定说明',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            _buildInfoItem('安全保护', '绑定后,您的账号只能在此设备上使用'),
            _buildInfoItem('数据安全', '设备绑定可防止他人盗用您的账号'),
            _buildInfoItem('灵活管理', '您可以随时解除绑定并绑定新设备'),
          ],
        ),
      ),
    );
  }

  Widget _buildInfoItem(String title, String description) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        children: [
          Icon(Icons.check_circle, color: Colors.green.shade400, size: 20),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(title, style: const TextStyle(fontWeight: FontWeight.w500)),
                Text(
                  description,
                  style: TextStyle(color: Colors.grey.shade600, fontSize: 13),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ============ 设备统计页面 ============

class DeviceStatsPage extends StatelessWidget {
  const DeviceStatsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('设备统计'),
        centerTitle: true,
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            _buildStatsCard(
              '总设备数',
              '1,234',
              Icons.devices,
              Colors.blue,
            ),
            const SizedBox(height: 16),
            _buildStatsCard(
              '活跃设备',
              '856',
              Icons.device_hub,
              Colors.green,
            ),
            const SizedBox(height: 16),
            _buildStatsCard(
              '新增设备(今日)',
              '42',
              Icons.add_circle,
              Colors.orange,
            ),
            const SizedBox(height: 24),
            _buildRecentDevicesCard(),
          ],
        ),
      ),
    );
  }

  Widget _buildStatsCard(String title, String value, IconData icon, Color color) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Row(
          children: [
            Container(
              width: 56,
              height: 56,
              decoration: BoxDecoration(
                color: color.withOpacity(0.1),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Icon(icon, color: color, size: 28),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    title,
                    style: TextStyle(color: Colors.grey.shade600, fontSize: 14),
                  ),
                  const SizedBox(height: 4),
                  Text(
                    value,
                    style: const TextStyle(
                      fontSize: 28,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildRecentDevicesCard() {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text(
              '最近注册设备',
              style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            ListView.separated(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              itemCount: 5,
              separatorBuilder: (_, __) => const Divider(),
              itemBuilder: (context, index) {
                return ListTile(
                  contentPadding: EdgeInsets.zero,
                  leading: CircleAvatar(
                    backgroundColor: Colors.indigo.withOpacity(0.1),
                    child: const Icon(Icons.devices, color: Colors.indigo),
                  ),
                  title: Text('设备 ${index + 1}'),
                  subtitle: Text('ID: XXXXXXXX${index + 1}'),
                  trailing: Text(
                    '${index + 1} 小时前',
                    style: TextStyle(color: Colors.grey.shade500, fontSize: 12),
                  ),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}

🏆 五、最佳实践与注意事项

⚠️ 5.1 隐私合规

用户知情:在获取和使用设备 ID 时,应告知用户并获得同意。

最小化使用:只在必要的功能中使用设备 ID,避免过度收集。

数据保护:妥善存储设备 ID,防止泄露。

🔐 5.2 安全性考虑

不可预测:设备 ID 不应包含可预测的模式。

不可伪造:使用系统级 API 生成,确保真实性。

定期更新:关注平台政策变化,及时调整实现方式。

📱 5.3 常见问题处理

获取失败:添加错误处理,提供降级方案。

ID 为空:检查权限配置,确保平台支持。

ID 变化:某些情况下 ID 可能变化,需要处理这种情况。


📌 六、总结

本文通过一个完整的智能设备标识管理系统案例,深入讲解了 mobile_device_identifier 插件的使用方法与最佳实践:

基础获取:掌握获取设备 ID 的基本方法。

设备绑定:实现设备绑定功能,增强账号安全。

设备统计:构建设备统计面板,了解用户设备情况。

隐私合规:遵循隐私保护最佳实践。

掌握这些技巧,你就能构建出专业级的设备标识管理功能,满足各种业务场景需求。


参考资料

相关推荐
松叶似针2 小时前
Flutter三方库适配OpenHarmony【secure_application】— 与 HarmonyOS 安全能力的深度集成
安全·flutter·harmonyos
lili-felicity4 小时前
进阶实战 Flutter for OpenHarmony:qr_flutter 第三方库实战 - 智能二维码生成系统
flutter
松叶似针4 小时前
Flutter三方库适配OpenHarmony【secure_application】— 自定义锁屏界面与品牌化设计
flutter
松叶似针4 小时前
Flutter三方库适配OpenHarmony【secure_application】— 敏感数据清除与安全增强
flutter
lili-felicity4 小时前
进阶实战 Flutter for OpenHarmony:image_cropper 第三方库实战 - 图片裁剪
flutter
lili-felicity4 小时前
进阶实战 Flutter for OpenHarmony:battery_plus 第三方库实战 - 电池状态监控
flutter
lili-felicity4 小时前
进阶实战 Flutter for OpenHarmony:image_gallery_saver 第三方库实战 - 图片保存
flutter
lili-felicity5 小时前
进阶实战 Flutter for OpenHarmony:app_settings 第三方库实战 - 系统设置跳转
flutter