Develop
Develop
Select your platform

Spatial SDK activity lifecycle

Updated: Mar 4, 2026

Overview

A Spatial SDK app’s immersive activity is an AppSystemActivity, which extends VrActivity. This inheritance hierarchy provides a lifecycle that builds upon the traditional Android activity lifecycle while adding spatial and VR-specific callbacks.
Spatial SDK activity Lifecycle
The lifecycle of a spatial activity extends the traditional Android activity lifecycle. Spatial SDK provides additional lifecycle callbacks such as onSceneReady, onVRReady, and onSpatialShutdown that help you manage the state of your XR app throughout its lifecycle.

Inheritance hierarchy

Activity (Android)
   ↓
VrActivity (Spatial SDK)
   ↓
AppSystemActivity (Spatial SDK)
   ↓
YourActivity (Your immersive app)
In your immersive app, you should extend AppSystemActivity for the immersive activity. However, for your panel activities (either hybrid activities or the activity-based panels), you can simply extend Android’s Activity class.

Android lifecycle callbacks

You should follow the Android Activity Lifecycle when developing a Spatial SDK app. The guidance from the official Android site also applies to the Spatial SDK activity lifecycle.

Initialization

onCreate is called when the activity is first created. In a Spatial SDK app, this is where:
  • Spatial SDK is initialized.
  • Features and panels are registered.
  • Components and systems are registered.
  • glXF resources are usually loaded.
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)  // Important: Always call super.onCreate() first

    // Register components
    componentManager.registerComponent<MyCustomComponent>(MyCustomComponent.Companion)

    // Register systems
    systemManager.registerSystem(MyCustomSystem())

    // Load resources
    loadGLXF { composition ->
        // Handle loaded 3D models
        val environmentEntity = composition.getNodeByName("environment").entity
        // Configure entity components
    }
}

onStart

Called when the activity becomes visible to the user. The VrActivity implementation forwards this call to all registered features via featureManager.onStart().

onResume

Called when the activity starts interacting with the user.

onPause

Called when the system is about to put the activity into the background. If the activity is finishing in this callback, it will invoke onSpatialShutdown().

onStop

Called when the activity is no longer visible to the user. Consider putting shutdown logic in onSpatialShutdown() instead as onStop is not guaranteed to be called.

onDestroy

Called before the activity is destroyed. Consider putting shutdown logic in onSpatialShutdown() instead as onDestroy is not guaranteed to be called.

Handling configuration changes

By default, Android destroys and recreates activities when device configuration changes occur, such as screen rotation, keyboard availability, or UI mode changes. For VR applications, these restarts are disruptive because they:
  • Destroy the 3D scene, including all loaded entities, meshes, and lighting configurations
  • Reset tracking state and user position
  • Interrupt immersive experiences with jarring visual transitions
  • May cause crashes if VR subsystems are mid-initialization

Preventing activity restarts

To prevent activity restarts, add android:configChanges to your immersive activity declaration in AndroidManifest.xml:
<activity
    android:name=".ImmersiveActivity"
    android:configChanges="screenSize|screenLayout|orientation|keyboardHidden|keyboard|navigation|uiMode"
    android:launchMode="singleTask"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="com.oculus.intent.category.VR" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
When you declare these configuration changes, Android calls onConfigurationChanged() instead of destroying and recreating your activity. The Spatial SDK handles common configuration changes internally, so you typically don’t need to override onConfigurationChanged() in your activity.

Required configuration change values

All immersive activities should declare these values:
ValuePurpose
uiMode
Handles VR mode transitions and display configuration changes
screenSize
Handles virtual screen dimension adjustments
screenLayout
Handles layout direction and size class changes
orientation
Prevents restarts when orientation events occur (VR apps use landscape)
keyboardHidden
Handles virtual keyboard visibility changes
keyboard
Handles keyboard availability changes
navigation
Handles navigation method changes

Symptoms of missing configChanges

Without the android:configChanges attribute, your app may exhibit these symptoms on Horizon OS v85 and later:
  • App exits within 1 second of launch with no error message
  • No crash logs appear (the exit is a clean shutdown via onSpatialShutdown)
  • Users report that the app “won’t start” or “immediately closes”
  • The issue may not reproduce on earlier OS versions
If your app exhibits these symptoms, verify that all immersive activities include the android:configChanges attribute with the values listed above.

Hybrid apps and panel activities

For hybrid apps with both immersive and 2D panel activities:
  • Immersive activities (extending AppSystemActivity): Use the full set of configChanges values shown above
  • 2D panel activities (extending Activity or ComponentActivity): May use a subset such as screenSize|smallestScreenSize|screenLayout|orientation, or omit the attribute entirely if activity recreation is acceptable
For more information on Android’s configuration change handling, see the Android developer guide on handling configuration changes.

Spatial lifecycle callbacks

Some spatial callbacks are tied to the Android lifecycle, while others are not. For example, onVrReady is triggered when 3D graphics is ready and loaded, but it’s not tied to any Android lifecycle. This section describes each callback in detail.

onSceneReady

This callback is triggered on the first onResume(), after the scene is loaded in onCreate(). This is the ideal place to:
  • Configure the scene environment and lighting.
  • Set up the reference space for tracking.
  • Create entities in the scene.
override fun onSceneReady() {
    super.onSceneReady()  // Important: Always call super.onSceneReady() first

    // Set the reference space for tracking
    scene.setReferenceSpace(ReferenceSpace.LOCAL_FLOOR)

    // Configure lighting
    scene.setLightingEnvironment(
        ambientColor = Vector3(0f),
        sunColor = Vector3(7.0f, 7.0f, 7.0f),
        sunDirection = -Vector3(1.0f, 3.0f, -2.0f),
        environmentIntensity = 0.3f
    )

    // Set environment map
    scene.updateIBLEnvironment("environment.env")

    // Set the view origin
    scene.setViewOrigin(0.0f, 0.0f, 0.0f, 0.0f)

    // Create entities in the scene
    Entity.create(
        listOf(
            Mesh(Uri.parse("mesh://skybox"), hittable = MeshCollision.NoCollision),
            Material().apply {
                baseTextureAndroidResourceId = R.drawable.skydome
                unlit = true
            },
            Transform(Pose(Vector3(x = 0f, y = 0f, z = 0f)))
        )
    )
}

onSceneTick

This callback is triggered every tick when the activity is run. It’s recommended to avoid overriding this method and instead create systems that handle per-tick logic.

onVRReady

This callback is not tied to any Android lifecycle. It is triggered when the VR system is initialized and ready, or when the app changes from 2D mode to immersive (VR) mode. This is different from onSceneReady as it specifically indicates that the VR subsystems (like tracking, controllers, and so on) are ready for use.

onVRPause

This callback is not tied to any Android lifecycle. It is triggered when the VR system is paused or when the app changes from immersive (VR) mode to 2D mode.

onHMDMounted

This callback is not tied to any Android lifecycle. It is triggered when the user puts on the headset. This is useful for:
  • Starting experiences that should only run when the user is wearing the headset

onHMDUnmounted

This callback is not tied to any Android lifecycle. This callback is triggered when the user takes off the headset. This is useful for:
  • Pausing experiences that should only run when the user is wearing the headset.
  • Adjusting UI or interactions based on the user’s absence.

onRecenter

This callback is not tied to any Android lifecycle. This callback is triggered when the VR system is recentered by the system. This is useful for:
  • Adjusting the position of UI elements or objects in the scene.
  • Resetting the user’s position in the virtual environment.
To check if the override was triggered by the user holding the Meta button, not the system, use the isUserInitiated flag like so override fun onRecenter(isUserInitiated: Boolean).

onSpatialShutdown

This callback is triggered when the Spatial SDK is shutting down. While Android’s onStop or onDestroy is not guaranteed to be called, onSpatialShutdown is guaranteed to be called before the activity is destroyed. This is useful for doing any final cleanup of Spatial SDK-related resources, such as entities and scene objects.
override fun onSpatialShutdown() {
    // Destroy entities
    entity.destroy()

    super.onSpatialShutdown()  // Important: Always call super.onSpatialShutdown() last
}

Registration methods

registerFeatures

This method is called during onCreate to register the features that your app will use. Features are the building blocks of Spatial SDK functionality, such as VR support, input handling, animations, and debugging tools.
override fun registerFeatures(): List<SpatialFeature> {
    val features = mutableListOf<SpatialFeature>(
        VRFeature(this),
        PanelAnimationFeature()
    )

    // Add debug features in development builds
    if (BuildConfig.DEBUG) {
        features.add(CastInputForwardFeature(this))
        features.add(HotReloadFeature(this))
        features.add(DataModelInspectorFeature(spatial, this.componentManager))
    }

    return features
}

registerPanels

This method is called during onCreate to register the UI panels that your app will use. Panels are 2D UI surfaces that can be placed in 3D space.
override fun registerPanels(): List<PanelRegistration> {
    return listOf(
        LayoutXMLPanelRegistration(
            R.id.main_panel,
            layoutIdCreator = { R.layout.main_panel },
            settingsCreator = {
                UIPanelSettings(
                    shape = CylinderShapeOptions(radius = 1.0f),
                    display = DpDisplayOptions(width = 720f),
                    style = PanelStyleOptions(themeResourceId = R.style.PanelAppThemeTransparent),
                    rendering = LayerRenderOptions()
                )
            }
        ),
        LayoutXMLPanelRegistration(
            R.id.settings_panel,
            layoutIdCreator = { R.layout.settings_panel },
            settingsCreator = { UIPanelSettings() }
        )
    )
}
Did you find this page helpful?