Monitor and react to block changes in real time by subscribing to creation, update, and destruction events in your CE.SDK scene.
Events let you monitor block changes as they happen. On Android,
engine.event.subscribe() returns a Flow<List<DesignBlockEvent>>, so you
collect batched updates inside a coroutine on Dispatchers.Main and cancel the
collection job when you no longer need it.
This guide covers subscribing to block lifecycle events, processing the three
event types (CREATED, UPDATED, DESTROYED), filtering events to specific
blocks, understanding batching and deduplication behavior, and properly
cleaning up subscriptions.
Setup#
Use evaluation mode by passing license = null, then create a scene and page
before subscribing. Engine calls stay on the main thread, while
bindOffscreen() gives the engine an offscreen surface for this headless demo.
val scene = engine.scene.create()val page = engine.block.create(DesignBlockType.Page)engine.block.setWidth(page, value = 800F)engine.block.setHeight(page, value = 600F)engine.block.appendChild(parent = scene, child = page)Event Types#
CE.SDK provides three event types that capture the block lifecycle:
| Type | Description |
|---|---|
DesignBlockEvent.Type.CREATED | Fires when a new block is created. |
DesignBlockEvent.Type.UPDATED | Fires when any property of a block changes. |
DesignBlockEvent.Type.DESTROYED | Fires when a block is destroyed. |
Each DesignBlockEvent contains a block property with the block ID and a
type property indicating which event occurred.
Subscribing to All Blocks#
Use engine.event.subscribe() to register a collector that receives batched
events. Pass emptyList() to receive events from every block:
val allBlocksSubscription = engine.event.subscribe(blocks = emptyList()) .onEach { events -> events.forEach { event -> println("[All Blocks] ${event.type} event for block ${event.block}") } }.launchIn(this)The launchIn(this) call returns a Job. Keep that job so you can cancel the
subscription during cleanup.
Subscribing to Specific Blocks#
When you only care about certain blocks, pass their IDs to filter the flow:
val specificBlocksSubscription = engine.event.subscribe(blocks = listOf(graphic)) .onEach { events -> events.forEach { event -> println("[Specific Block] ${event.type} event for block ${event.block}") } }.launchIn(this)Filtering reduces overhead because the engine only prepares events for the blocks you are tracking.
API Reference#
Signature: fun subscribe(blocks: List<DesignBlock> = emptyList()): Flow<List<DesignBlockEvent>>
Subscribe to block life-cycle events
blocks: a list of blocks to filter events by. If the list is empty, events for every block are sent.
Creating Blocks and Handling CREATED Events#
Creating a block triggers a CREATED event. This example also appends the block
to the page, adds a 400×300 graphic at (200, 150) with a remote image fill,
and sets Cover content fill mode.
val graphic = engine.block.create(DesignBlockType.Graphic)val rectShape = engine.block.createShape(ShapeType.Rect)engine.block.setShape(block = graphic, shape = rectShape)engine.block.setPositionX(graphic, value = 200F)engine.block.setPositionY(graphic, value = 150F)engine.block.setWidth(graphic, value = 400F)engine.block.setHeight(graphic, value = 300F)
val imageFill = engine.block.createFill(FillType.Image)engine.block.setString( block = imageFill, property = "fill/image/imageFileURI", value = "https://img.ly/static/ubq_samples/sample_1.jpg",)engine.block.setFill(block = graphic, fill = imageFill)engine.block.setEnum( block = graphic, property = "contentFill/mode", value = "Cover",)engine.block.appendChild(parent = page, child = graphic)Use CREATED events to start tracking a block, update local state, or trigger
follow-up work such as analytics or sync jobs.
Updating Blocks and Handling UPDATED Events#
Changing any property of a block triggers an UPDATED event. Here, the example
rotates the block by 0.1f radians and sets its opacity to 0.9f.
engine.block.setRotation(graphic, radians = 0.1F)engine.block.setFloat( block = graphic, property = "opacity", value = 0.9F,)The event itself does not tell you which property changed, only that the block was updated.
Processing Events by Type#
Process each batch by switching on event.type. For CREATED and UPDATED
events, Block API calls are safe. For DESTROYED events, treat the block ID as
invalid and clean up local references without calling more Block API methods on
that ID.
val processedEventsSubscription = engine.event.subscribe(blocks = emptyList()) .onEach { events -> events.forEach { event -> when (event.type) { DesignBlockEvent.Type.CREATED -> { val blockType = engine.block.getType(event.block) println("Block created with type: $blockType") }
DesignBlockEvent.Type.UPDATED -> { println("Block ${event.block} was updated") }
DesignBlockEvent.Type.DESTROYED -> { println("Block ${event.block} was destroyed") } } } }.launchIn(this)Handling DESTROYED Events Safely#
Once a block is destroyed, its ID becomes invalid. If your app keeps cached block
IDs, use engine.block.isValid() to prune stale references before passing them
to other Block API methods:
val cachedBlocks = mutableSetOf(graphic)cachedBlocks.removeAll { block -> !engine.block.isValid(block) }Destroying the tracked graphic block in the example triggers a DESTROYED event:
engine.block.destroy(graphic)println("Destroyed graphic block $graphic")After a DESTROYED event, clean up matching cached references in your app state
instead of calling more Block API methods on that ID.
Unsubscribing from Events#
On Android, unsubscribing means cancelling the Jobs that collect your event
flows:
allBlocksSubscription.cancel()specificBlocksSubscription.cancel()processedEventsSubscription.cancel()Cancel subscriptions when your screen leaves composition, the editor closes, or you stop tracking a block. Leaving collectors active forces the engine to keep preparing event lists on every update.
Event Batching and Deduplication#
Events are collected during an engine update and delivered together at the end.
The engine deduplicates UPDATED events, so you receive at most one
UPDATED event per block per update cycle.
This batching behavior means:
- Multiple rapid changes to a single block result in one
UPDATEDevent. - The array order does not reflect the chronological order of changes inside one update cycle.
- If you need to know which property changed, compare against cached values in your app.
Use Cases#
Events support several reactive patterns in Android apps:
- Syncing external state: Keep ViewModels, Redux stores, or persistence layers aligned with scene changes.
- Building reactive UI: Update Compose state when blocks change without polling the engine.
- Tracking changes for undo/redo: Monitor block changes for custom history or analytics pipelines.
- Validating scene constraints: React to new or updated blocks when you enforce design rules in code.
Troubleshooting#
Events not firing: Make sure you have not cancelled the collection job too early, and confirm the filtered block IDs are still valid.
Exception on DESTROYED events: Treat destroyed block IDs as invalid, and prune stale cached IDs with engine.block.isValid() before calling other Block API methods.
Missing UPDATED events: The engine deduplicates updates, so multiple rapid property changes become one UPDATED event per block.
Leaking collectors: Store the Job returned by launchIn() and cancel it during cleanup.
Next Steps#
Blocks — Learn about block types, properties, and lifecycle.
Undo and History — Implement undo/redo functionality.
Scenes — Understand scene structure and management.