Extending BDUI
← Compatibility · Documentation · Next: Over-the-air widgets →
The built-in vocabulary is intentionally finite. Apps may register namespaced widgets and actions without reflection or renderer forks.
Naming and registration
| Capability | JSON | Registration |
|---|---|---|
| Custom widget | "type": "acme:Map" | widgets.register("acme:Map", renderer) |
| Custom action | "type": "custom", "name": "acme:favoriteOffer" | actions.register("acme:favoriteOffer", handler) |
Custom names must contain a namespace. Plain names and the BDUI namespace are reserved. Duplicate registration fails during Bdui.init / config construction.
Registries are capability allowlists: backend JSON cannot instantiate or invoke anything the app did not expose.
Custom widgets
data class BduiWidgetScope(
val id: String?,
val params: BduiResolvedParams,
val state: JsonObject,
val modifier: Modifier,
val theme: BduiTheme,
val dispatch: (BduiAction?) -> Unit,
val renderChildren: @Composable () -> Unit,
)
typealias BduiWidgetRenderer =
@Composable (BduiWidgetScope) -> Unit
class BduiWidgetRegistry {
fun register(
type: String,
renderer: BduiWidgetRenderer,
)
}
BDUI builds scope.modifier from the envelope modifier array, then applies clickable when envelope onClick is present and params.enabled is not false. Custom params may still declare their own actions (for example onMarkerClick).
Registration:
Bdui.init(
BduiConfig {
widgets.register("acme:Map") { scope ->
AcmeMap(
latitude = scope.params.number("latitude"),
longitude = scope.params.number("longitude"),
markerTitle = scope.params.string("markerTitle"),
modifier = scope.modifier,
onMarkerClick = {
scope.dispatch(
scope.params.action("onMarkerClick"),
)
},
)
}
}
)
JSON:
{
"id": "offer-map",
"type": "acme:Map",
"params": {
"latitude": "$parent.location.latitude",
"longitude": "$parent.location.longitude",
"markerTitle": "$parent.title",
"style": "$theme.components.AcmeMap.default",
"onMarkerClick": {
"type": "analytics",
"event": "offer_map_marker_clicked"
}
}
}
Rules:
- use
BduiResolvedParams; never parse paths independently; - never mutate
BduiStore; dispatch actions; - use supplied
modifierand effective theme; - call
renderChildrenonly when the custom widget is a container; - define parameter types/defaults and accessibility behavior in module-local docs;
- catch rendering failures at the renderer boundary, log, and skip the custom node;
- use
expect/actualcomposables for native map/camera/media controls.
Unknown/unregistered custom widgets are skipped, as defined by compatibility.
Custom actions
sealed interface BduiCustomActionResult {
data object Done : BduiCustomActionResult
data class Dispatch(
val action: BduiAction,
) : BduiCustomActionResult
data class Fail(
val message: String,
val code: String = "custom",
) : BduiCustomActionResult
}
fun interface BduiCustomActionHandler {
suspend fun execute(
args: JsonObject,
context: BduiActionContext,
): BduiCustomActionResult
}
class BduiActionRegistry {
fun register(
name: String,
handler: BduiCustomActionHandler,
)
}
Registration:
Bdui.init(
BduiConfig {
actions.register("acme:favoriteOffer") { args, context ->
val offerId = args
.getValue("offerId")
.jsonPrimitive
.content
runCatching { favorites.add(offerId) }.fold(
onSuccess = {
BduiCustomActionResult.Dispatch(
BduiAction.Toast("Added to favorites"),
)
},
onFailure = {
BduiCustomActionResult.Fail(
message = "Could not add favorite",
)
},
)
}
}
)
JSON:
{
"type": "custom",
"name": "acme:favoriteOffer",
"args": {
"offerId": "$root.offer.id",
"source": "details"
},
"onError": {
"type": "toast",
"message": "$error.message",
"style": "error"
}
}
args resolve recursively before invocation. Handler rules:
- no direct store reference;
- return
Dispatchfor further BDUI work; - return
Failfor expected failure; thrown exceptions are caught as failure; - inherit dispatch source and cancellation;
- execute under host timeout/coroutine policy;
- never resolve JVM/ObjC method or class names from JSON;
- call app repositories/native APIs through injected services.
Unknown custom action names are compatibility skips, not failures.
Platform capabilities
Common code should depend on narrow interfaces:
interface BduiNavigator
interface BduiNotifier
interface BduiClipboard
interface BduiShare
interface BduiUriHandler
expect class BduiJsRuntime
Implement them in platform source sets. Do not put Android Fragment, Activity, Context, iOS controller, browser DOM, or JVM reflection in the common protocol.
Adding built-ins
When behavior is broadly useful across integrations, add it to the built-in vocabulary:
- define serializable schema and validation;
- implement common behavior/platform capability;
- add detailed entry to widgets or actions;
- add JSON fixtures and runtime tests;
- bump
BduiSpec.CURRENTwhen it introduces new vocabulary; - update server negotiation for old clients.
Avoid duplicating widgets only for colors or spacing; extend semantic theme tokens/variants instead.
Recommended implementation order
- KMP model and
BduiStore - theme layers and safe token resolution
- resolved paths and modifiers
- widget/action registries
- basic layout/content widgets
patch, groups, effects, and failure pipeline- HTTP and response apply specs
- input write-back
- state/error observers
- safe JavaScript runtime
- SSE session
- remaining platform effects/widgets
← Compatibility · Documentation · Next: Over-the-air widgets →