Architecture and lifecycle

← Documentation · Next: State →

Mental model

BDUI has five core concepts:

ConceptResponsibility
Widget treeRecursive JSON layout. Containers own children; leaves do not
StateJSON attached to root or individual widget nodes
ActionSerializable instruction for a mutation, request, host effect, or group
StoreOwns the current tree and commits all mutations atomically
ThemeSemantic tokens layered from config, host app, and optional backend overrides
Bdui.init(config)
        │
BduiView(initialJson?, streamUrl?)
        │
        ├── JSON snapshot ──┐
        └── Ktor SSE ───────┤
                            ▼
                    BduiActionRuntime
                  serialized commit queue
                            │
              ┌─────────────┴─────────────┐
              ▼                           ▼
       BduiStore mutation           BduiEffectBus
              │                           │
              ▼                           ▼
     Compose recomposition       host one-shot effect
              │
              ▼
       observer evaluation

All mutations—UI events, observers, HTTP responses, and SSE events—use the same runtime. Network callbacks and custom handlers never mutate BduiStore directly. State, tree, and backend theme writes are patch (RFC 6902 plus BDUI node roots and merge).

Embedding a view

BduiView is a Compose composable. Place it anywhere the host already composes: a full destination, a tab, a card, a list header, a bottom sheet, or beside native widgets. It is not a navigation destination and does not own the back stack.

Call Bdui.init once at app start with the default BduiConfig. Each view may pass an optional config to override it.

Bdui.init(
    BduiConfig {
        httpClient = appHttpClient
        navigator = appNavigator
        notifier = appNotifier
        theme = AppBduiTheme
    }
)
@Composable
fun BduiView(
    config: BduiConfig? = null,
    initialJson: String? = null,
    streamUrl: String? = null,
    modifier: Modifier = Modifier,
)

Omitted config uses the instance from Bdui.init. Missing both Bdui.init and config is invalid.

Column {
    Text("Native title")
    BduiView(
        initialJson = payload,
        modifier = Modifier.fillMaxWidth(),
    )
    Button(onClick = onHostCta) { Text("Native action") }
}

JSON, stream, or both:

BduiView(initialJson = payload)
BduiView(streamUrl = "/v1/bdui/offer/42/stream")
BduiView(initialJson = shell, streamUrl = stream)
InputInitial behavior
initialJson onlyDecode and display immediately
streamUrl onlyStay loading until a snapshot or a patch that replaces $root arrives
BothDisplay the shell immediately, then apply stream actions
NeitherInvalid initialization; the host shows an error in that view's slot

Each BduiView owns an isolated store and runtime. The default BduiConfig from Bdui.init is shared. Several views may appear on the same host screen; pass config only when a view needs different registries, theme, or HTTP.

LazyColumn {
    item {
        BduiView(
            initialJson = heroJson,
            modifier = Modifier.fillMaxWidth(),
        )
    }
    items(nativeRows) { NativeRow(it) }
    item {
        BduiView(
            initialJson = reviewsShell,
            streamUrl = reviewsStream,
            modifier = Modifier.fillMaxWidth(),
        )
    }
}

Leaving composition disposes that view's runtime and SSE. Host navigation (navigate) is an effect the app implements for native destinations. Full-screen BDUI destinations use BduiScreenHost and openScreen.

Full-screen hosts

BduiView never owns a back stack. BduiScreenHost does: it renders an embeddable root view, then presents each openScreen destination as a window-filling screen with its own store, runtime, optional initialJson, and optional streamUrl.

@Composable
fun BduiScreenHost(
    config: BduiConfig? = null,
    initialJson: String? = null,
    streamUrl: String? = null,
    modifier: Modifier = Modifier,
)

Place it at the activity/window root (or anywhere a full-screen overlay is acceptable). The opened screen covers the entire app window; it is not clipped to the host's embedded slot.

BduiScreenHost(
    initialJson = payload,
    streamUrl = stream,
    modifier = Modifier.fillMaxSize(),
)

Each pushed screen is isolated like a separate BduiView. Closing it with popScreen (or system back / dismiss when no dialog overlay is open) delivers an optional JSON result to the caller's onResult as $event.

Config

BduiConfig is the integration surface. The app supplies a default with Bdui.init and may pass another instance into a specific BduiView.

Bdui.init(
    BduiConfig {
        httpClient = appHttpClient
        navigator = appNavigator
        notifier = appNotifier

        theme = AppBduiTheme
        backendThemePolicy = BduiThemePolicy(
            allowOverrides = true,
            allowedSections = setOf(
                "colors", "typography", "shapes", "spacing", "components",
            ),
            allowedFontFamilies = setOf("brand", "system"),
        )

        widgets.register("acme:Map", AcmeMapRenderer)
        actions.register("acme:favoriteOffer", FavoriteOfferHandler)
    }
)

BduiView(
    initialJson = payload,
    streamUrl = streamUrl,
    modifier = Modifier.fillMaxWidth(),
)

The config owns:

  • the authenticated Ktor HttpClient;
  • optional BduiAppsana endpoint for POST SSE transforms;
  • host navigation, dialog overlays, notifications, clipboard, sharing, and URL handling;
  • theme defaults and backend-override policy;
  • custom widget and action registries;
  • optional sensitiveData extra paths (unioned with the document's root sensitiveData);
  • JavaScript execution policy;
  • client spec version;
  • optional persistence/checkpoint provider.

Registries belong to that BduiConfig, not the process. Tests pass an isolated config into BduiView or Bdui.init.

Widget document

Every widget uses the same envelope:

{
  "id": "offer-42",
  "type": "Card",
  "state": { "liked": false },
  "modifier": [
    { "name": "fillMaxWidth" },
    { "name": "paddingHorizontal", "value": "16" }
  ],
  "onClick": { "type": "navigate", "screen": "offer" },
  "params": {
    "enabled": true,
    "visible": true,
    "variant": "elevated"
  },
  "observers": [],
  "children": []
}
FieldTypeRequiredMeaning
typestringyesCase-sensitive built-in or registered widget name
idstringnoStable identity and named state/tree target. Root defaults to root
stateobjectnoState owned by this node
modifiermodifier[]noLayout modifiers, applied left-to-right. See themes
onClickactionnoMakes this widget clickable. Absent means not clickable
paramsobjectnoWidget-specific params plus common visible / enabled
childrenarraycontainers onlyChild widgets. Omit for leaves
topBar / bottomBar / floatingActionButtonwidgetScaffold onlyNamed layout slots; same widget envelope as children
observersarraynoState and error observers declared on this node
themeobjectroot onlyRestricted theme-token override
sensitiveDatastring[]root onlyPatch paths whose values are tokenized before an AppSana transform. See streaming
minSpecintegernoSkip this node on older clients

modifier and onClick are envelope fields, not params. Any widget type may set onClick; BDUI applies clickable. params.enabled: false suppresses it. A child onClick consumes the click so ancestors do not also fire.

Unknown fields are ignored. Unknown or too-new widgets and their subtrees are skipped and logged; they do not crash, display a BDUI placeholder, or produce a toast.

The Kotlin model:

@Serializable
data class BduiWidgetModel(
    val type: String,
    val id: String? = null,
    val state: JsonObject? = null,
    val modifier: List<BduiModifier> = emptyList(),
    val onClick: BduiAction? = null,
    val params: Map<String, JsonElement> = emptyMap(),
    val children: List<BduiWidgetModel> = emptyList(),
    val observers: List<BduiObserver> = emptyList(),
    val theme: BduiThemeOverride? = null,
    val minSpec: Int? = null,
    val sensitiveData: List<String> = emptyList(),
)

Rendering

BduiView owns a view-scoped store and runtime. It renders loading until a root exists, then recursively dispatches by type.

Host layout
  └── BduiView
        └── BduiWidget(root)
              ├── resolve state/theme paths
              ├── build Modifier from envelope `modifier`
              ├── apply clickable from envelope `onClick`
              ├── render built-in or registered composable
              └── render children if this type is a container

Compose uses id as identity when present. Stable IDs preserve focus, lazy-list identity, and local state while nearby streamed subtrees change.

Runtime invariants

  1. The tree is the only render document.
  2. The store is the only mutable UI source of truth.
  3. Every store write is serialized and atomic.
  4. Effects never enter state and are consumed once.
  5. Widget rendering never performs network or native reflection.
  6. Extension handlers receive resolved data and dispatch actions; they do not receive the store.
  7. Malformed input becomes a logged skip or an action error, never an exception from composition.

Process death and checkpoints

Configuration changes keep the in-memory view store. Process death may restore a serialized checkpoint:

@Serializable
data class BduiCheckpoint(
    val viewKey: String,
    val tree: BduiWidgetModel,
    val lastEventId: String? = null,
    val generation: Long? = null,
    val savedAtEpochMs: Long,
)

Persist tree + node states + theme and the SSE cursor. Do not persist effects, running actions, HTTP requests, $event, $response, or $error.

On cold start:

  1. Load a fresh, compatible checkpoint for viewKey.
  2. Commit it as the initial tree without firing initial observers.
  3. Reconnect SSE with Last-Event-ID.
  4. If missing, expired, or incompatible, initialize from initialJson / streamUrl.

Small form-only state may use platform saved state. Full trees belong in file-backed storage. Checkpoints need a TTL and schema/spec compatibility check.

Multiplatform layout

commonMain
  BduiConfig, BduiSpec, BduiJsPolicy
  model/BduiWidgetModel
  store/BduiStore
  action/BduiActionRuntime
  observer/BduiObserverEngine
  sse/BduiSseSession
  theme/BduiTheme
  widget catalog and extension registries
  expect host/platform capabilities

androidMain / iosMain / desktopMain / jsMain / wasmJsMain
  actual JavaScript sandbox
  navigator, notifier, clipboard, sharing, URI handler

The host places BduiView in any Compose (or platform) layout. A Fragment/UIViewController wrapper is optional host code, not part of the common protocol.

← Documentation · Next: State →

AppSana — экраны под задачу в приложениях, которые у вас уже есть.