---
title: Android SDK threading model
description: How the Extentos Android SDK uses Kotlin coroutines and Flow — suspend functions for one-shot ops, Flow/StateFlow for streams, and which dispatcher to collect on.
type: guide
platform: android
vendor: meta
related:
  - /docs/sdk/android/initialization
  - /docs/sdk/android/lifecycle
  - /docs/sdk/android/runtime-internals
  - /docs/reference/errors
---

<Callout type="warn">
  **Camera and display need the Meta vendor module.** `com.extentos:glasses`
  carries no vendor SDK, so add `implementation("com.extentos:glasses-meta")`
  alongside it — see [install](/docs/sdk/android/install). Without it the build
  still succeeds and voice still works, but `capabilities.camera` is `false` and
  captures return errors. The SDK logs a warning at startup when it spots that
  combination.
</Callout>


The Extentos Android SDK is coroutine-native. Its public surface is split cleanly into two shapes:

- **`suspend` functions** for one-shot operations that complete with a result.
- **`Flow` / `StateFlow`** for continuous streams and observable state.

There are no callbacks to register and no threads to manage — you `collect` flows and `await` suspend functions from your own coroutine scopes.

## One-shot operations are `suspend`

Discrete capability calls suspend until they finish and return an [`ExtentosResult`](/docs/reference/errors):

```kotlin
suspend fun takeShot() {
    when (val r = glasses.camera.capturePhoto()) {
        is ExtentosResult.Ok  -> show(r.value)
        is ExtentosResult.Err -> report(r.error)
    }
    glasses.audio.speak("Got it.")        // also suspend
}
```

Examples: `camera.capturePhoto()`, `camera.captureVideo()`, `audio.recordDiscrete(...)`, `audio.speak(...)`, `audio.cancelSpeak()`, and `glasses.shutdown()`. Call them from a coroutine (`lifecycleScope`, `viewModelScope`, or any scope you control). They are cooperative with cancellation — cancelling the calling coroutine cancels the operation.

## Streams are `Flow`

Continuous capabilities return a cold `Flow` you collect for as long as you want the stream:

```kotlin
// Live transcription
glasses.audio.transcriptions().collect { transcript ->
    updateCaption(transcript.text)
}

// Raw mic chunks / camera frames
glasses.audio.audioChunks().collect { chunk -> /* ... */ }
glasses.camera.videoFrames().collect { frame -> /* ... */ }
```

These are cold — the underlying capture starts when collection starts and stops when the collecting coroutine is cancelled. Run them on a scope whose lifetime matches the subscription (and start the [foreground service](/docs/sdk/android/lifecycle) if the stream must survive backgrounding).

## Observable state is `StateFlow` / event `Flow`

Two always-available streams describe the SDK's own state:

| Stream | Type | What it carries |
|---|---|---|
| `glasses.connection.state` | `StateFlow<GlassesState>` | The live connection state — always has a current value. |
| `glasses.runtime.events` | `Flow<RuntimeEvent>` | The on-device event log (toggles, transport, logs, warnings). |

Collect connection state lifecycle-aware so the UI tracks it without leaking:

```kotlin
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        glasses.connection.state.collect { state ->
            render(state)
        }
    }
}
```

In Compose, `collectAsStateWithLifecycle()` is the idiomatic equivalent:

```kotlin
val connState by glasses.connection.state.collectAsStateWithLifecycle()
```

Because `state` is a `StateFlow`, a new collector immediately receives the current value — you don't miss the connection state by subscribing late. The event log additionally offers a one-shot snapshot if you don't want to collect: `glasses.runtime.snapshotEvents()` (a `suspend` function returning `List<RuntimeEvent>`).

## Dispatcher guidance

- **You don't pick a dispatcher for the SDK's internals.** It runs its own work on a `SupervisorJob` + `Dispatchers.Default` scope created at `create()` and cancelled at `shutdown()`. Flow emissions and suspend-function bodies execute there.
- **Collect on a UI-aware scope** (`lifecycleScope`, `viewModelScope`) and touch UI from the main dispatcher. Use `flowOn(...)` only if *your own* downstream operators are heavy — the SDK already does its capture work off the main thread.
- **Don't block.** Never call a `suspend` operation from `runBlocking` on the main thread; launch it on a coroutine scope.

## Don't wrap results in `runCatching`

Capability and lifecycle operations return `ExtentosResult` and **do not throw** for expected failures. Pattern-match the result; `runCatching` would only ever catch unrelated exceptions and would hide the typed failure. See [Errors](/docs/reference/errors) for the failure variants and the `valueOrNull()` / `map` / `flatMap` helpers.
