Getting Started with RemoteCompose: A Practical Guide (And When You'll Outgrow It)

RemoteCompose alpha07 just dropped with 16 API changes. Here's a hands-on tutorial to get you building β€” plus an honest look at where it stops and full SDUI starts.

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:

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:

build.gradle.kts Kotlin DSL
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:

HelloRemoteCompose.kt Kotlin
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:

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:

MainActivity.kt Kotlin
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:

CardBuilder.kt (server) Kotlin
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:

DynamicCardScreen.kt Kotlin
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:

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.

Debugging RemoteCompose What you get
// 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:

API Breakage: alpha05 β†’ alpha06 β†’ alpha07 Migration pain
// 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:

Comparison: RemoteCompose vs. Typed DSL Authoring experience
// 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:

Graduate to full SDUI when:

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:

  1. Pin your version. Don't use + or version ranges. Pin to 1.8.0-alpha07 and upgrade deliberately.
  2. Wrap the API. Create an abstraction layer so that when alpha08 breaks things (it will), you only fix one place.
  3. Keep scope small. Widgets and cards, not screens and flows.
  4. 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:

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 Assessment

Related Articles