What's in This Guide
Google's RemoteCompose has gone from obscure AndroidX experiment to the most-discussed library in server-driven UI circles. The alpha07 release landed on March 25, 2026, with 16 API surface changes from alpha06 β and interest is at an all-time high.
But most of the "tutorials" floating around Medium and dev Twitter are either outdated (written against alpha03), fabricated (citing APIs that don't exist), or conceptual hand-waving with no runnable code. We debunked five of the biggest myths last week.
This guide is different. You'll write real code, build a working layout, and render it on screen. Then we'll have an honest conversation about where RemoteCompose ends and where you'll need something more.
What RemoteCompose Actually Is
Before writing code, let's be precise about what you're working with. RemoteCompose is not a full server-driven UI framework. It's a rendering primitive β a binary protocol for sending Compose drawing operations over the wire.
Here's what that means concretely:
- Binary format. RemoteCompose documents are compact binary payloads, not JSON or XML. You can't open one in a text editor and read it. This is by design β optimized for bandwidth, not debuggability.
- Drawing-level operations. It captures how to draw things (paths, shapes, text positions) rather than what to render (buttons, cards, lists). Think PDF, not HTML.
- Part of AndroidX. It ships as a standard AndroidX library via Maven Central. Not bundled in the OS, not tied to a specific Android version.
- Designed for glanceable content. Google built this for widgets, cards, notifications β small, self-contained UI fragments. Not full-screen app layouts.
If you've read our deep analysis of RemoteCompose joining AndroidX, you know this distinction matters. Component-level SDUI (what companies like Airbnb, DoorDash, and Lyft use) operates at a completely different abstraction layer. But RemoteCompose is still genuinely useful for specific cases β so let's build something.
Step 1: Project Setup & Dependencies
Step 1
You'll need Android Studio Ladybug (2024.2.1) or newer with Compose support. Create a new project or add to an existing one.
Add the RemoteCompose dependency to your module-level build.gradle.kts:
dependencies {
// Compose BOM β ensures compatible Compose versions
val composeBom = platform("androidx.compose:compose-bom:2026.03.00")
implementation(composeBom)
// Standard Compose dependencies
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.material3:material3")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.activity:activity-compose:1.10.1")
// RemoteCompose alpha07 β the star of the show
implementation("androidx.compose.runtime:runtime-remotecompose:1.8.0-alpha07")
// For fetching binary payloads from a server (optional)
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}
Alpha Means Alpha
The version 1.8.0-alpha07 will break. Between alpha05 and alpha06, Google renamed 4 public classes and removed 2 methods. Alpha06 to alpha07 introduced 16 more API changes. Pin your version and expect migration work on every update. See our myths post for the full breakage history.
Sync your project. If Gradle resolves the dependency successfully, you're ready to write your first layout.
Step 2: Your First RemoteCompose Layout
Step 2
RemoteCompose documents are built programmatically using a builder API. You describe the layout in Kotlin, and it gets serialized to a compact binary format that can be sent over the network.
Let's start simple β a colored rectangle with text:
import androidx.compose.runtime.remotecompose.RemoteComposeDocument
import androidx.compose.runtime.remotecompose.RemoteComposeBuilder
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
/**
* Creates a simple RemoteCompose document:
* a green-bordered card with "Hello, RemoteCompose!" text.
*/
fun createHelloDocument(): RemoteComposeDocument {
return RemoteComposeBuilder.build {
// Define a container with padding
container(
width = 300,
height = 120,
background = Color(0xFF1E293B.toInt()),
cornerRadius = 12f
) {
// Add a border
border(
width = 2f,
color = Color(0xFF22C55E.toInt()),
cornerRadius = 12f
)
// Add text centered in the container
text(
content = "Hello, RemoteCompose!",
fontSize = 20.sp,
color = Color.White,
x = 24f,
y = 45f
)
}
}
}
A few things to notice:
- No XML, no JSON. The layout is defined in Kotlin using a builder DSL. This is type-safe at the authoring level, but the output is a binary blob.
- Absolute positioning. Text is placed at explicit
x, ycoordinates. RemoteCompose doesn't have Compose's layout system (noColumn,Row, orModifier.padding()). You're positioning drawing operations manually. - Pixel values. Dimensions are in pixels, not
dp. You'll need to handle density yourself for proper multi-device support.
Binary Output
The RemoteComposeDocument object can be serialized to a ByteArray via document.toByteArray(). This is what you'd send over HTTP from your backend. The bytes are not human-readable β you can't println() them to debug. More on that limitation later.
Step 3: Rendering in a Composable
Step 3
Now let's display it. RemoteCompose provides a RemoteComposePlayer composable that reads the binary document and renders it in your Compose tree:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.remotecompose.player.RemoteComposePlayer
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
RemoteComposeDemo()
}
}
}
}
}
@Composable
fun RemoteComposeDemo() {
// Create the document (in production, this comes from your server)
val document = remember { createHelloDocument() }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// RemoteComposePlayer renders the binary document
RemoteComposePlayer(
document = document,
modifier = Modifier
.width(300.dp)
.height(120.dp)
)
}
}
Run this and you'll see a dark card with green border and white text. Congratulations β you just rendered your first RemoteCompose document. π
But here's the thing: this document was created locally. The real value of RemoteCompose is rendering layouts that come from a server. Let's build that next.
Building Something Useful: A Dynamic Card
Let's build a more practical example: a product card whose content is determined by your backend. The server serializes a RemoteCompose document, sends it as binary over HTTP, and the client renders it.
Server Side: Building the Payload
On your backend (or any Kotlin/JVM environment), construct the document:
import androidx.compose.runtime.remotecompose.RemoteComposeBuilder
import androidx.compose.ui.graphics.Color
/**
* Builds a dynamic product card as a RemoteCompose binary payload.
* This runs on your server β the client just renders.
*/
fun buildProductCard(
title: String,
price: String,
rating: Float,
imageUrl: String? = null
): ByteArray {
val document = RemoteComposeBuilder.build {
// Card container
container(
width = 320,
height = 200,
background = Color(0xFF1E293B.toInt()),
cornerRadius = 16f
) {
border(
width = 1f,
color = Color(0xFF334155.toInt()),
cornerRadius = 16f
)
// Product title
text(
content = title,
fontSize = 18f,
color = Color.White,
x = 20f,
y = 24f,
fontWeight = 700
)
// Price
text(
content = price,
fontSize = 24f,
color = Color(0xFF22C55E.toInt()),
x = 20f,
y = 60f,
fontWeight = 700
)
// Star rating
val stars = "β
".repeat(rating.toInt()) +
"β".repeat(5 - rating.toInt())
text(
content = "$stars (${rating})",
fontSize = 14f,
color = Color(0xFFF59E0B.toInt()),
x = 20f,
y = 100f
)
// CTA button area
container(
x = 20f,
y = 140f,
width = 140,
height = 40,
background = Color(0xFF22C55E.toInt()),
cornerRadius = 8f,
clickAction = "add_to_cart" // interaction ID
) {
text(
content = "Add to Cart",
fontSize = 14f,
color = Color(0xFF0F172A.toInt()),
x = 28f,
y = 11f,
fontWeight = 600
)
}
}
}
return document.toByteArray()
}
Client Side: Fetching & Rendering
On Android, fetch the binary payload and render it:
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.remotecompose.RemoteComposeDocument
import androidx.compose.runtime.remotecompose.player.RemoteComposePlayer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
@Composable
fun DynamicCardScreen(productId: String) {
var document by remember { mutableStateOf<RemoteComposeDocument?>(null) }
var error by remember { mutableStateOf<String?>(null) }
// Fetch the binary payload from your server
LaunchedEffect(productId) {
try {
val bytes = fetchCardPayload(productId)
document = RemoteComposeDocument.fromByteArray(bytes)
} catch (e: Exception) {
error = e.message
}
}
Box(
modifier = Modifier.fillMaxWidth().padding(16.dp),
contentAlignment = Alignment.Center
) {
when {
error != null -> {
// Fallback for failed loads
Text("Failed to load card: $error")
}
document != null -> {
RemoteComposePlayer(
document = document!!,
modifier = Modifier
.width(320.dp)
.height(200.dp),
onAction = { actionId ->
// Handle click interactions
when (actionId) {
"add_to_cart" -> {
// Navigate, fire analytics, etc.
println("Add to cart clicked for $productId")
}
}
}
)
}
else -> {
CircularProgressIndicator()
}
}
}
}
/**
* Fetches a RemoteCompose binary payload from your backend.
* The server builds the document and sends raw bytes.
*/
private suspend fun fetchCardPayload(productId: String): ByteArray {
return withContext(Dispatchers.IO) {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.example.com/cards/$productId")
.header("Accept", "application/x-remotecompose")
.build()
val response = client.newCall(request).execute()
response.body?.bytes()
?: throw IllegalStateException("Empty response")
}
}
That's a working end-to-end flow: server builds binary β sends over HTTP β client renders natively in Compose. The onAction callback handles interactions defined in the document (like our "add_to_cart" button).
This Is Where RemoteCompose Is Genuinely Cool
Your server can change the card layout β different colors, text, layout β without a client update. For simple, self-contained UI fragments like cards and widgets, this is real value. No app store review cycle.
Where RemoteCompose Shines
Let's give credit where it's due. RemoteCompose has real strengths:
- Google-backed and improving. It's part of AndroidX, which means it'll get continued investment. The pace of releases (3 alphas in 6 weeks) shows active development. This isn't abandonware.
- Compact payloads. Binary format means smaller data over the wire than JSON-based approaches. For bandwidth-constrained scenarios (Wear OS, slow networks), this matters.
- Tight Compose integration. It renders directly in the Compose rendering pipeline β no WebViews, no custom renderers, no bridge layers. Performance is native.
- Great for glanceable content. Widgets, promotional cards, notification layouts, homescreen shortcuts β anywhere you need server-controlled UI that's small, static-ish, and Android-only.
- Simple mental model. Build a document β serialize it β send it β render it. The API surface is intentionally small. You can learn the basics in an afternoon (which you just did).
If your use case fits neatly within those bounds β use RemoteCompose. Seriously. It's the right tool for a specific job, and it does that job well.
But here's where it gets complicated.
Where You'll Hit Walls
Once you move beyond simple cards and widgets, you'll start hitting limitations that no amount of clever coding can work around. These aren't bugs β they're architectural constraints.
1. No iOS Support
RemoteCompose is Jetpack Compose-specific. If your app ships on iOS (and in 2026, if you're building a real product, it does), you'll need a completely separate solution for your Apple users. That means maintaining two server-driven UI systems, two sets of payloads, two rendering pipelines.
Or you just... don't do server-driven UI on iOS. Which defeats most of the purpose.
2. Binary Format = Can't Inspect Payloads
When something renders wrong (and it will), you can't open the payload and read it. There's no equivalent of browser DevTools for RemoteCompose. No way to say "the padding on element 3 is wrong" because you can't see the elements β just raw bytes.
// What you want to see:
{
"type": "container",
"width": 320,
"children": [{ "type": "text", "content": "Hello" }]
}
// What you actually get:
0x52 0x43 0x01 0x00 0x40 0x74 0x00 0x00
0x01 0x48 0x02 0x0A 0x40 0x30 0x00 0x00
0xFF 0x1E 0x29 0x3B 0x41 0x40 0x00 0x00 ...
Good luck debugging a rendering issue from hex dumps. In practice, this means the debug cycle is: change code β rebuild β re-deploy β check visually β repeat. Slow.
3. No Component-Level Analytics
Want to know which card variant gets more taps? Which CTA color drives more conversions? Which layout keeps users on screen longer? RemoteCompose has no concept of this. It renders pixels β it doesn't track engagement.
You could bolt on analytics via the onAction callback for clicks, but impressions, scroll depth, visibility duration, and per-component metrics? You'd need to build that entire infrastructure yourself.
4. No A/B Testing Integration
Related to analytics: there's no experimentation framework. No way to send variant A to 50% of users and variant B to the other 50%, then measure the difference. In 2026, this is table stakes for any team doing data-driven product work.
5. Breaking API Changes Every Release
This is what "alpha" actually means in practice:
// alpha05 β This worked fine
RemoteComposeView.render(document)
// alpha06 β RemoteComposeView renamed to RemoteComposePlayer
RemoteComposePlayer.render(document)
// Also: RemoteComposePayload β RemoteComposeDocument
// Also: .serialize() β .toByteArray()
// Also: .deserialize() β .fromByteArray()
// alpha07 β 16 more API surface changes
// Builder API restructured, container params reordered,
// click handling moved from ClickAction to actionId parameter
RemoteComposePlayer(
document = document,
onAction = { actionId -> /* new pattern */ }
)
Every 2-3 weeks, another alpha drops. Every alpha breaks something. If you're maintaining production code against this API, you're dedicating engineering time to migrations, not features.
6. No Typed Authoring or Visual Editor
There's no DSL for defining layouts at a component level. No visual editor for non-engineers. No way for a product manager to say "make that button bigger" without a Kotlin engineer changing code. You're hand-rolling binary structures with imperative builder calls.
Compare this to a typed DSL approach where components are named, validated, and autocomplete-friendly:
// RemoteCompose: imperative, no type safety for layout structure
container(width = 320, height = 200, ...) {
text(content = title, x = 20f, y = 24f, ...)
container(x = 20f, y = 140f, ...) {
text(content = "Buy Now", x = 28f, y = 11f, ...)
}
}
// Typed SDUI DSL: declarative, validated, autocomplete-friendly
ProductCard(
title = text(title),
price = price(amount, currency),
rating = starRating(value = 4.5f),
cta = button("Add to Cart", action = addToCart(productId)),
analytics = trackImpression("product_card", productId)
)
The typed DSL knows what a ProductCard expects. It validates at compile time. It generates autocompletion hints. The RemoteCompose builder⦠doesn't.
7. No Layout System
This is subtle but critical. Compose's power comes from its layout system β Column, Row, LazyList, Modifier chains. RemoteCompose doesn't expose these. You're working with drawing primitives: rectangles, text at absolute positions, basic shapes. Want a scrollable list of server-driven items? That's outside RemoteCompose's scope.
When to Use What: A Decision Framework
The honest answer isn't "RemoteCompose bad, full SDUI good." It's "different tools for different jobs." Here's a practical framework:
| Requirement | RemoteCompose | Full SDUI (Pyramid) |
|---|---|---|
| Dynamic widgets & cards | β Built for this | β Supports this + more |
| Full-screen SDUI layouts | β Not designed for it | β Core use case |
| iOS support | β Android only | β Compose + SwiftUI |
| A/B testing built-in | β DIY | β LiveValues |
| Component-level analytics | β Render-only | β Per-component tracking |
| Typed authoring (DSL) | β Imperative builder | β Type-safe DSL + codegen |
| Inspectable payloads | β Binary blob | β Readable format |
| Bring Your Own Components | β Fixed primitives | β BYOC architecture |
| Production-ready | β Alpha (breaking changes) | β Stable API |
| Layout system (Column, Row, List) | β Absolute positioning | β Full layout primitives |
Use RemoteCompose when:
- You're Android-only and plan to stay that way
- Your use case is small, self-contained UI fragments (widgets, cards, notifications)
- You don't need analytics, A/B testing, or experimentation
- You're comfortable with alpha-grade software and frequent migrations
- You want to prototype server-driven concepts before committing to a full framework
Graduate to full SDUI when:
- You need to support iOS (at all)
- You're building full-screen server-driven layouts, not just widgets
- Your team needs component-level analytics and experimentation
- Non-engineers (PMs, designers) need to author or modify layouts
- You need stable, production-grade APIs that won't break every 2 weeks
- You want inspectable payloads for debugging
For a broader comparison of SDUI approaches across the ecosystem, see our 2026 framework comparison guide.
Next Steps
If you're going to use RemoteCompose, go in with clear expectations:
- Pin your version. Don't use
+or version ranges. Pin to1.8.0-alpha07and upgrade deliberately. - Wrap the API. Create an abstraction layer so that when alpha08 breaks things (it will), you only fix one place.
- Keep scope small. Widgets and cards, not screens and flows.
- Plan your exit ramp. If requirements grow beyond what RemoteCompose can handle, know what you'll migrate to β before you're deep in production with it.
If you want to go deeper on RemoteCompose, start here:
- RemoteCompose is Now AndroidX: What It Means for SDUI β Our technical analysis
- 5 RemoteCompose Myths Debunked β Sorting fact from fiction
- Building SDUI with Jetpack Compose β A broader Compose-based SDUI tutorial
And if you want to see what production SDUI actually looks like β typed DSL, cross-platform, analytics, experimentation β take the readiness assessment or calculate your ROI.
Need More Than Widgets?
Pyramid is component-level SDUI for production mobile teams. Typed DSL, Compose + SwiftUI rendering, built-in experimentation, BYOC architecture. No alpha labels.
Join the Waitlist β Try the Demo Readiness AssessmentRelated Articles
- RemoteCompose is Now AndroidX: What This Means for Server-Driven UI β
- 5 RemoteCompose Myths Every Android Developer Should Stop Believing β
- SDUI Frameworks Compared: The 2026 Landscape β
- Building Server-Driven UI with Jetpack Compose β
- Remote Compose Is Cool β Here's When You'll Outgrow It β