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

CapabilityJSONRegistration
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 modifier and effective theme;
  • call renderChildren only 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/actual composables 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 Dispatch for further BDUI work;
  • return Fail for 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:

  1. define serializable schema and validation;
  2. implement common behavior/platform capability;
  3. add detailed entry to widgets or actions;
  4. add JSON fixtures and runtime tests;
  5. bump BduiSpec.CURRENT when it introduces new vocabulary;
  6. update server negotiation for old clients.

Avoid duplicating widgets only for colors or spacing; extend semantic theme tokens/variants instead.

  1. KMP model and BduiStore
  2. theme layers and safe token resolution
  3. resolved paths and modifiers
  4. widget/action registries
  5. basic layout/content widgets
  6. patch, groups, effects, and failure pipeline
  7. HTTP and response apply specs
  8. input write-back
  9. state/error observers
  10. safe JavaScript runtime
  11. SSE session
  12. remaining platform effects/widgets

← Compatibility · Documentation · Next: Over-the-air widgets →

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