x_app_utils
A Flutter plugin that exposes unified app, device, locale, timezone, and identifier information on iOS and Android.
The package keeps native values cached in memory and initializes itself on first use, so values can be read through synchronous getters in the rest of your app.
Features
- App metadata: app name, package name, version, build number, installer store, install time, update time, and Android build signature.
- Device and system metadata: model, manufacturer/platform, OS version, device name, physical device flag, disk size, and memory size.
- Locale and timezone metadata: language code, ISO-3 language code, country code, ISO-3 country code, locale, timezone, and UTC offset.
- Identifiers: IDFA, IDFV, Android ID, Google advertising ID, App Set ID,
secure primary/secondary local IDs, and unified
advertisingId/deviceIdconvenience getters. - iOS ATT authorization helper for requesting IDFA access at the right moment in your app flow.
- Lifecycle-friendly EventBus helpers for plain Dart owners and Flutter
Stateobjects.
Platform Field Guide
All fields are exposed from the same XAppUtils.instance API. Platform-specific
fields return documented fallbacks on unsupported platforms instead of null.
App metadata
| Getter | Android | iOS |
|---|---|---|
appName |
Application label | CFBundleDisplayName or CFBundleName |
packageName |
Android package name | Bundle identifier |
version |
versionName |
CFBundleShortVersionString |
buildNumber |
versionCode / long version code |
CFBundleVersion |
buildSignature |
SHA-256 signing certificate digest | '' |
installerStore |
Installing package/store when available | '' |
installTime |
First install time, milliseconds since epoch | 0 |
updateTime |
Last update time, milliseconds since epoch | 0 |
Device, system, locale, and storage
| Getter | Android | iOS |
|---|---|---|
deviceModel |
Build.MODEL |
Hardware machine identifier, such as iPhone16,1 |
platform |
Build.MANUFACTURER |
UIDevice.model |
osVersion |
Android release | iOS system version |
systemName |
Android |
UIDevice.systemName |
deviceName |
Device model/name fallback | UIDevice.name |
isPhysicalDevice |
Emulator heuristic | Simulator check |
freeDiskSize / totalDiskSize |
Data partition bytes | Home filesystem bytes |
physicalRamSize |
Total RAM in MiB | Physical memory in MiB |
availableRamSize |
Available RAM in MiB | 0 |
languageCode / languageCode3 |
Locale language and ISO-3 code | Preferred language and ISO-3 code |
languageTag |
BCP-47 language tag without region | Preferred language tag without region |
languageScriptCode |
Script code when available | Script code when available |
countryCode / countryCode3 |
Locale region and ISO-3 code | Preferred region and ISO-3 code |
locale |
Full locale tag | Full preferred language identifier |
timeZone |
Time zone ID | Time zone ID |
utcOffsetSeconds |
Current UTC offset | Current UTC offset |
Identifiers
| Getter | Android | iOS |
|---|---|---|
advertisingId |
Google Advertising ID after refreshAdvertisingId() |
IDFA when authorized |
deviceId |
Android ID, App Set ID, then local ID fallback | IDFV, then local ID fallback |
idfa / idfv |
'' |
IDFA / IDFV |
gaid / aaid / aifa |
Google Advertising ID when available | '' |
androidId / andi |
Settings.Secure.ANDROID_ID |
'' |
asid |
App Set ID after refreshDeviceId() or getAll |
'' |
primaryLocalId / secondaryLocalId |
Android KeyStore-backed encrypted values | Keychain-backed values |
Android-only fields
androidBoard, androidBootloader, androidBrand, androidDevice,
androidDisplay, androidFingerprint, androidHardware, androidHost,
androidProduct, androidSupported32BitAbis, androidSupported64BitAbis,
androidSupportedAbis, androidTags, androidType,
androidSystemFeatures, androidIsLowRamDevice, androidBaseOs,
androidSdkInt, androidRelease, androidCodename,
androidIncremental, androidPreviewSdkInt, and
androidSecurityPatch are populated only on Android.
iOS-only fields
iosModelName, iosLocalizedModel, isiOSAppOnMac, isiOSAppOnVision,
iosUtsnameSysname, iosUtsnameNodename, iosUtsnameRelease,
iosUtsnameVersion, and iosUtsnameMachine are populated only on iOS.
Installation
dependencies:
x_app_utils: ^0.1.6
Then import the package:
import 'package:x_app_utils/x_app_utils.dart';
Usage
For a complete Chinese integration guide, see doc/USAGE.md.
Read values from the shared instance:
final info = XAppUtils.instance;
final appName = info.appName;
final packageName = info.packageName;
final version = info.version;
final model = info.deviceModel;
final country = info.countryCode;
final timeZone = info.timeZone;
final deviceId = info.deviceId;
final primaryLocalId = info.primaryLocalId;
final secondaryLocalId = info.secondaryLocalId;
If native values have not been loaded yet, the first getter read starts loading them automatically and returns the documented fallback value for that read. Later reads return the cached native values.
XAppUtils.instance is the recommended entry point. XAppUtils() is
kept as a singleton factory for compatibility.
You may also initialize explicitly during app startup when you want to preload native values:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final initialized = await XAppUtils.instance.init();
if (!initialized) {
// Some native values or secure local ID persistence are unavailable.
// primaryLocalId and secondaryLocalId still have in-memory UUID fallbacks.
}
runApp(const MyApp());
}
init() returns true when native values were loaded and false when they
were unavailable. It does not throw for channel/native read failures.
If you must guarantee that native values are loaded before a local read, use:
final info = await XAppUtils.instance.ready;
final model = info.deviceModel;
Refreshing Values
final refreshed = await XAppUtils.instance.refresh();
await XAppUtils.instance.refreshAdvertisingId();
await XAppUtils.instance.refreshDeviceId();
await XAppUtils.instance.resetLocalId();
The plugin also refreshes cached values when the app returns to the foreground.
This passive refresh does not read, create, or replace primaryLocalId and
secondaryLocalId; those cached values change only through the explicit local
ID write or reset APIs. If a local-ID read finds native storage missing an ID,
it creates the ID and updates the cache; otherwise reads leave the cache
unchanged.
Advertising IDs and Android privacy
init() and foreground refreshes do not read advertising identifiers. Read the
Google advertising ID only when your app has obtained any required user consent:
await XAppUtils.instance.refreshAdvertisingId();
The package intentionally does not add Android's AD_ID permission to the host
app. If you use refreshAdvertisingId(), add it to the host application's
android/app/src/main/AndroidManifest.xml and complete the applicable Google
Play Data safety disclosure:
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
refreshDeviceId() reads Android's App Set ID on demand. Both calls return an
empty identifier when the relevant service, permission, or user setting makes
the value unavailable.
Secure Local IDs
primaryLocalId and secondaryLocalId are UUID values generated independently
on each platform. Android stores them with Android KeyStore + RSA OAEP +
AES-GCM encrypted SharedPreferences. iOS stores them in Keychain with
kSecAttrAccessibleWhenUnlocked.
By default, storage keys use the Android package name or iOS bundle identifier. If the platform namespace is temporarily unavailable, provide a fallback namespace; when both namespaces are available on the same platform, values are synchronized so future reads stay consistent.
await XAppUtils.instance.init(
localIdStorageOptions: const LocalIdStorageOptions(
fallbackNamespace: 'my_app',
),
);
Manage either local ID through a single slot-based API:
final primary = await XAppUtils.instance.readLocalId();
final secondary = await XAppUtils.instance.readLocalId(
slot: LocalIdSlot.secondary,
);
await XAppUtils.instance.writeLocalId('custom-id');
final exists = await XAppUtils.instance.containsLocalId();
final newPrimary = await XAppUtils.instance.resetLocalId();
iOS IDFA and ATT
init() never shows the ATT prompt. It only reads IDFA when tracking permission
has already been granted.
To request ATT authorization:
final result = await XAppUtils.instance.requestAppTrackingTransparency();
if (result.isSuccess) {
final idfa = result.idfa;
} else {
final failure = result.failure;
}
Before calling requestAppTrackingTransparency() in a real iOS app, add
NSUserTrackingUsageDescription to the host app's ios/Runner/Info.plist:
<key>NSUserTrackingUsageDescription</key>
<string>This identifier is used to provide more relevant content and advertising.</string>
Fallbacks
String getters never return null. Unavailable values return '', except these
documented locale defaults:
| Getter | Default |
|---|---|
languageCode |
en |
languageCode3 |
eng |
languageTag |
en |
languageScriptCode |
'' |
countryCode |
US |
countryCode3 |
USA |
locale |
en |
Integer getters return 0, boolean getters return false, and list getters
return an empty list when native values are unavailable.
Disk sizes are bytes. RAM sizes are MiB; iOS reports 0 for
availableRamSize, because iOS has no supported system-wide available-memory
API.
All public async APIs keep operational failures internal. Local-ID write and reset failures leave the existing cache unchanged; callers receive the method's safe default rather than an exception.
Publishing
Before publishing from this directory, run:
flutter test
flutter analyze
flutter pub publish --dry-run
Libraries
- event_bus_manager
- A lightweight, application-wide event bus with optional lifecycle owners.
- event_bus_manager_flutter
- Flutter State lifecycle support for EventBusManager.
- x_app_utils