Getting Started with Pyramid: Add Server-Driven UI to Your Android App in 30 Minutes
This tutorial walks you through adding server-driven UI to an existing Jetpack Compose app using Pyramid. By the end, you'll be deploying screen changes from your backend — no app release required.
Prerequisites
- An Android app using Jetpack Compose (Compose 1.5+)
- Kotlin 2.0+
- A Pyramid account (sign up for early access)
What We'll Build
We'll take an existing ProductCard component and make it server-drivable. Then we'll build a home screen from the backend using Pyramid's generated DSL.
Hardcoded layout, requires app release to change.
Backend-defined layout, deploy in seconds.
Step 1: Add the Pyramid SDK
Add Pyramid's Android SDK to your app's build.gradle.kts:
// build.gradle.kts (app module)
plugins {
id("com.google.devtools.ksp") version "2.0.0-1.0.22"
}
dependencies {
implementation("dev.pyramid:sdk:1.0.0")
ksp("dev.pyramid:ksp-compiler:1.0.0")
}
Sync your project. The KSP compiler will scan for @PyramidBlock annotations at build time.
Step 2: Annotate Your Components
Take any Compose component you want to make server-drivable and add @PyramidBlock:
import dev.pyramid.sdk.annotations.PyramidBlock
import dev.pyramid.sdk.annotations.PyramidProp
import dev.pyramid.sdk.annotations.PyramidEvent
@PyramidBlock(keyType = "PRODUCT_CARD")
@Composable
fun ProductCard(
@PyramidProp(required = true) title: String,
@PyramidProp(required = true) imageUrl: String,
@PyramidProp price: Double = 0.0,
@PyramidProp badge: String? = null,
@PyramidEvent onTap: () -> Unit = {},
@PyramidEvent onAddToCart: () -> Unit = {}
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onTap)
) {
Column(modifier = Modifier.padding(16.dp)) {
AsyncImage(
model = imageUrl,
contentDescription = title,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = title, style = MaterialTheme.typography.titleMedium)
if (badge != null) {
Badge(text = badge)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "$${price}",
style = MaterialTheme.typography.titleLarge
)
IconButton(onClick = onAddToCart) {
Icon(Icons.Default.ShoppingCart, "Add to cart")
}
}
}
}
}
That's it. Your component code doesn't change at all — you're just adding metadata annotations.
What Pyramid Extracts
When you build, KSP generates a schema from your annotations:
{
"keyType": "PRODUCT_CARD",
"name": "ProductCard",
"props": {
"title": { "type": "STRING", "required": true },
"imageUrl": { "type": "STRING", "required": true },
"price": { "type": "FLOAT", "default": 0.0 },
"badge": { "type": "STRING" }
},
"events": {
"onTap": {},
"onAddToCart": {}
}
}
This schema is the contract between your mobile app and your backend.
Step 3: Initialize Pyramid
In your Application class:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
Pyramid.init(
context = this,
config = PyramidConfig(
serverUrl = "https://api.pyramid.dev",
apiKey = BuildConfig.PYRAMID_API_KEY
)
)
}
}
Step 4: Sync Your Schemas
Install the Pyramid CLI and sync your component schemas to the server:
# Install CLI
npm install -g @pyramid/cli
# Login
pyramid login --api-key your_api_key
# Initialize (creates pyramid.json in project root)
pyramid init
# Sync schemas (after building your app with KSP)
pyramid sync
Output:
✓ Schema bundle v1.0.0 created (checksum: a1b2c3d4)
✓ Code generation started (run_id: run_abc123)
✓ Generated DSL package: dev.pyramid.yourorg:dsl:1.0.0
Step 5: Build Screens from Your Backend
Now your backend team can compose screens using your actual components:
Option A: Kotlin DSL (for Kotlin/JVM backends)
// build.gradle.kts (backend)
dependencies {
implementation("dev.pyramid.yourorg:dsl:1.0.0")
}
// Usage — full autocomplete and type safety!
fun buildHomeScreen(): PyramidScreen {
return pyramid {
screen("home") {
title("Featured Products")
column(gap = 16) {
productCard(
title = "Wireless Headphones",
imageUrl = "https://cdn.example.com/headphones.jpg",
price = 79.99,
badge = "New",
onTap = navigate("/products/headphones"),
onAddToCart = api("/cart/add", method = "POST", body = mapOf(
"productId" to "headphones-001"
))
)
productCard(
title = "USB-C Hub",
imageUrl = "https://cdn.example.com/hub.jpg",
price = 49.99,
onTap = navigate("/products/hub")
)
}
}
}
}
Option B: TypeScript SDK (for Node.js backends)
import { screen, action } from '@pyramid/backend-sdk';
import { ProductCard } from './generated/components';
const homeScreen = screen('home')
.title('Featured Products')
.body(
ProductCard.create({
title: 'Wireless Headphones',
imageUrl: 'https://cdn.example.com/headphones.jpg',
price: 79.99,
badge: 'New',
onTap: action.navigate('/products/headphones'),
onAddToCart: action.api('/cart/add', 'POST', {
productId: 'headphones-001'
}),
}),
ProductCard.create({
title: 'USB-C Hub',
imageUrl: 'https://cdn.example.com/hub.jpg',
price: 49.99,
onTap: action.navigate('/products/hub'),
})
)
.build();
// Serve via Express
app.get('/screens/home', (req, res) => {
res.json(homeScreen);
});
Step 6: Render Server Screens in Your App
Replace your hardcoded screen with Pyramid's renderer:
@Composable
fun HomeScreen() {
// This fetches and renders the server-defined screen
PyramidScreen(
screenId = "home",
loading = { CircularProgressIndicator() },
error = { error ->
Text("Failed to load: ${error.message}")
}
)
}
That's it. When your backend changes the screen definition, the app renders the new version — no release needed.
Step 7: Deploy a Change
Let's say marketing wants to add a "Sale" badge to the headphones. Your backend developer updates one line:
Deploy the backend. Users see the change in seconds. No app store review. No waiting.
Bonus: A/B Test a Screen
Pyramid's Live Values let you experiment on any property:
// Backend
fun buildHomeScreen(userId: String): PyramidScreen {
return pyramid {
screen("home") {
// Live Value — 50% of users see each variant
val ctaText = liveValue("home_cta_text",
default = "Shop Now",
variants = mapOf("variant_b" to "Browse Deals")
)
column {
// ... products
button(
text = ctaText,
style = "primary",
onTap = navigate("/shop")
)
}
}
}
}
Pyramid automatically tracks which variant each user sees and measures engagement. No extra analytics code needed.
What's Next?
- Add more components: Annotate your headers, cards, lists, forms
- Set up workflows: Chain screens into multi-step flows (onboarding, checkout)
- Invite your team: PMs can use the visual editor to compose screens
- Go cross-platform: Add the iOS SDK for SwiftUI support
FAQ
Does this slow down my app?
No. Pyramid renders your actual native Compose components. There's no WebView, no interpretation overhead. The JSON is parsed once and rendered natively.
What if the server is down?
Pyramid caches the last successful response. Your app always has a fallback. You can also bundle default screens in your APK.
Can I use this for just part of my app?
Absolutely. Most teams start with one or two screens (home, onboarding, promotions) and expand over time. Pyramid works alongside your regular Compose code.
How does this compare to Jetpack Compose Navigation?
They're complementary. Pyramid handles what a screen shows; Compose Navigation handles how you get there. Pyramid integrates with your existing navigation setup.
What about offline support?
Pyramid's SDK includes local caching. Screens load from cache when offline and update when connectivity returns.
Related Articles
Ready to Add SDUI to Your App?
Pyramid helps mobile teams implement Server-Driven UI with their existing Jetpack Compose components. Bring your own components — we handle the infrastructure.
Join the Waitlist