Block & Action
Annotate, register, and provide custom blocks and actions with the Nativeblocks SDK.
Blocks are UI components. Actions are business logic handlers. Both are annotated in your code, compiled into schemas, and registered with the SDK at runtime.
Step 1: Add the Compiler
The compiler reads your annotations and generates JSON schemas that the CLI syncs to Nativeblocks.
Add to your module build.gradle:
dependencies {
implementation("io.nativeblocks:nativeblocks-android:1.8.1")
implementation("io.nativeblocks:nativeblocks-compiler-android:1.3.2")
ksp("io.nativeblocks:nativeblocks-compiler-android:1.3.2")
}
Also add the Gradle plugin to sync schemas via the CLI:
plugins {
id("io.nativeblocks.nativeblocks-gradle-plugin") version "1.2.1"
}
Step 2: Annotate a Block
@NativeBlock(
name = "Custom Button",
keyType = "myOrg/CUSTOM_BUTTON",
description = "A configurable button block",
version = 1,
versionName = "1.0.0"
)
@Composable
fun CustomButton(
@NativeBlockData(description = "Button label")
text: String,
@NativeBlockProp(
description = "Size variant",
valuePicker = NativeBlockValuePicker.DROPDOWN,
valuePickerOptions = [
NativeBlockValuePickerOption("S", "Small"),
NativeBlockValuePickerOption("M", "Medium"),
NativeBlockValuePickerOption("L", "Large"),
]
)
size: String = "M",
@NativeBlockSlot(description = "Leading icon")
onLeadingIcon: @Composable (index: BlockIndex) -> Unit,
@NativeBlockEvent(description = "On click")
onClick: () -> Unit,
) {
// your Composable implementation
}
After building, the compiler generates a provider. Register it after SDK init:
DemoBlockProvider.provideBlocks()
Annotation reference
| Annotation | Target | Purpose |
|---|---|---|
@NativeBlock | Function / Struct | Marks the component as a block with a unique keyType |
@NativeBlockData | Parameter | Runtime variable bound to frame state |
@NativeBlockProp | Parameter | Static property configured via the CLI; supports valuePicker, valuePickerGroup, valuePickerOptions, and defaultValue |
@NativeBlockSlot | Parameter | Slot where child blocks can be nested |
@NativeBlockEvent | Parameter | Event the block can fire (tap, change, etc.); supports dataBinding |
@NativeBlockValuePickerOption | — | Defines a selectable option (id, text) for DROPDOWN or COMBOBOX_INPUT pickers |
@NativeBlockValuePickerPosition | — | Groups related properties under a named section in the CLI UI |
Step 3: Annotate an Action
@NativeAction(
keyType = "myOrg/NAVIGATE_ACTION",
name = "Navigate",
description = "Navigates to a new screen",
version = 1,
versionName = "1.0.0"
)
class NavigateAction {
@NativeActionParameter
data class Parameter(
@NativeActionProp(description = "Destination route")
val route: String = "",
@NativeActionData(description = "Error message stored in a variable")
val error: String = "",
@NativeActionEvent(then = Then.SUCCESS)
val onSuccess: () -> Unit,
@NativeActionEvent(then = Then.FAILURE, dataBinding = ["error"])
val onFailure: (String) -> Unit,
)
@NativeActionFunction
suspend fun navigate(parameter: Parameter) {
try {
// perform navigation
parameter.onSuccess()
} catch (e: Exception) {
parameter.onFailure(e.message ?: "Unknown error")
}
}
}
Register after SDK init:
DemoActionProvider.provideActions()
Annotation reference
| Annotation | Target | Purpose |
|---|---|---|
@NativeAction | Class | Marks the class as an action with a unique keyType |
@NativeActionParameter | Class | Marks a class as a parameter holder passed to the action at runtime |
@NativeActionFunction | Function | Marks the function that executes the action logic |
@NativeActionProp | Parameter | Static property configured via the CLI; supports valuePicker, valuePickerGroup, valuePickerOptions, and defaultValue |
@NativeActionData | Parameter | Runtime variable bound to frame state |
@NativeActionEvent | Parameter | Outcome event the action can emit; then controls flow (SUCCESS, FAILURE, NEXT, END) |
@NativeActionValuePickerOption | — | Defines a selectable option (id, text) for DROPDOWN pickers |
@NativeActionValuePickerPosition | — | Groups related properties under a named section in the CLI UI |
Step 4: Register at Runtime
You can also register blocks and actions manually:
// Block
NativeblocksManager.getInstance().provideBlock(
blockType = "myOrg/CUSTOM_BUTTON",
block = { props -> CustomButton(props) }
)
// Action
NativeblocksManager.getInstance().provideAction(
actionType = "myOrg/NAVIGATE_ACTION",
action = NavigateAction()
)
Type Converters
Frame properties are stored as JSON strings in the server. A type converter bridges between that raw string and a typed Kotlin or Swift value so you can use rich types directly in your block properties.
Important: Type converters only apply to @NativeBlockProp (static properties configured via the CLI). They do not apply to @NativeBlockData (runtime data variables bound to frame state).
Implement a converter
Implement INativeType<T> with two methods: toString to serialise your type to a string, and fromString to deserialise it back. A common property use case is a Color stored as a hex string in the frame definition.
class ColorNativeType : INativeType<Color> {
override fun toString(value: Color): String {
val argb = value.toArgb()
return String.format("#%08X", argb)
}
override fun fromString(value: String): Color {
return try {
Color(android.graphics.Color.parseColor(value))
} catch (e: Exception) {
Color.Unspecified
}
}
}
Register a converter
Call provideTypeConverter after SDK initialisation, before any blocks are rendered.
NativeblocksManager.getInstance().provideTypeConverter(
type = Color::class,
converter = ColorNativeType()
)
Use it in a block property
Once registered, annotate a block property with your custom type. The SDK calls the converter automatically when reading from frame JSON.
@NativeBlock(name = "Custom Button", keyType = "myOrg/CUSTOM_BUTTON", version = 1, versionName = "1.0.0")
@Composable
fun CustomButton(
@NativeBlockData(description = "Button label")
text: String,
@NativeBlockProp(description = "Background color as hex (e.g. #FF6200EE)")
backgroundColor: Color = Color.Blue,
) {
Box(modifier = Modifier.background(backgroundColor)) {
Text(text = text)
}
}