Part of Github Copilot with Kotlin

Kotlin Copilot: Jetpack compose get device ID

Sandy LaneSandy Lane

Video: Kotlin Copilot: Jetpack compose get device ID by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Jetpack Compose: Get the Device ID

Settings.Secure.ANDROID_ID via LocalContext. Wrap in remember so it's read once. Useful for analytics; risky for anything that requires a stable identifier across factory resets.

A small puzzle: display the Android ANDROID_ID in a Composable. Two pieces — accessing the system context from a Composable, and remembering the read so it doesn't repeat on recomposition.

The Copilot prompt

import androidx.compose.runtime.Composable

// Composable that displays the Android device ID
@Composable
fun DeviceIdDisplay() {

Copilot completes:

import android.content.Context
import android.provider.Settings
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext

@Composable
fun DeviceIdDisplay() {
  val context = LocalContext.current
  val deviceId = remember {
    Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
  }
  Text("Device ID: $deviceId")
}

Walkthrough

Three pieces.

1. LocalContext.current. LocalContext is a CompositionLocal provided automatically by Compose's Activity/Application setup. .current reads the current value — the Context of the hosting Activity.

Composables don't have a Context parameter by default. LocalContext is the bridge to Android-platform APIs (resources, services, content resolvers).

2. remember { ... } to cache the read. Without remember, the Settings.Secure.getString call would run on every recomposition — wasteful for a value that never changes.

remember caches the lambda's return value. With no key, it caches once for the Composable's lifetime in this composition.

3. Settings.Secure.ANDROID_ID. A 64-bit hex string. Generated at first boot; stays constant until factory reset.

Important caveats

ANDROID_ID isn't a reliable unique identifier:

  • Resets on factory reset. Two devices that have been wiped both produce new IDs.
  • Per-app on Android 8+. Each signing key sees a different ID for the same device. Apps with the same signing key see the same ID.
  • Same on all users. If the device has multiple users, each user's apps see a separate ID.
  • Was unreliable on older Android. Pre-O, some devices returned null or a hardcoded "9774d56d682e549c".

For analytics or per-install identification: use Firebase Installations or an app-generated UUID stored in app preferences.

For DRM/anti-fraud: there's no truly stable cross-app identifier on Android, by design. Apple's IDFA had the same trajectory.

A more realistic device-info display

@Composable
fun DeviceInfo() {
  val context = LocalContext.current
  val info = remember {
    DeviceInfo(
      androidId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID),
      manufacturer = Build.MANUFACTURER,
      model = Build.MODEL,
      sdkVersion = Build.VERSION.SDK_INT,
      versionCode = Build.VERSION.RELEASE,
    )
  }
  Column {
    Text("Android ID: ${info.androidId}")
    Text("${info.manufacturer} ${info.model}")
    Text("Android ${info.versionCode} (SDK ${info.sdkVersion})")
  }
}

data class DeviceInfo(
  val androidId: String,
  val manufacturer: String,
  val model: String,
  val sdkVersion: Int,
  val versionCode: String,
)

Build.MANUFACTURER, Build.MODEL, Build.VERSION.* come from android.os.Build — they don't need a Context.

remember vs derivedStateOf

// Once on first composition, cached forever:
val deviceId = remember { Settings.Secure.getString(...) }

// Re-runs whenever the dependency changes:
val deviceId by remember(context) {
  derivedStateOf { Settings.Secure.getString(context.contentResolver, ANDROID_ID) }
}

For a value that never changes within a composition's lifetime (like ANDROID_ID), plain remember is correct. derivedStateOf is for values that change as their inputs change.

For "compute once," remember wins on simplicity.

Alternatives: hoist out of the Composable

@Composable
fun DeviceIdDisplay(deviceId: String) {
  Text("Device ID: $deviceId")
}

// Caller:
val context = LocalContext.current
val deviceId = remember { Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) }
DeviceIdDisplay(deviceId)

Hoisting state up makes the inner Composable testable without a real Context. Standard Compose pattern: read system state at the top, pass plain values down.

For a UI test, you'd just call DeviceIdDisplay(deviceId = "test-id") with no Context wiring.

A word on permissions

ANDROID_ID requires no special permission. Reading it from a Composable on launch is fine.

For other device-identifying APIs:

  • IMEI / IMSI / phone numbers — require READ_PHONE_STATE (and often a Play Store justification).
  • MAC address — restricted on Android 6+; returns a constant fake value.
  • Serial number (Build.SERIAL) — returns "unknown" on Android 8+.

The trend is clear: Android is making cross-app device fingerprinting increasingly difficult. Lean on app-scoped IDs from Firebase or your own preferences.

Common mistakes

Reading without remember. Recomposition triggers Settings.Secure.getString repeatedly. For a constant value, that's wasted work.

Treating ANDROID_ID as unique. It's per-app-signing-key-per-user on Android 8+. Multiple installs of your app on one device share an ID; other apps see a different ID.

Calling from non-Activity Context. Settings.Secure.getString works on the Application Context too, but if you accidentally pass a null content resolver (rare), it crashes.

Using Build.SERIAL. Deprecated since Android 8 — returns "unknown".

Storing the ID server-side without privacy disclosure. It's a stable-ish identifier; treat it as PII for compliance.

What's next

Episode 8: Set up a Ktor client and fetch JSON. Build a typed API client using Ktor, kotlinx.serialization, and runBlocking for a quick demo.

Recap

LocalContext.current to access the platform context from a Composable. remember { ... } to compute once. Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) for the device ID. Caveats: not unique across factory reset, per-signing-key on Android 8+, never trust for security. For UI tests, hoist the ID out of the Composable as a parameter. Other identifying APIs (IMEI, MAC, serial) are mostly restricted on modern Android.

Next episode: Ktor client setup.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.