返回

Flutter Web 获取设备名:device_info_plus 限制与变通方法

windows

【解惑】Flutter Web 如何获取设备名称?device_info_plus 在 Web 上的局限性与替代方案

不少开发者想在 Flutter Web 应用里拿到具体的设备名称,比如想知道用户用的是 "iPhone 11" 还是 "Samsung S11 Ultra",或者是 "Asus 笔记本" 还是 "HP 电脑"。很自然地会想到 device_info_plus 这个强大的包,但问题来了:它在 Web 平台上能实现这个需求吗?

直接说答案:不能。 device_info_plus 在 Web 上无法直接获取你期望的那种精确的硬件型号名称。但这并不意味着它在 Web 上毫无用处,只是它返回的信息类型不同。

这篇博客就来聊聊为什么 Web 环境下获取精确设备名这么困难,device_info_plus 在 Web 上到底能做些什么,以及有哪些变通的方法来获取一些有用的设备相关信息。

一、为什么在 Web 上获取精确设备名这么难?

这事儿吧,主要卡在浏览器的安全和隐私设计上。想象一下,随便一个网站就能知道你正在用的是具体哪个型号的手机或电脑,这听起来是不是有点毛骨悚然?

  1. 隐私保护是第一位: 浏览器是用户访问互联网的窗口,它的核心职责之一就是保护用户的隐私。暴露过于详细的硬件信息(比如唯一的设备型号、序列号等)会大大增加用户被追踪(fingerprinting)的风险。攻击者可能利用这些信息来识别特定用户,或者针对性地发起攻击。所以,浏览器厂商有意地限制了网页脚本能获取到的硬件细节。
  2. 安全沙箱机制: Web 应用运行在一个受限的沙箱环境中,与操作系统底层硬件的直接交互被严格控制。不像原生 App 可以调用系统 API 获取更详细的设备信息,Web 应用的能力边界被浏览器牢牢限制住。
  3. 缺乏统一标准: 即便撇开隐私问题,也没有一个广泛接受的 Web 标准来强制要求设备制造商或操作系统向浏览器提供如此精细的硬件型号信息。不同厂商、不同系统对这些信息的处理方式千差万别。
  4. 关注点不同: Web 技术的核心在于跨平台兼容性。相比于知道用户是“小米 14 Pro”还是“华为 Mate 60 Pro”,浏览器更关心的是提供一致的渲染能力、支持哪些 Web API、屏幕尺寸多大、是否支持触控等与 Web 体验直接相关的信息。

简而言之,Web 环境的设计初衷就不是为了让网站轻松获取到用户设备的“身份证号”。

二、device_info_plus 在 Web 上的真实能力

虽然拿不到具体的硬件型号,device_info_plus 并没有在 Web 平台撂挑子。它确实可以在 Web 上运行,但它获取和返回的是 浏览器环境信息 ,而不是底层的硬件信息。

使用 device_info_plus (确保已添加到 pubspec.yaml):

dependencies:
  flutter:
    sdk: flutter
  device_info_plus: ^10.1.0 # 使用最新版本

获取 Web 浏览器信息的示例代码:

import 'package:flutter/material.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart' show kIsWeb; // 用于判断是否是Web平台

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

  @override
  State<DeviceInfoWebWidget> createState() => _DeviceInfoWebWidgetState();
}

class _DeviceInfoWebWidgetState extends State<DeviceInfoWebWidget> {
  Map<String, dynamic> _deviceData = <String, dynamic>{};
  final DeviceInfoPlugin _deviceInfoPlugin = DeviceInfoPlugin();

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

  Future<void> _initPlatformState() async {
    var deviceData = <String, dynamic>{};

    try {
      if (kIsWeb) { // 关键:只在Web平台执行
        print('Running on Web, getting WebBrowserInfo...');
        WebBrowserInfo webBrowserInfo = await _deviceInfoPlugin.webBrowserInfo;
        deviceData = _readWebBrowserInfo(webBrowserInfo);
        print('WebBrowserInfo fetched successfully.');
      } else {
         print('Not running on Web.');
        // 你可以在这里添加获取其他平台信息的逻辑 (Android, iOS, etc.)
        // 例如:
        // if (Platform.isAndroid) {
        //   deviceData = _readAndroidBuildData(await deviceInfoPlugin.androidInfo);
        // } else if (Platform.isIOS) {
        //   deviceData = _readIosDeviceInfo(await deviceInfoPlugin.iosInfo);
        // }
        deviceData['Error'] = 'Not a web platform';
      }
    } catch (e) {
       print('Error getting device info: $e');
       deviceData['Error'] = 'Failed to get platform version.';
    }

     if (!mounted) return;

    setState(() {
      _deviceData = deviceData;
    });
  }

  Map<String, dynamic> _readWebBrowserInfo(WebBrowserInfo data) {
     print('Parsing WebBrowserInfo...');
    return <String, dynamic>{
      'browserName': data.browserName.toString(), // 获取浏览器名称枚举值
      'appCodeName': data.appCodeName,             // 一般是 "Mozilla"
      'appName': data.appName,                 // 浏览器官方名称
      'appVersion': data.appVersion,             // 浏览器版本详细信息
      'deviceMemory': data.deviceMemory?.toString() ?? 'N/A', // 设备内存(如果可用)
      'language': data.language,                 // 浏览器语言
      'languages': data.languages?.join(', ') ?? 'N/A',   // 浏览器语言列表
      'platform': data.platform,                 // 操作系统平台 (e.g., "Win32", "MacIntel", "Linux x86_64")
      'product': data.product,                 // 产品名称 (通常是 "Gecko" 或 "WebKit")
      'productSub': data.productSub,             // 产品子版本
      'userAgent': data.userAgent,               // 用户代理字符串 (重要!)
      'vendor': data.vendor,                   // 浏览器供应商
      'vendorSub': data.vendorSub,               // 供应商子版本
      'hardwareConcurrency': data.hardwareConcurrency?.toString() ?? 'N/A', // CPU核心数 (逻辑)
      'maxTouchPoints': data.maxTouchPoints?.toString() ?? 'N/A',     // 最大触控点数
    };
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: _deviceData.isEmpty
            ? [const Center(child: CircularProgressIndicator())]
            : _deviceData.entries.map((entry) {
                return Padding(
                  padding: const EdgeInsets.symmetric(vertical: 4.0),
                  child: Text('${entry.key}: ${entry.value}'),
                );
              }).toList(),
      ),
    );
  }
}

运行结果示例 (可能因浏览器和系统而异):

在 Windows 11 上的 Chrome 浏览器中,你可能看到类似这样的信息:

browserName: BrowserName.chrome
appCodeName: Mozilla
appName: Netscape
appVersion: 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/12X.X.XXXX.XXX Safari/537.36 // (X为版本号)
deviceMemory: 8 // (可能是估计值)
language: zh-CN
languages: zh-CN, zh
platform: Win32
product: Gecko
productSub: 20030107
userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/12X.X.XXXX.XXX Safari/537.36
vendor: Google Inc.
vendorSub:
hardwareConcurrency: 16 // (CPU逻辑核心数)
maxTouchPoints: 0 // (非触摸屏)

在 macOS 上的 Safari 浏览器中,可能看到:

browserName: BrowserName.safari
appCodeName: Mozilla
appName: Netscape
appVersion: 5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.X Safari/605.1.15 // (X为版本号)
// ... 其他信息
platform: MacIntel
userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.X Safari/605.1.15
vendor: Apple Computer, Inc.
// ... 其他信息
maxTouchPoints: 0 // (非触摸屏)

在 iPhone 上的 Safari (iOS 模拟器或真机):

browserName: BrowserName.safari
appCodeName: Mozilla
appName: Netscape
appVersion: 5.0 (iPhone; CPU iPhone OS 17_X like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.X Mobile/15E148 Safari/604.1 // (X为版本号)
// ... 其他信息
platform: iPhone // 注意,这里指示了是iPhone平台,但没有型号
userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_X like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.X Mobile/15E148 Safari/604.1
vendor: Apple Computer, Inc.
// ... 其他信息
maxTouchPoints: 5 // (支持多点触控)

看到了吗?device_info_plus 在 Web 上提供的是关于浏览器本身和它运行的非常粗略的平台信息 (比如 Win32, MacIntel, Linux x86_64, iPhone, Linux armv8l (可能是 Android)),以及一些浏览器特性(如 hardwareConcurrency, maxTouchPoints)。绝对没有 "iPhone 11" 或 "Asus ROG Strix" 这样的具体型号。

所以,如果你需要知道用户使用的是什么浏览器、大致的操作系统类别或者 CPU 核心数、是否支持触控等信息,device_info_plus 在 Web 上依然是个好帮手。但如果你的目标是获取硬件型号,那它就无能为力了。

三、获取可用信息的变通方案

既然直接获取行不通,我们只能退而求其次,尝试通过一些间接手段来获取尽可能有用的信息,或者调整我们的需求。

方案一:分析 User Agent 字符串

User Agent (UA) 字符串是 device_info_plus 在 Web 上能返回的最有信息量的字段之一 ( webBrowserInfo.userAgent )。虽然它设计上是给服务器用来识别客户端类型的,但里面常常会包含一些关于操作系统、设备类型(有时甚至是品牌)的蛛丝马迹。

原理与作用:

UA 字符串通常包含浏览器名称、版本、渲染引擎、操作系统信息,有时还会包含 Mobile 或特定设备标识(如 iPhone, iPad, Android)。通过解析这个字符串,我们可以尝试推断出设备的大致类型。

代码示例 (简单判断):

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:device_info_plus/device_info_plus.dart';
// 或者直接使用 dart:html (如果不想依赖 device_info_plus)
// import 'dart:html' as html;

Future<String> guessDeviceTypeFromUA() async {
  String userAgent = '';
  if (kIsWeb) {
    // 使用 device_info_plus 获取
    try {
       final deviceInfo = DeviceInfoPlugin();
       final webInfo = await deviceInfo.webBrowserInfo;
       userAgent = webInfo.userAgent ?? '';
    } catch (e) {
       print('Error getting UA via device_info_plus: $e');
       // 备选:直接使用 dart:html
       // try {
       //   userAgent = html.window.navigator.userAgent;
       // } catch (htmlError) {
       //    print('Error getting UA via dart:html: $htmlError');
       //    return 'Unknown';
       // }
    }

     if (userAgent.isEmpty) {
       // 如果两种方法都失败,尝试用 dart:html 的 navigator 对象
        try {
           // 需要 import 'dart:html' as html;
           // userAgent = html.window.navigator.userAgent ?? '';
           // 由于上方device_info_plus包含了dart:html 的依赖, 在Web上可直接调用
            userAgent = await DeviceInfoPlugin().webBrowserInfo.then((info) => info.userAgent ?? '');
        } catch(e) {
             print('Fallback: Error getting UA: $e');
            return 'Unknown';
        }
     }


    print('User Agent: $userAgent');

    // !!!注意:下面的判断非常粗略,且容易出错!!!
    userAgent = userAgent.toLowerCase(); // 转小写方便匹配

    if (userAgent.contains('iphone')) return 'Likely iPhone';
    if (userAgent.contains('ipad')) return 'Likely iPad';
    if (userAgent.contains('android')) {
       if (userAgent.contains('mobile')) {
         return 'Likely Android Phone';
       } else {
         return 'Likely Android Tablet/Device';
       }
    }
    if (userAgent.contains('windows nt')) return 'Likely Windows Desktop';
    if (userAgent.contains('macintosh') || userAgent.contains('mac os x')) return 'Likely macOS Desktop';
    if (userAgent.contains('linux') && !userAgent.contains('android')) return 'Likely Linux Desktop';

    // 可以根据需要添加更多规则... 但效果有限

  } else {
      return 'Not a web platform';
  }
  return 'Potentially Desktop or Unidentified Mobile'; // 默认猜测
}

// 使用示例
// String guessedType = await guessDeviceTypeFromUA();
// print('Guessed device type: $guessedType');

局限性与安全建议:

  1. 极不可靠: UA 字符串可以被用户或浏览器扩展轻易修改(Spoofing) 。很多浏览器甚至提供选项让用户选择发送不同的 UA。
  2. 格式多变: 没有严格统一的格式,不同浏览器、版本、甚至同一浏览器的不同模式(如桌面模式)下,UA 都可能不同。
  3. 信息有限: 即使 UA 可信,它通常也只包含非常通用的信息(例如,只会说 "iPhone",而不会说 "iPhone 15 Pro Max")。Android 设备的 UA 更是五花八门,很少直接暴露精确型号。
  4. 维护困难: 依赖 UA 的解析逻辑需要不断更新,以适应新的设备和浏览器变化,这是一场永无止境的“猫鼠游戏”。
  5. 隐私考虑: 过度依赖和解析 UA 可能也涉及到用户隐私问题,尤其是在结合其他信息进行用户画像时。

结论: 把 UA 作为一种非常粗略的猜测 手段可以,比如判断是移动端还是桌面端,大概是哪个操作系统。但绝对不能 依赖它来获取精确的设备型号,或者用于任何关键的业务逻辑或安全判断。

进阶使用技巧:

  • 有一些第三方库(JavaScript 社区比较多,Dart 的较少且维护可能滞后)尝试维护更复杂的 UA 解析规则库,可以解析出更丰富的信息(比如浏览器内核、更具体的 OS 版本等)。但对于精确硬件型号,它们同样面临上述所有挑战。引入这类库需要评估其维护性和准确性。

方案二:利用屏幕尺寸和特性检测

相比于猜测型号,检测设备的能力 通常更有用,也更符合 Web 开发的思路。比如,屏幕有多大?是否支持触控?

原理与作用:

  • 屏幕尺寸: 使用 MediaQuery.of(context).size 获取 Flutter 应用的逻辑尺寸,或者通过 dart:htmlwindow.screen.width / height 获取屏幕物理尺寸(注意两者区别和像素密度问题)。这有助于判断设备是小屏(手机)、中屏(平板)还是大屏(桌面)。
  • 特性检测: 使用 device_info_plus 返回的 maxTouchPoints 判断设备是否支持触控,以及支持多少点触控。或者检查 navigator.platform (webBrowserInfo.platform) 提供的大致系统类型。结合 kIsWebdefaultTargetPlatform (import 'package:flutter/foundation.dart';) 也能得到平台信息。

代码示例:

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
// 可能需要 device_info_plus 来获取 maxTouchPoints
import 'package:device_info_plus/device_info_plus.dart';
// 或者使用 dart:html
// import 'dart:html' as html;

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

  Future<int?> _getMaxTouchPoints() async {
      if (!kIsWeb) return null;
      try {
         final deviceInfo = DeviceInfoPlugin();
         final webInfo = await deviceInfo.webBrowserInfo;
         return webInfo.maxTouchPoints;
      } catch (e) {
         print("Error getting maxTouchPoints: $e");
         // Fallback or check via dart:html maybe?
         // return html.window.navigator.maxTouchPoints;
         return null;
      }
  }

  @override
  Widget build(BuildContext context) {
    final screenSize = MediaQuery.of(context).size;
    final orientation = MediaQuery.of(context).orientation;
    final platform = kIsWeb ? 'Web' : defaultTargetPlatform.toString(); // 基础平台

    return FutureBuilder<int?>(
      future: _getMaxTouchPoints(),
      builder: (context, snapshot) {
        final maxTouchPoints = snapshot.data;
        bool hasTouch = maxTouchPoints != null && maxTouchPoints > 0;

        String deviceCategory = 'Unknown';
        // 根据屏幕尺寸和触摸能力做简单推断 (非常粗略)
        if (screenSize.width < 600 && hasTouch) {
          deviceCategory = 'Likely Small Mobile (Phone-like)';
        } else if (screenSize.width < 1024 && hasTouch) {
           deviceCategory = 'Likely Tablet-like or Large Mobile';
        } else if (!hasTouch) {
           deviceCategory = 'Likely Desktop/Laptop (No Touch)';
        } else {
           deviceCategory = 'Likely Large Screen with Touch (e.g., Touch Laptop, large Tablet)';
        }


        return Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Screen Size: ${screenSize.width.toStringAsFixed(0)} x ${screenSize.height.toStringAsFixed(0)}'),
              Text('Orientation: $orientation'),
              Text('Platform Target: $platform'),
              if (snapshot.connectionState == ConnectionState.done)
                Text('Max Touch Points: ${maxTouchPoints ?? "N/A"}'),
              if (snapshot.connectionState == ConnectionState.waiting)
                 const Text('Checking touch capabilities...'),
              Text('Has Touch Support: $hasTouch'),
              const SizedBox(height: 10),
              Text('Guessed Category: $deviceCategory', style: const TextStyle(fontWeight: FontWeight.bold)),
               const SizedBox(height: 10),
              const Text('Note: Category is a rough guess based on size and touch.'),
            ],
          ),
        );
      }
    );
  }
}

局限性:

  • 屏幕尺寸不等于设备型号。很多不同型号的手机可能分辨率相近。
  • 用户可以在桌面上调整浏览器窗口大小,使得 MediaQuery.size 变化很大。window.screen 尺寸相对固定,但也不能唯一确定型号。
  • 触控支持也不绝对。现在很多笔记本电脑也带触摸屏。

优势:

  • 这种方法更关注于适配 ,而不是识别 。了解屏幕尺寸和触控能力,对于构建响应式布局(Responsive UI)至关重要,这通常比知道用户具体型号更有实际意义。

进阶使用技巧:

  • 可以结合 UA 字符串提供的一些线索(比如是否包含 Mobile )和屏幕尺寸、触控能力进行综合判断 ,得到一个稍微靠谱一点的设备类型猜测。但记住,这依然是猜测
  • 考虑使用 LayoutBuilder 来根据可用空间动态调整布局,这比基于猜测的设备类型去适配要健壮得多。

方案三:服务端参与或接受现实

如果你的应用场景非常 需要知道近似的设备型号(比如,为了统计分析或者提供非常 специфичный 的用户体验),并且你有相应的后端服务:

  • 带 Native Shell 的 Webview: 如果你的 Flutter Web 应用是嵌入在一个原生 App 的 WebView 中运行,那么原生 App 部分可以获取到详细的设备信息,并通过 JavaScript bridge 或其他通信方式传递给 Web 应用。这只适用于这种特定的混合应用场景。
  • 后台辅助信息: 后端可以通过分析请求头(包括 UA)、IP 地址归属等信息,结合第三方数据库(可能有成本和隐私风险)尝试推断设备信息。但这同样不精确且有局限。
  • 用户自愿提供: 在某些场景下(例如用户反馈、设备管理后台),你可以让用户手动选择或输入 他们的设备型号。这是最准确但也最需要用户配合的方式。

最重要的一点: 重新审视你的需求。你真的 需要知道用户是 "iPhone 11" 还是 "Samsung S21" 吗?或者你其实只是需要:

  • 为小屏幕优化布局?(使用 MediaQueryLayoutBuilder
  • 判断是否支持触控?(使用 maxTouchPoints
  • 区分 iOS 和 Android 用户?(UA 字符串或许能提供模糊线索,或者看 platform 字符串)
  • 区分移动端和桌面端?(结合屏幕尺寸、触控和 UA 判断)

通常,关注设备的能力和特性,并据此调整应用行为和界面,是更符合 Web 开发理念且更可靠的做法。

四、总结与建议

  1. 正视现实: 由于隐私和安全原因,浏览器环境下的 Web 应用无法直接、可靠地获取精确的硬件设备型号 (如 "iPhone 11")。
  2. 理解工具: device_info_plus 在 Flutter Web 上获取的是浏览器环境信息 (名称、版本、UA、大致平台、特性等),而不是硬件型号。它对于了解运行环境很有用。
  3. 谨慎变通:
    • 解析 User Agent (UA) 字符串可以提供一些非常粗略 的线索(操作系统、是否移动端),但它极不可靠 且易被篡改,不应用于关键逻辑。
    • 检测屏幕尺寸设备特性 (如触控支持)对于构建响应式布局 更有价值,但这也不能唯一确定设备型号。
  4. 调整思路: 优先考虑基于设备能力和特性 (屏幕大小、触控、平台类型)来适配你的 Flutter Web 应用,而不是执着于获取一个无法保证准确的设备型号名称。问问自己:“我拿到这个型号名称后,打算做什么?” 答案往往指向通过能力检测就能实现的目标。
  5. 管理预期: 如果需求方坚持要获取精确型号,需要明确告知 Web 技术的局限性。

所以,回到最初的问题,如果你想在 Flutter Web 里知道用户用的是不是 "iPhone 11" 或 "Samsung S11 Ultra",答案是:你基本做不到。接受这个限制,然后专注于利用那些可以 获取到的信息(浏览器信息、屏幕尺寸、触控能力)来打造更好的 Web 应用体验吧。