2D panel communication
Updated: Oct 1, 2025
Spatial SDK applications often need communication between 2D panels and the scene. Panel buttons spawn objects in the scene. Changes in the scene update panel UI elements.
Choose the right communication pattern based on your application’s complexity:
- Simple shared state: Single activity with basic state sharing between panels
- Global reference pattern: Direct operations between activities using global references
- Multi-activity inter-process communication (IPC): Required when panel activities run on separate processes from the 3D activity
Simple shared state communication
Use shared ViewModels when you have a single activity with multiple panels that need to share basic state.
This example creates two panels that share a simple counter state. One panel has a button to increment the counter. Another panel displays the current count. Both panels stay synchronized automatically.
Step 1: Create your UI components
Define the Composable functions for your panels:
@Composable
fun ControlPanel(viewModel: SharedPanelViewModel) {
val counter by viewModel.counter.observeAsState(0)
Column {
Text("Counter: $counter")
Button(onClick = { viewModel.incrementCounter() }) {
Text("Increment")
}
}
}
@Composable
fun DisplayPanel(viewModel: SharedPanelViewModel) {
val counter by viewModel.counter.observeAsState(0)
Text("Count: $counter")
}
Step 2: Create a shared ViewModel
The ViewModel manages the shared state that both panels observe:
class SharedPanelViewModel : ViewModel() {
private val _counter = MutableLiveData<Int>(0)
val counter: LiveData<Int> = _counter
fun incrementCounter() {
_counter.value = (_counter.value ?: 0) + 1
}
}
Step 3: Register panels with shared ViewModel
Create the shared ViewModel and pass it to both panels:
class MyActivity : AppSystemActivity() {
// Create shared ViewModel at activity level
private val sharedViewModel = SharedPanelViewModel()
override fun registerPanels(): List<PanelRegistration> {
return listOf(
ComposeViewPanelRegistration(
R.id.control_panel,
composeViewCreator = { _, ctx ->
ComposeView(ctx).apply {
setContent { ControlPanel(sharedViewModel) }
}
},
settingsCreator = { UIPanelSettings() }
),
ComposeViewPanelRegistration(
R.id.display_panel,
composeViewCreator = { _, ctx ->
ComposeView(ctx).apply {
setContent { DisplayPanel(sharedViewModel) }
}
},
settingsCreator = { UIPanelSettings() }
)
)
}
}
Click the
Increment button on the control panel. Both panels automatically update to show the new counter value. This communication setup works well with Jetpack Compose. You can use the same approach with view-based panels. For more information on Jetpack Compose UI in Spatial SDK see
Build UIs with Jetpack Compose.
- Only works for UI within a single Activity
- Activity-based panels cannot leverage this communication setup
Global reference pattern for bidirectional communication
This allows communication between panels and the scene, even when they run in different Activities. Store weak references to Activities so any code can call functions on specific Activities. Beware of potential downsides. managing global references can introduce lifecycle issues and testing complexity.
Panel -> scene (built-in)
Your VR activity has a function that spawns an Entity in the scene:
class MyVRActivity : AppSystemActivity() {
fun spawnSphere() {
Entity.create(
listOf(
Mesh("mesh://sphere".toUri()),
Transform(Pose(Vector3(0f, 1f, -2f)))
)
)
}
}
If you want to spawn this when a button is clicked in a Panel, you can use the global SpatialActivityManager to give your 2d panel code the ability to execute code that will run within the VR Activity:
@Composable
fun SpawnObjectPanel() {
Button(onClick = {
SpatialActivityManager.executeOnVrActivity<MyVRActivity> { activity ->
// This works from any panel - same activity or separate activity
activity.spawnSphere()
}
}) {
Text("Spawn Sphere")
}
}
Scene -> Panel communication (set up yourself)
For the reverse direction, set up the same pattern in your panel activities:
class MyPanelActivity : ComponentActivity() {
companion object {
var instance: WeakReference<MyPanelActivity>? = null
inline fun executeOnPanelActivity(crossinline runnable: (activity: MyPanelActivity) -> Unit) {
instance?.get()?.let { activity ->
activity.runOnMainThread { runnable(activity) }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
instance = WeakReference(this) // Store reference
}
fun updateFromVR(message: String) {
// Update panel UI from VR activity
}
}
MyPanelActivity.executeOnPanelActivity { activity ->
activity.updateFromVR("Sphere grabbed!")
}
Both directions use the same essential mechanism:
- Store a weak reference to the target activity in a companion object
- Call functions directly on that activity from anywhere in your code
Multi-activity IPC communication
Complex applications run panel Activities and the 3D Activity on separate processes. Use inter-process communication techniques to set up communication between your Activities.
For implementation details, see Android’s official documentation: