Develop spatial UI with Jetpack Compose for XR

Applicable XR devices
This guidance helps you build experiences for these types of XR devices.
XR Headsets
Wired XR Glasses

With Jetpack Compose for XR, you can declaratively build your spatial UI and layout using familiar Compose concepts such as rows and columns. This lets you extend your existing Android UI into 3D space or build entirely new immersive 3D applications.

If you are spatializing an existing Android Views-based app, you have several development options. You can use interoperability APIs, use Compose and Views together, or work directly with the SceneCore library. See our guide to working with views for more details.

About subspaces and spatialized components

When you're writing your app for Android XR, it's important to understand the concepts of subspace and spatialized components.

About subspace

When developing for Android XR, you'll need to add a subspace to your app or layout. A subspace is a partition of 3D space within your app where you can place 3D content, build 3D layouts, and add depth to otherwise 2D content. A subspace is rendered only when spatialization is enabled. In Home Space or on non-XR devices, any code within that subspace is ignored.

There are a few ways to create a subspace:

  • Subspace: This composable creates a new, independent Spatial UI hierarchy. It does not inherit the spatial position, orientation, or scale of any parent Subspace it is nested within. Subspace is automatically bound by the system's recommended content box.
  • PlanarEmbeddedSubspace: This composable can be placed within your app's UI hierarchy, allowing you to maintain layouts for 2D and spatial UI. PlanarEmbeddedSubspace respects the constraints and positioning of its parent. The 3D content placed inside it is then positioned relative to this 2D-defined area.

For more information, see Add a subspace to your app.

About spatialized components

Subspace composables: These components can only be rendered in a subspace. They must be enclosed within Subspace before being placed within a 2D layout. A SubspaceModifier lets you add attributes like depth, offset, and positioning to your subspace composables.

Other spatialized components don't require being called inside a subspace. They consist of conventional 2D elements wrapped within a spatial container. These elements can be used within 2D or 3D layouts if defined for both. When spatialization is not enabled, their spatialized features will be ignored and they will fall back to their 2D counterparts.

Create a spatial panel

A SpatialPanel is a subspace composable that lets you display app content–for example, you could display video playback, still images, or any other content in a spatial panel.

Example of a spatial UI panel

You can use SubspaceModifier to change the size, behavior, and positioning of the spatial panel, as shown in the following example.

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .movable()
            .resizable(),
    ) {
        SpatialPanelContent()
    }
}

@Composable
fun SpatialPanelContent() {
    Box(
        Modifier
            .background(color = Color.Black)
            .height(500.dp)
            .width(500.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = "Spatial Panel",
            color = Color.White,
            fontSize = 25.sp
        )
    }
}

Key points about the code

  • Because SpatialPanel APIs are subspace composables, you must call them inside Subspace. Calling them outside of a subspace throws an exception.
  • The size of the SpatialPanel has been set using the height and width specifications on the SubspaceModifier. Omitting these specifications lets the size of the panel be determined by the measurements of its contents.
  • Allow the user to move a panel by adding a movable subspace modifier.
  • Allow the user to resize a panel by adding a resizable subspace modifier.
  • See our spatial panel design guidance for details on sizing and positioning. See our reference documentation for more specifics on code implementation.

How the movable modifier works

As a user moves a panel away from them, by default, the movable modifier scales the panel in a similar way to how panels are resized by the system in home space. All children content inherit this behavior. To disable this, set the shouldScaleWithDistance parameter to false.

Create an orbiter

An orbiter is a spatial UI component. It's designed to be attached to a corresponding spatial panel or spatial layout component like SpatialColumn, SpatialRow, or SpatialBox. An orbiter typically contains navigation and contextual action items related to the entity it's anchored to. For example, if you've created a spatial panel to display video content, you could add video playback controls inside an orbiter.

Example of an orbiter

As shown in the following example, call an orbiter inside the 2D layout in a SpatialPanel to wrap user controls like navigation. Doing so extracts them from your 2D layout and attaches them to the spatial panel according to your configuration.

Subspace {
    SpatialPanel(
        SubspaceModifier
            .height(824.dp)
            .width(1400.dp)
            .resizable()
            .movable(),
    ) {
        SpatialPanelContent()
        OrbiterExample()
    }
}

@Composable
fun OrbiterExample() {
    Orbiter(
        alignment = OrbiterAlignment.BottomCenter(
            edgeOffsetType = OrbiterEdgeOffsetType.OuterEdge,
            offset = DpVolumeOffset(y = 96.dp)
        ),
    ) {
        Surface(Modifier.clip(CircleShape)) {
            Row(
                Modifier
                    .background(color = Color.Black)
                    .height(100.dp)
                    .width(600.dp),
                horizontalArrangement = Arrangement.Center,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = "Orbiter",
                    color = Color.White,
                    fontSize = 50.sp
                )
            }
        }
    }
}

Key points about the code

  • Because orbiters are spatial UI components, the code can be reused in 2D or 3D layouts. In a 2D layout, your app renders only the content inside the orbiter and ignores the orbiter itself.
  • Check out our design guidance for more information on how to use and design orbiters.

Add multiple spatial panels to a spatial layout

You can create multiple spatial panels and place them within a spatial layout using SpatialRow, SpatialColumn, SpatialBox, and SpatialSpacer.

Example of multiple spatial panels in a spatial layout

The following code example shows how to do this.

Subspace {
    SpatialRow {
        SpatialColumn {
            SpatialPanel(SubspaceModifier.height(250.dp).width(400.