Develop
Develop
Select your platform

2D panel communication

Updated: Oct 1, 2025

Overview

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

Illustrating using shared ViewModels to create communication across panels.
Use shared ViewModels when you have a single activity with multiple panels that need to share basic state.

Implementation

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.

Limitations

  • 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!")
}

How the pattern works

Both directions use the same essential mechanism:
  1. Store a weak reference to the target activity in a companion object
  2. 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:
Did you find this page helpful?
Thumbs up icon
Thumbs down icon