How WARP Works Under the Hood¶
This document dives deep into the technical architecture, compilation pipeline, abstract syntax tree (AST), serialization protocol, and native platform execution engine behind WARP (Widget Abstraction Rendering Pipeline).
Technical Overview¶
At its core, WARP separates UI description from UI rendering.
Instead of rendering canvas pixels or binding directly to platform views during composition, WARP compiles declarative @Composable code into a lightweight, serializable Abstract Syntax Tree (AST) called WarpNode. This tree is encoded into JSON and handed off to native renderers on Android and iOS.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Kotlin Multiplatform Code โ
โ @Composable Content() โโ> Builds In-Memory WarpNode AST โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโดโโโโโโโโโโโโโ
โ kotlinx.serialization โ
โ JSON Serialization โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโ
โ Android Glance Host โ โ iOS WidgetKit Host โ
โ 1. WarpGlanceWidgetReceiver โ โ 1. WarpWidgetBridge (spm4Kmp) โ
โ 2. AST โโ> Glance Composables โ โ 2. App Group UserDefaults โ
โ 3. System RemoteViews compilation โ โ 3. WarpSwiftUIRenderer.swift (SwiftUI)โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1. Modular Architecture Breakdown¶
WARP is structured into decoupled, single-responsibility modules:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ warp-widget โ
โ ยท WarpWidget<S> (State, ID, Default State, Content) โ
โ ยท WarpWidgetSession (Environment, PlatformContext, ID) โ
โ ยท WarpWidgetHost (compose / JSON / dispatch / prepare) โ
โ ยท WarpTheme & WarpAdaptive size bucketing โ
โโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ warp-runtime โ โ warp-ui โ
โ ยท Compose Compiler DSL โ โ ยท Glance AST Renderer โ
โ ยท WarpNode AST Data Classes โ โ ยท SwiftUI JSON Generator โ
โ ยท Recomposer Frame Clock โ โ ยท WarpClickHandler โ
โ ยท WarpModifier Chain โ โ ยท WarpClicksRegistry โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โผ
warpWidgetKit (SPM)
SwiftUI & AppIntent Bridge
warp-runtime: Provides the@ComposableDSL (WarpColumn,WarpText,WarpButton), theWarpNodeAST tree definition, theWarpModifierchain, and the in-memoryRecomposerengine.warp-ui: Houses platform renderers that mapWarpNodetrees to Jetpack Glance composables on Android, and generate JSON payloads for SwiftUI on iOS.warp-widget: DefinesWarpWidget<S>,WarpWidgetSession, persistent state stores (WarpWidgetStateStore), theme resolution (WarpTheme), and adaptive size bucketing (WarpAdaptive).warpWidgetKit: A standalone Swift Package Manager (SPM) library providing SwiftUI renderers (WarpSwiftUIRenderer.swift) and iOS 17+ interactiveAppIntenthandlers (WarpClickAppIntent).
2. Phase 1: Composition to AST (warp-runtime)¶
When you write a WARP UI using @Composable primitives, WARP uses Compose Runtime (without Compose UI, Foundation, or Material dependencies):
val json = composeWarpToJson(CounterState(count = 42)) { state ->
WarpColumn(modifier = WarpModifier.padding(16.dp)) {
WarpText("Counter")
WarpRow {
WarpButton(text = "โ", onClick = CounterActions.Decrement.asClickAction())
WarpText("${state.count}")
WarpButton(text = "+", onClick = CounterActions.Increment.asClickAction())
}
}
}
How composeWarp Executes:¶
- Root Creation: Initializes a temporary
RootHolderbucket. - Recomposer Frame Clock: Uses a headless
RecomposerandBroadcastFrameClockto execute composable functions. If localmutableStateOfchanges occur during execution, WARP drives recomposition passes until the tree settles. - CompositionLocal Parent Tracking: An internal
CompositionLocaltracks current parent containers (WarpColumnHolder,WarpRowHolder,WarpBoxHolder). As composables run, they register child holders into their parent's children list. - AST Conversion: Once composition completes,
root.toWarpNode()transforms temporary internal holders into an immutable, serializableWarpNodetree.
3. Phase 2: Serialization (WarpNode JSON)¶
The resulting WarpNode tree is a hierarchy of @Serializable Kotlin data classes. kotlinx.serialization serializes the tree using a polymorphic class discriminator ("type"):
{
"type": "column",
"modifier": {
"elements": [
{ "type": "padding", "start": 16, "end": 16, "top": 16, "bottom": 16 }
]
},
"children": [
{ "type": "text", "text": "Counter" },
{
"type": "row",
"children": [
{
"type": "button",
"text": "โ",
"onClick": { "type": "click", "actionId": "decrement", "parameters": {} }
},
{ "type": "text", "text": "42" },
{
"type": "button",
"text": "+",
"onClick": { "type": "click", "actionId": "increment", "parameters": {} }
}
]
}
]
}
4. Phase 3: Native Platform Rendering (warp-ui)¶
Because Android and iOS enforce strict system boundaries for home screen widgets, WARP delegates visual layout to true native framework elements on each OS.
Android Rendering (Jetpack Glance)¶
On Android, home screen widgets are hosted inside system RemoteViews:
- Receiver Broadcast:
WarpGlanceWidgetReceiverreceivesACTION_APPWIDGET_UPDATEorUI_MODE_CHANGEDbroadcasts. - State & Environment: Reads
CounterStatefromSharedPreferencesviaWarpWidgetStateStoreand resolvesWidgetEnvironment. - Glance Mapping:
WarpRenderrecursively convertsWarpNodeobjects into Jetpack Glance composables: WarpNode.Column\(\rightarrow\)androidx.glance.layout.ColumnWarpNode.Row\(\rightarrow\)androidx.glance.layout.RowWarpNode.Box\(\rightarrow\)androidx.glance.layout.BoxWarpNode.Text\(\rightarrow\)androidx.glance.text.TextWarpNode.Button\(\rightarrow\)androidx.glance.Button- RemoteViews Compilation: Jetpack Glance compiles the layout into Android system
RemoteViews.
iOS Rendering (WidgetKit & SwiftUI)¶
iOS extensions run in isolated background processes and expect native SwiftUI views:
- State Store Sync: Calling
updateWarpWidgetStatein KMP updates App GroupNSUserDefaults("group.com.company.app"). - Timeline Trigger: KMP calls
WarpWidgetBridge.shared.reloadTimelinesOfKind("CounterWidget")to notify iOSWidgetCenter. - JSON Evaluation: WidgetKit invokes
CounterWidgetEntryView. SwiftUI queries Kotlin viacomposeWidgetJson(...)to generate the latestWarpNodeAST JSON string. - SwiftUI Parsing:
WarpSwiftUIRenderer.swift(insidewarpWidgetKit) parses the AST JSON at render time and builds a native SwiftUI view tree: column\(\rightarrow\)VStackrow\(\rightarrow\)HStackbox\(\rightarrow\)ZStacktext\(\rightarrow\)Textbutton\(\rightarrow\)Button(intent: CounterWidgetClickIntent(...))
5. Phase 4: Action Dispatch & Cold Starts¶
Widgets cannot execute live Kotlin lambdas on button clicks because taps must survive process restarts and OS sleep cycles.
Common UI Host (Android / iOS)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
WarpButton( Native renderer reads WarpButton.onClick
onClick = CounterActions โโ> Wires native button tap with actionId
.Increment.asClickAction() User Taps Button
) โ
โ โผ
โผ Glance ActionCallback / AppIntent
JSON: { onClick: { โ
type: "click", โผ
actionId: "increment" WarpClicksRegistry.dispatch(
}} actionId, parameters
)
โ
โผ
WarpClickHandler.onClick(action)
โ
โผ
updateWarpWidgetState { ... }
โ
โผ
Reload Widget Timelines
Cold-Start Handling:¶
- Android: Tapping a button fires a Glance
ActionCallback(WarpRegistryActionCallback). If the main app process is dead, Android initializes background workers, registers widget receivers, dispatchesactionId+parametersthroughWarpClicksRegistry, updates state inSharedPreferences, and callsGlanceAppWidget.update(). - iOS 17+: Tapping a button fires a Swift
AppIntent(WarpClickAppIntent). WidgetKit launches the extension target, initializes KMP viaWarpWidgetHost.shared.prepare(...), dispatches the click toWarpClickHandler, mutatesNSUserDefaults, and callsWidgetCenter.shared.reloadTimelines(...).
6. Beyond Widgets: The "Compose-Native" Vision ๐ก¶
WARP proves an exciting architectural concept: Writing declarative UI with Compose syntax in Kotlin, compiling it to a clean serializable AST, and rendering true 100% native platform views on both platforms.
A Food-For-Thought Question for Curious Minds:¶
If we can compile Compose functions into a serializable AST that renders native Glance RemoteViews on Android and native SwiftUI views on iOS for home screen widgets...
Could the exact same architecture be expanded to build full mobile apps via
Compose-Native?
Imagine a future cross-platform framework where: - You write 100% shared Compose code in Kotlin Multiplatform. - Instead of drawing pixels on a Skia/OpenGL canvas (like Compose Multiplatform), the engine translates AST nodes into native UIKit / SwiftUI views on iOS and native Jetpack Compose / Android Views on Android. - App buttons, navigation stacks, text fields, and scroll views use native OS animation engines, accessibility APIs, and platform themes automatically!
Could this AST pipeline be the key to achieving true native platform look-and-feel from a single Compose codebase? ๐