# Glyph — Full Documentation
> Glyph is hosted server-driven UI for Android, built on `androidx.compose.remote`. Tenants sign up, push binary `.rc` screen documents from their Gradle build, and Android apps render the updated UI on next launch — natively, no WebView, no JSON schema, no Play Store release.
This file concatenates every page under for one-shot ingestion by AI agents. The structured index lives at . Individual pages are still served as plain markdown at .
Site URL:
Upstream framework:
---
# Glyph documentation
Glyph hosts [`androidx.compose.remote`](https://developer.android.com/jetpack/androidx/releases/compose-remote)
documents for your Android apps. Write screens in Kotlin, push them
with one Gradle task, and your users render the new UI on next launch
— natively, no WebView.
## Pages
- [Getting started](/docs/getting-started)
- [Platforms](/docs/platforms) — Android, iOS, Web, KMP support matrix.
- [Authoring screens](/docs/authoring) — `@RemoteScreen` + `RemoteText` / `RemoteColumn` / `RemoteBox`.
- [Consuming screens](/docs/consuming) — `Glyph.Foo(...)` typed accessors.
- [Params and actions](/docs/params-and-actions) — typed slots, `hostAction(...)`, `valueChange`.
- [Consumer keys](/docs/consumer-keys) — opt-in read auth.
- [REST API](/docs/api-reference).
---
# Getting started
## 1. Create an app
Sign in at , click **Create app**, copy the
generated `appId` and `apiKey` (the publish key — `rck_...`).
## 2. settings.gradle.kts
```kotlin
pluginManagement {
repositories {
maven { url = uri("https://artifacts.premex.se/api/maven/premex/glyph/") }
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
maven { url = uri("https://artifacts.premex.se/api/maven/premex/glyph/") }
google()
mavenCentral()
}
}
// Foojay resolves the JDK 21 toolchain Glyph requires (see below).
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
```
---
## Producer (publishes screens)
### 3. Authoring module — `screens/build.gradle.kts`
```kotlin
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose") // REQUIRED — see note
id("se.premex.glyph") version "0.3.5"
}
glyph {
appId = "your-app-id"
apiKey = providers.environmentVariable("GLYPH_API_KEY")
generatedPackage = "com.example.app.glyph" // pick a package you own
}
android {
namespace = "com.example.app.screens"
compileSdk = 37
defaultConfig { minSdk = 29 } // Glyph needs ≥ 29
testOptions { unitTests { isIncludeAndroidResources = true } }
}
kotlin {
jvmToolchain(21) // Glyph needs JDK 21
}
```
> **The `org.jetbrains.kotlin.plugin.compose` plugin is required.**
> Without it `@Composable` annotations are inert and you'll see
> `Argument type mismatch: actual type is 'Function0', but
> 'ComposableFunction0' was expected` on every nested
> `RemoteBox { … }` / `RemoteColumn { … }`.
**Hard requirements**
| | Why |
|---|---|
| `minSdk = 29` | Inherited from `androidx.compose.remote:1.0.0-alpha13`. |
| `compileSdk = 37` | `androidx.compose.remote:1.0.0-alpha13` requires `compileSdk` ≥ 37. |
| `jvmToolchain(21)` | Plugin + capture-test classes target JDK 21. Foojay (declared in `settings.gradle.kts` above) downloads it for you. |
The `se.premex.glyph` plugin auto-applies KSP and brings in
`plugin-runtime`, `plugin-ksp`, `remote-creation-compose`, plus the
Robolectric capture-test deps. No manual dependency wiring needed.
### 4. Write a screen
```kotlin
@file:OptIn(androidx.compose.remote.creation.compose.ExperimentalRemoteCreationComposeApi::class)
@file:Suppress("RestrictedApi")
package com.example.app.screens
import androidx.compose.remote.creation.compose.action.hostAction
import androidx.compose.remote.creation.compose.layout.RemoteBox
import androidx.compose.remote.creation.compose.layout.RemoteColumn
import androidx.compose.remote.creation.compose.layout.RemoteText
import androidx.compose.remote.creation.compose.modifier.RemoteModifier
import androidx.compose.remote.creation.compose.modifier.clickable
import androidx.compose.remote.creation.compose.modifier.fillMaxWidth
import androidx.compose.remote.creation.compose.modifier.padding
import androidx.compose.remote.creation.compose.state.RemoteFloat
import androidx.compose.remote.creation.compose.state.RemoteString
import androidx.compose.remote.creation.compose.state.rdp
import androidx.compose.remote.creation.compose.state.rf
import androidx.compose.remote.creation.compose.state.rs
import androidx.compose.remote.creation.compose.state.rsp
import androidx.compose.runtime.Composable
import se.premex.glyph.runtime.RemoteScreen
@RemoteScreen("checkout")
@Composable
fun checkout(
userName: RemoteString = "guest".rs,
totalAmount: RemoteFloat = 0f.rf,
) {
RemoteBox(modifier = RemoteModifier.fillMaxWidth().padding(24.rdp)) {
RemoteColumn {
RemoteText("Hi, ".rs + userName, fontSize = 22.rsp)
RemoteText("Total: ".rs + totalAmount.toRemoteString(), fontSize = 16.rsp)
RemoteBox(
modifier = RemoteModifier
.fillMaxWidth()
.padding(top = 8.rdp)
.clickable(hostAction("purchase".rs)),
) {
RemoteText("Purchase", fontSize = 18.rsp)
}
RemoteBox(
modifier = RemoteModifier
.fillMaxWidth()
.padding(top = 8.rdp)
.clickable(hostAction("cancel".rs)),
) {
RemoteText("Cancel", fontSize = 16.rsp)
}
}
}
}
```
> **Both file-level annotations are required on `androidx.compose.remote:1.0.0-alpha13`.**
> Lots of APIs are marked `@RestrictTo(LIBRARY_GROUP)` (e.g. `RemoteFloat.toRemoteString()`)
> or `@ExperimentalRemoteCreationComposeApi` (e.g. `RemoteText`, `RemoteBox`). Without
> these two lines at the top of the file you'll see errors like *"can only be called from
> within the same library group"* or *"This declaration is experimental"*. They'll likely
> relax in a later alpha — for now, every example file in this repo carries them.
### 5. Publish
```sh
GLYPH_API_KEY=rck_... ./gradlew :screens:glyphPublish
```
The bytes + contract land on the server. The producer is done — no
artefact is shared with the consumer.
---
## Consumer (renders screens)
### 6. Consumer Android app — `app/build.gradle.kts`
```kotlin
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("se.premex.glyph") version "0.3.5"
}
glyph {
appId = "your-app-id"
generatedPackage = "com.example.app.glyph" // pick a package you own
}
android {
defaultConfig { minSdk = 29 }
}
kotlin {
jvmToolchain(21)
}
```
No dependency on the producer's authoring module. The plugin's
codegen reads from the server's manifest. `generatedPackage` is
required — the plugin won't guess one for you.
### 7. Sync the manifest
```sh
./gradlew :app:glyphSync
```
Fetches `https://glyph.premex.se/v1/apps//manifest.json` and
writes a snapshot to `app/glyph.lock.json`. Commit it to version
control — like a lockfile, it pins which screens + contracts your
build sees, so CI is reproducible without network.
Re-run `glyphSync` whenever the producer ships changes.
### 8. Render
```kotlin
import com.example.app.glyph.Glyph // matches `generatedPackage`
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
RemoteComposeClient.install(context = this)
}
}
@Composable
fun App() {
Glyph.Checkout(
userName = "Stefan",
totalAmount = 49.99f,
callbacks = object : Glyph.CheckoutCallbacks {
override fun onPurchase() { /* navigate */ }
override fun onCancel() { /* navigate */ }
},
modifier = Modifier.fillMaxSize(),
)
}
```
`glyphGenerate` (run automatically before `compileKotlin`) reads
`glyph.lock.json` and emits `Glyph.(...)` accessors into your
`generatedPackage` — one parameter per typed slot, a
`Callbacks` interface for clicks. The accessor delegates to
the SDK, which fetches the bytes from the server at runtime with
ETag revalidation + disk cache.
---
# Platforms
Glyph hosts one binary `.rc` document per screen and one shared
manifest per app. Each platform's SDK speaks the same wire format and
contributes a host-native renderer.
| Platform | SDK | Renderer | Status |
|---|---|---|---|
| **Android** | `se.premex:glyph-sdk:0.3.5` | `androidx.compose.remote:remote-player-compose:1.0.0-alpha13` | shipping |
| **iOS** | `Glyph` Swift Package | rcX C++ engine + Skia-Ganesh-Metal | network + types ship; renderer wiring documented |
| **Web** | TypeScript player bundle (vendored at `/rc-player/bundle.js` on the portal) | Canvas2D | embedded in the portal as live previews |
| **KMP common** | `:sdk-kmp` | — (types + URL helpers only) | shipping JVM, native targets blocked on AGP+KMP plumbing |
| **JVM Desktop** | (planned) | Compose Multiplatform `:remote-player` from the experiments repo | path documented |
| **C++ / iOS native / macOS** | (planned) | rcX engine direct | upstream `players/cpp/` |
## Wire format — what's shared
All SDKs decode the same JSON manifest emitted by the server at
`/v1/apps//manifest.json`:
```json
{
"schemaVersion": 1,
"appId": "your-app-id",
"screens": [
{
"id": "checkout",
"version": 7,
"etag": "\"sha256-...\"",
"sizeBytes": 721,
"dimensions": { "width": 360, "height": 640 },
"contract": {
"actions": ["purchase", "cancel"],
"params": [
{ "id": "userName", "type": "string" },
{ "id": "totalAmount", "type": "float" }
]
},
"previewUrl": "https://.../checkout.png"
}
]
}
```
`.rc` bytes live at `/v1/apps//screens/.rc` and are
ETag-revalidated. Same bytes render identically on every player
(Android Compose, iOS Skia-Metal, browser Canvas2D, JVM desktop).
## Android (shipping)
```kotlin
plugins {
id("se.premex.glyph") version "0.3.5"
// …
}
glyph {
appId = "your-app-id"
generatedPackage = "com.example.app.glyph"
}
```
```kotlin
import com.example.app.glyph.Glyph
@Composable
fun App() {
Glyph.Checkout(
userName = "Stefan",
totalAmount = 49.99f,
callbacks = object : Glyph.CheckoutCallbacks {
override fun onPurchase() { /* … */ }
override fun onCancel() { /* … */ }
},
)
}
```
Runtime fetches `.rc` bytes from glyph.premex.se with ETag revalidation
+ disk cache; player Composable renders the document.
See [Getting started](/docs/getting-started) for the full Producer
+ Consumer walk-through.
## iOS (network ships, renderer pending)
Add via Swift Package Manager — point at this repo's `sdk-ios/`
directory or its eventual SPM mirror.
```swift
import Glyph
@main struct MyApp: App {
init() {
GlyphClient.configure(appId: "your-app-id")
}
var body: some Scene {
WindowGroup {
GlyphScreen(
screenID: "compose-checkout",
params: ["userName": "Stefan", "totalAmount": Float(49.99)],
onAction: { id in /* … */ }
)
}
}
}
```
`GlyphClient.configure(...)` initialises a singleton with `URLSession`
+ a per-app disk cache. `GlyphScreen` is a SwiftUI view that fetches
the bytes and hands them to the renderer.
The renderer currently shows a placeholder — to wire it up, build the
`librccore` / `librcskia` static libs from
[camaelon/remotecompose-experiments/players/cpp](https://github.com/camaelon/remotecompose-experiments/tree/main/players/cpp)
and follow `sdk-ios/README.md → Wiring the renderer`. Same Skia-Ganesh-Metal
backend the Android player uses, so visual parity is expected.
## Web (live previews on the portal)
The portal at embeds
the screen renderer directly. Each `ScreenCard` shows a live `