Device and App Info

Read build and device facts with expo-application and expo-device, and branch on OS version rather than on a model name.

Syntaxconst label = `${Application.nativeApplicationVersion} (${Application.nativeBuildVersion})`

Two small modules answer most questions about where your app is running. expo-application describes the installed app; expo-device describes the hardware and operating system. Install them with npx expo install expo-application expo-device.

Constants, not hooks

Most of what these modules expose are plain constants read at startup, so Device.osVersion and Application.nativeApplicationVersion are values, not calls and not hooks. A few things need a call because the OS answers asynchronously: Application.getInstallationTimeAsync() resolves to a Date, and Device.getDeviceTypeAsync() resolves to a DeviceType member such as PHONE or TABLET. Anything the platform cannot answer is null, which is why Device.modelName ?? "Unknown" appears so often. That is shorthand for Device.modelName !== null ? Device.modelName : "Unknown".

Version against build number

nativeApplicationVersion is the human version: CFBundleShortVersionString on iOS, version on Android. nativeBuildVersion is the number the stores order uploads by: CFBundleVersion on iOS, versionCode on Android. Users quote the first; support needs the second. Print both.

Branch on the version, not the model

A model name is a marketing string that changes every year. Capability tracks the OS version. On Android, Platform.Version and Device.platformApiLevel are the API level as a number; on iOS Platform.Version is a version string and platformApiLevel is null. Compare those, never modelName.

Device.isDevice is false on a simulator, which is useful for keeping test payments out of production.

Example

Example · tsx
import * as Application from "expo-application";
import * as Device from "expo-device";
import { Platform, StyleSheet, Text, View } from "react-native";

export default function AboutScreen() {
  // These are constants, not function calls.
  const version = Application.nativeApplicationVersion ?? "unknown";
  const build = Application.nativeBuildVersion ?? "unknown";
  // ?? is shorthand for: Device.osName !== null ? Device.osName : Platform.OS
  const os = `${Device.osName ?? Platform.OS} ${Device.osVersion ?? ""}`.trim();

  return (
    <View style={styles.screen}>
      <Text style={styles.heading}>About this app</Text>
      <Text style={styles.row}>{`Version ${version} (build ${build})`}</Text>
      <Text style={styles.row}>{os}</Text>
      <Text style={styles.row}>{Device.modelName ?? "Unknown device"}</Text>
      {/* A string must sit inside Text, so the simulator badge is a Text too. */}
      {Device.isDevice ? null : <Text style={styles.badge}>Simulator build</Text>}
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, padding: 24, gap: 8 },
  heading: { fontSize: 22, fontWeight: "600", marginBottom: 8 },
  row: { fontSize: 16, color: "#374151" },
  badge: { fontSize: 13, color: "#b45309", marginTop: 12 },
});

When to use it

  • Put version and build on a settings screen so support can match a report to a binary.
  • Attach device and OS details to every crash report.
  • Skip a feature on OS versions that do not support it.
  • Point the app at a payment sandbox when Device.isDevice is false.

More examples

Branching on the OS version

This is the right shape for every capability check: a pure function of the OS and its version, plus one line that feeds it the real constants. Android 12 and earlier granted notifications at install, Android 13 added a runtime prompt, and the API level is the only reliable way to tell those apart. Comparing modelName instead would break on every device released after you wrote the check.

Example · tsx
// capabilities.ts
import { Platform } from "react-native";
import * as Device from "expo-device";

// Takes the facts as arguments so it can be tested without a device.
export function needsNotificationPrompt(
  os: string,
  androidApiLevel: number | null,
): boolean {
  if (os === "ios") return true; // iOS has always prompted
  if (os !== "android") return false;
  // Android 13 is API level 33, the release that added POST_NOTIFICATIONS.
  return androidApiLevel !== null && androidApiLevel >= 33;
}

// The call site supplies the real values.
export const shouldPrompt = needsNotificationPrompt(
  Platform.OS,
  Device.platformApiLevel, // number on Android, null on iOS
);

console.log(needsNotificationPrompt("android", 33));
// Output: true
console.log(needsNotificationPrompt("android", 31));
// Output: false
console.log(needsNotificationPrompt("ios", null));
// Output: false

A diagnostics payload for bug reports

Attach this object to every crash report and support email. Three fields do most of the work: build tells you which binary, bundleId tells you which variant (a staging build has a different application ID), and realDevice separates a tester in a simulator from a real user. Keep it to facts the user can see on an about screen; identifiers that follow a person around belong in a privacy review, not in a convenience payload.

Example · tsx
import * as Application from "expo-application";
import * as Device from "expo-device";
import { Platform } from "react-native";

export type Diagnostics = {
  app: string;
  build: string;
  bundleId: string;
  os: string;
  device: string;
  realDevice: boolean;
  installedAt: string | null;
};

export async function collectDiagnostics(): Promise<Diagnostics> {
  // Resolves to a Date on Android and iOS, null on web.
  const installed = await Application.getInstallationTimeAsync();
  return {
    app: Application.nativeApplicationVersion ?? "unknown",
    build: Application.nativeBuildVersion ?? "unknown",
    bundleId: Application.applicationId ?? "unknown",
    os: `${Device.osName ?? Platform.OS} ${Device.osVersion ?? "?"}`,
    device: Device.modelName ?? "unknown",
    realDevice: Device.isDevice,
    installedAt: installed ? installed.toISOString() : null,
  };
}

Phone or tablet layout

getDeviceTypeAsync returns a DeviceType member, and comparing against Device.DeviceType.TABLET is clearer than guessing from screen size. Use it for decisions that are really about the class of device, such as whether a split view makes sense. For decisions about available space, measure the window instead, because a tablet in a small multitasking pane is as narrow as a phone. On screen: two stacked grey panes on a phone, side by side on a tablet.

Example · tsx
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import * as Device from "expo-device";

export default function Dashboard() {
  const [twoColumn, setTwoColumn] = useState(false);

  useEffect(() => {
    let cancelled = false;
    Device.getDeviceTypeAsync().then((type) => {
      if (cancelled) return;
      setTwoColumn(type === Device.DeviceType.TABLET);
    });
    return () => {
      cancelled = true;
    };
  }, []);

  return (
    <View style={[styles.screen, twoColumn ? styles.row : styles.column]}>
      <View style={styles.pane}>
        <Text>List</Text>
      </View>
      <View style={styles.pane}>
        <Text>Detail</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, gap: 12, padding: 12 },
  // flexDirection is column by default, so row has to be stated.
  row: { flexDirection: "row" },
  column: { flexDirection: "column" },
  pane: { flex: 1, backgroundColor: "#f3f4f6", borderRadius: 12, padding: 16 },
});

Discussion

  • Be the first to comment on this lesson.