Flutter Device Info Detection Tutorial: A Web‑Safe, Production Pattern

Build a robust, web-safe Flutter device info layer using device_info_plus, platform checks, screen metrics, caching, and privacy best practices.

ASOasis
8 min read
Flutter Device Info Detection Tutorial: A Web‑Safe, Production Pattern

Image used for representation purposes only.

Overview

Detecting device information is a common need in Flutter apps: you may want to tailor layouts, gate features by OS/version, or attach environment metadata to diagnostics. In this tutorial you’ll build a production-ready, web-safe device info layer using device_info_plus, along with patterns for platform detection, screen metrics, caching, and privacy.

What you’ll learn:

  • Which device details you can/should collect
  • How to read cross-platform info with device_info_plus
  • Web-safe platform detection without importing dart:io on web
  • Gathering screen, locale, and accessibility metrics
  • Caching results and handling edge cases

Prerequisites: Flutter installed, comfortable with Dart and Flutter widgets.

What you can (and cannot) detect

  • Reasonable to read without extra permissions:
    • OS name/version, device model/brand, CPU ABI list (Android), machine ID like utsname.machine (iOS)
    • Browser name/version, userAgent (Web)
    • Screen size, pixel density, text scale, platform brightness (from MediaQuery)
    • Locale(s), platform accessibility flags (high contrast, bold text, etc.)
  • Sensitive or restricted:
    • IMEI, serial numbers, Wi‑Fi MAC, stable cross-app identifiers are off-limits on modern Android/iOS.
    • Advertising IDs are separate systems with their own policies and user consent flows.

Guideline: collect the minimum data you need, and never fingerprint users.

Project setup

  1. Create or open a Flutter project.
  2. Add the plugins:
dart pub add device_info_plus package_info_plus
  • device_info_plus: device/browser/hardware metadata
  • package_info_plus: app name/version/build number (handy for diagnostics)

No runtime permissions are needed for device_info_plus.

Choosing the right APIs

  • device_info_plus: strongly-typed per-platform info (AndroidBuild, IosDeviceInfo, WebBrowserInfo, etc.)
  • Platform detection: Prefer kIsWeb + defaultTargetPlatform to avoid importing dart:io on web. If you must use Platform, isolate it behind conditional imports (shown later).
  • MediaQuery/PlatformDispatcher: screen and accessibility metrics.

Data model

Create a single, serializable model your app can rely on regardless of platform.

class DeviceProfile {
  final String platform;        // 'android', 'ios', 'web', 'macos', 'windows', 'linux'
  final String os;              // e.g., 'Android', 'iOS', 'Web'
  final String osVersion;       // e.g., '14', '17.5', 'Chrome 126'
  final String model;           // e.g., 'Google Pixel 8', 'iPhone15,3', 'MacBookPro'
  final bool isPhysicalDevice;  // emulators/simulators/browsers => false
  final List<String> abis;      // Android only; others empty
  final Map<String, Object?> raw; // Keep a raw snapshot for diagnostics (optional)

  const DeviceProfile({
    required this.platform,
    required this.os,
    required this.osVersion,
    required this.model,
    required this.isPhysicalDevice,
    required this.abis,
    required this.raw,
  });

  Map<String, Object?> toJson() => {
    'platform': platform,
    'os': os,
    'osVersion': osVersion,
    'model': model,
    'isPhysicalDevice': isPhysicalDevice,
    'abis': abis,
    'raw': raw,
  };
}

Web-safe platform detection

Avoid importing dart:io directly in web builds. Use kIsWeb + defaultTargetPlatform.

import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform;

String currentPlatformLabel() {
  if (kIsWeb) return 'web';
  switch (defaultTargetPlatform) {
    case TargetPlatform.android: return 'android';
    case TargetPlatform.iOS: return 'ios';
    case TargetPlatform.macOS: return 'macos';
    case TargetPlatform.windows: return 'windows';
    case TargetPlatform.linux: return 'linux';
    case TargetPlatform.fuchsia: return 'fuchsia';
  }
}

If you truly need dart:io Platform, place it behind conditional imports (example later) so web builds remain green.

Implement DeviceInfoService

import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform;

class DeviceInfoService {
  final _plugin = DeviceInfoPlugin();

  Future<DeviceProfile> load() async {
    if (kIsWeb) {
      final web = await _plugin.webBrowserInfo;
      final browser = web.browserName.name; // e.g., chrome, safari
      final version = web.appVersion ?? web.userAgent ?? '';
      return DeviceProfile(
        platform: 'web',
        os: 'Web',
        osVersion: '$browser $version'.trim(),
        model: web.platform ?? 'browser',
        isPhysicalDevice: false,
        abis: const [],
        raw: {
          'browserName': browser,
          'userAgent': web.userAgent,
          'appName': web.appName,
          'appVersion': web.appVersion,
          'platform': web.platform,
          'deviceMemory': web.deviceMemory,
          'hardwareConcurrency': web.hardwareConcurrency,
          'vendor': web.vendor,
        },
      );
    }

    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        final a = await _plugin.androidInfo;
        return DeviceProfile(
          platform: 'android',
          os: 'Android',
          osVersion: a.version.release ?? '${a.version.sdkInt}',
          model: '${a.manufacturer} ${a.model}'.trim(),
          isPhysicalDevice: a.isPhysicalDevice ?? true,
          abis: List<String>.from(a.supportedAbis),
          raw: a.data,
        );

      case TargetPlatform.iOS:
        final i = await _plugin.iosInfo;
        return DeviceProfile(
          platform: 'ios',
          os: i.systemName ?? 'iOS',
          osVersion: i.systemVersion ?? '',
          model: i.utsname.machine ?? i.model ?? 'iPhone/iPad',
          isPhysicalDevice: i.isPhysicalDevice ?? true,
          abis: const [],
          raw: i.data,
        );

      case TargetPlatform.macOS:
        final m = await _plugin.macOsInfo;
        return DeviceProfile(
          platform: 'macos',
          os: 'macOS',
          osVersion: m.osRelease ?? '',
          model: m.model ?? 'Mac',
          isPhysicalDevice: m.isPhysicalDevice ?? true,
          abis: const [],
          raw: m.data,
        );

      case TargetPlatform.windows:
        final w = await _plugin.windowsInfo;
        return DeviceProfile(
          platform: 'windows',
          os: 'Windows',
          osVersion: '${w.majorVersion}.${w.minorVersion}.${w.buildNumber}',
          model: w.computerName ?? 'Windows PC',
          isPhysicalDevice: true,
          abis: const [],
          raw: w.data,
        );

      case TargetPlatform.linux:
        final l = await _plugin.linuxInfo;
        return DeviceProfile(
          platform: 'linux',
          os: l.name ?? 'Linux',
          osVersion: l.version ?? '',
          model: l.machineId ?? 'Linux',
          isPhysicalDevice: true,
          abis: const [],
          raw: l.data,
        );

      case TargetPlatform.fuchsia:
        return const DeviceProfile(
          platform: 'fuchsia', os: 'Fuchsia', osVersion: '', model: 'Fuchsia',
          isPhysicalDevice: true, abis: [], raw: {},
        );
    }
  }
}

Notes:

  • Use the data maps (e.g., a.data) for structured logging; they mirror platform fields and vary by OS version.
  • iOS models often appear as machine codes (e.g., iPhone15,3). Map to marketing names only if you truly need to—and maintain that mapping carefully.

Screen, density, and accessibility metrics

Collect runtime UI metrics alongside device info.

import 'package:flutter/material.dart';

class DeviceMetrics {
  final Size size;           // logical pixels
  final double pixelRatio;   // devicePixelRatio
  final double textScale;    // user text scale preference
  final Brightness brightness; // light/dark
  final EdgeInsets padding;  // safe areas
  final Locale? locale;      // primary locale

  DeviceMetrics({
    required this.size,
    required this.pixelRatio,
    required this.textScale,
    required this.brightness,
    required this.padding,
    required this.locale,
  });

  Map<String, Object?> toJson() => {
    'width': size.width,
    'height': size.height,
    'pixelRatio': pixelRatio,
    'textScale': textScale,
    'brightness': brightness.name,
    'padding': {
      'top': padding.top, 'left': padding.left, 'right': padding.right, 'bottom': padding.bottom,
    },
    'locale': locale?.toLanguageTag(),
  };
}

DeviceMetrics gatherMetrics(BuildContext context) {
  final mq = MediaQuery.of(context);
  return DeviceMetrics(
    size: mq.size,
    pixelRatio: mq.devicePixelRatio,
    textScale: mq.textScaleFactor,
    brightness: mq.platformBrightness,
    padding: mq.padding,
    locale: Localizations.maybeLocaleOf(context),
  );
}

Tip: For code that needs metrics outside widget trees, use WidgetsBinding.instance.platformDispatcher to read views and locales.

Caching and lifecycle

Device info changes rarely during a session. Cache it after first load to avoid redundant plugin calls.

class DeviceInfoRepository {
  DeviceInfoRepository._();
  static final instance = DeviceInfoRepository._();

  Future<DeviceProfile>? _memoized;
  Future<DeviceProfile> loadOnce() => _memoized ??= DeviceInfoService().load();
}

Use your state manager of choice (Provider, Riverpod, Bloc) to expose the cached Future.

Wiring it into UI

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

  @override
  Widget build(BuildContext context) {
    final metrics = gatherMetrics(context);

    return Scaffold(
      appBar: AppBar(title: const Text('Device Info')),
      body: FutureBuilder<DeviceProfile>(
        future: DeviceInfoRepository.instance.loadOnce(),
        builder: (context, snap) {
          if (snap.connectionState != ConnectionState.done) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snap.hasError) {
            return Center(child: Text('Error: ${snap.error}'));
          }
          final profile = snap.data!;
          final payload = {
            'device': profile.toJson(),
            'metrics': metrics.toJson(),
          };
          return ListView(
            padding: const EdgeInsets.all(16),
            children: [
              Text('Platform: ${profile.platform}'),
              Text('OS: ${profile.os} ${profile.osVersion}'),
              Text('Model: ${profile.model}'),
              Text('Physical: ${profile.isPhysicalDevice}'),
              const SizedBox(height: 12),
              Text('Screen: ${metrics.size.width.toStringAsFixed(0)}×${metrics.size.height.toStringAsFixed(0)} @ ${metrics.pixelRatio}x'),
              Text('Text scale: ${metrics.textScale}  Brightness: ${metrics.brightness.name}'),
              const Divider(height: 32),
              const Text('Raw JSON:', style: TextStyle(fontWeight: FontWeight.bold)),
              const SizedBox(height: 8),
              SelectableText(const JsonEncoder.withIndent('  ').convert(payload)),
            ],
          );
        },
      ),
    );
  }
}

Optional: compile-safe Platform checks with conditional imports

If you need dart:io Platform flags without breaking web builds, wrap them as follows.

File: lib/platform_check.dart

export 'platform_check_io.dart' if (dart.library.html) 'platform_check_web.dart';

File: lib/platform_check_io.dart

import 'dart:io' as io;

class PlatformCheck {
  static bool get isAndroid => io.Platform.isAndroid;
  static bool get isIOS => io.Platform.isIOS;
  static String get os => io.Platform.operatingSystem; // 'android', 'ios', 'macos', 'windows', 'linux'
}

File: lib/platform_check_web.dart

class PlatformCheck {
  static bool get isAndroid => false;
  static bool get isIOS => false;
  static String get os => 'web';
}

Use PlatformCheck in non-UI layers while keeping web compatibility.

Adding app metadata

It’s often useful to log your app version/build with device data.

import 'package:package_info_plus/package_info_plus.dart';

Future<Map<String, String>> loadAppInfo() async {
  final p = await PackageInfo.fromPlatform();
  return {
    'appName': p.appName,
    'packageName': p.packageName,
    'version': p.version,
    'buildNumber': p.buildNumber,
  };
}

Testing across platforms

  • Emulators/Simulators: expect isPhysicalDevice=false, and model/brand may be generic.
  • Android: Try devices running different SDK levels to see field variability (e.g., supportedAbis).
  • iOS: You’ll often see machine codes (utsname.machine). That’s normal.
  • Web: Verify across Chrome/Firefox/Safari; userAgent strings vary.
  • Desktop: Windows/macOS/Linux info depends on OS APIs; some fields may be empty.

Privacy and policy checklist

  • Minimize: only collect fields you actually use.
  • Disclose: state what you collect and why in your privacy policy.
  • Avoid identifiers: do not attempt fingerprinting. Never collect IMEI/serial even if a plugin exposes it on older devices.
  • Respect platform rules: Google Play and Apple guidelines restrict persistent identifiers and tracking without consent.
  • Security: redact logs, prefer transient analytics fields, and encrypt at rest if stored.

Troubleshooting

  • Web build fails due to dart:io import: move Platform usage behind conditional imports or rely on kIsWeb + defaultTargetPlatform only.
  • Null/empty fields: OS vendors may redact info; always handle nulls and provide defaults.
  • iOS marketing names: use a maintained mapping only if UX truly requires it; otherwise display the machine code as-is.
  • Locale vs. language: locale may differ from in-app language if you offer overrides.

Complete minimal example (main.dart)

import 'dart:convert';
import 'package:flutter/material.dart';

// Add: device_info_plus and package_info_plus to pubspec.yaml
// Then include the classes from earlier sections in proper files and imports.

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Device Info Demo',
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      home: const DeviceInfoPage(),
    );
  }
}

// Include DeviceInfoPage, DeviceInfoService, DeviceProfile, and gatherMetrics

Summary

You now have a robust, cross-platform device info pipeline for Flutter that:

  • Uses device_info_plus for structured metadata
  • Avoids web pitfalls via kIsWeb and conditional imports when necessary
  • Captures screen and accessibility metrics from MediaQuery
  • Caches results and handles variability across OS versions

Start with the minimal example, then wire the profile into your analytics, diagnostics, or feature flags—always keeping privacy and platform policies front and center.

Related Posts