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.
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
- Create or open a Flutter project.
- 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
Flutter WorkManager Scheduled Tasks: The Complete, Practical Guide (2026)
A practical guide to scheduling background tasks in Flutter with WorkManager on Android and iOS, with setup, periodic jobs, constraints, and debugging.
Flutter WorkManager: Reliable Background Tasks on Android and iOS
A practical guide to Flutter WorkManager for background tasks: setup, scheduling, constraints, iOS limits, testing, and best practices.
Flutter CI CD with GitHub Actions: A Step-by-Step Tutorial
Step-by-step Flutter CI CD with GitHub Actions: lint, test, build Android iOS, sign, cache, and release to stores. Ready-to-copy YAML included.