For decades, our entertainment has been largely confined to a two-dimensional lens. But Android XR allows us to break through those boundaries, stepping into truly three-dimensional experiences that place you right in the heart of the action. 

1. Understanding modes and spatial panels in XR app development

  • Home space mode: A user can multitask with your app running side by side with other apps. Notably, any compatible mobile or large-screen Android app can readily operate in Home Space with no additional development.
  • Full space mode: Your app takes center stage as the focus of the user's experience with full access to the immersive capabilities of Android XR. To truly capitalize on this mode, enhance your app by incorporating spatial panels, 3D models and spatial environments to take advantage of the space.
  • Spatial panel: Spatial panels act as containers for your app's content. In Home Space mode, your app is confined to a single panel, much like on a large-screen Android device. Conversely, Full Space mode allows you to distribute your app's content across multiple panels, creating a richer, more immersive environment.

2. Set up dependencies

  • Add a dependency on the Jetpack Compose for XR library

libs.version.toml

[versions]
compose = "1.0.0-alpha06"

[libraries]
androidx-compose = { module = "androidx.xr.compose:compose", version.ref = "compose" }

build.gradle.kts (Module :app)

dependencies {
	implementation(libs.androidx.compose)
}

After updating these files, make sure to do a gradle sync to ensure the dependencies are downloaded to your project.

3. Full space mode

Application must run in Full Space mode to make use of XR features.

XRView.kt

@Composable
fun AndroidXRApp() {
    FlatTwoPaneLayout(
        secondaryPane = {
            ContentOne()
            ContentTwo()
        },
        primaryPane = { ContentMain() }
    )
}

@Composable
private fun FlatTwoPaneLayout(
    primaryPane: @Composable () -> Unit,
    secondaryPane: @Composable () -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
        modifier = . . .
    ) {
        TogglePaneLayout()
        Spacer(Modifier.height(16.dp))
        SideBySidePaneLayout(primaryPane, secondaryPane)
    }
}

@Composable
private fun SideBySidePaneLayout(
    primaryPane: @Composable () -> Unit,
    secondaryPane: @Composable () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(modifier) {
        Surface(
            modifier = . . .
        ) {
            Column {
                secondaryPane()
            }
        }
        Spacer(Modifier.width(16.dp))
        Surface(modifier.clip(RoundedCornerShape(16.dp))) {
            primaryPane()
        }
    }
}

@Composable
private fun ContentMain(modifier: Modifier = Modifier) {
    TextPane(
        text = "Welcome to Android XR!",
        modifier = modifier.clip(RoundedCornerShape(16.dp))
    )
}

@Composable
private fun ContentOne(modifier: Modifier = Modifier) {
    TextPane(text = "Content One")
}

@Composable
private fun ContentTwo(modifier: Modifier = Modifier) {
    TextPane(text = "Content Two")
}

@Composable
fun TextPane(text: String, modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier.fillMaxSize()
    ) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.fillMaxSize()
        ) {
            Text(
                text = text,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

@Composable
private fun TogglePaneLayout() {
    Row(
        horizontalArrangement = Arrangement.SpaceBetween,
        modifier = . . .
    ) {
        Spacer(Modifier.weight(1f))
        Orbiter(
            position = ContentEdge.Top,
            offset = 96.dp,
            alignment = Alignment.End
        ) {
            Controls()
        }
    }
}

Create a controller to switch between Home space mode and Full space mode.

Controller.kt

 @Composable
fun Controls(modifier: Modifier = Modifier) {
    val activity = LocalActivity.current
    val session = LocalSession.current
    if (session != null && activity is ComponentActivity) {
        Surface(modifier.clip(CircleShape)) {
            if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
                RequestHomeSpaceButton {
                    session.scene.requestHomeSpaceMode()
                }
            } else {
                RequestFullSpaceButton {
                    session.scene.requestFullSpaceMode()
                }
            }
        }
    }
}

@Composable
fun RequestHomeSpaceButton(onclick: () -> Unit) {
    IconButton(
        onClick = onclick,
        modifier = Modifier
            .padding(16.dp)
            .background(MaterialTheme.colorScheme.onSecondary, CircleShape)
            .size(56.dp)
    ) {
        Icon(
            painter = painterResource(id = R.drawable.ic_request_home_space),
            contentDescription = stringResource(R.string.switch_to_home_space_mode)
        )
    }
}

@Composable
fun RequestFullSpaceButton(onclick: () -> Unit) {
	. . . 
}

MainActivity.kt 

class MainActivity : ComponentActivity() {

    @SuppressLint("RestrictedApi")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

        setContent {
            DemoXRTheme {
                AndroidXRApp()
            }
	   }
	}
}

4. Spatial panels in full space mode

 

@Composable
fun AndroidXRApp() {
    if(LocalSpatialCapabilities.current.isSpatialUiEnabled) {
        SpatialLayout(
            primaryContent = { ContentMain() },
            supportingContentOne = { ContentOne() },
            supportingContentTwo = { ContentTwo() }
        )
    } else {
        FlatTwoPaneLayout()
    }
}

@Composable
private fun SpatialLayout(
    primaryContent: @Composable () -> Unit,
    supportingContentOne: @Composable () -> Unit,
    supportingContentTwo: @Composable () -> Unit,
) {
    Subspace {
        SpatialRow(
            modifier = SubspaceModifier.height(816.dp).fillMaxWidth()
        ) {
            SpatialColumn(modifier = SubspaceModifier.width(400.dp)) {
                SpatialPanel(. . .) {
                    supportingContentOne()
                }
                SpatialPanel(. . .) {
                    supportingContentTwo()
                }
            }
            SpatialPanel(. . .) {
                Column {
                    TogglePaneLayout()
                    primaryContent()
                }
            }
        }
    }
}

5. Update the manifest

As you added spatial features to your app, you need to update the manifest to indicate that it supports these features. Thus, Google Play will show your app for Android XR devices. 

<application ...>
    <uses-feature android:name="android.software.xr.api.spatial" android:required="true"/>
</application>

Using a value of true for the android:required attribute indicates that the app will only show to XR devices. Change it to false to make it available for both XR and non XR supported devices (phones). 

6. Configure Android XR emulator

Install Android XR system image

  • Open the SDK manager in Android Studio and select the SDK Platforms tab if it is not already selected. In the bottom-right corner of the SDK Manager window, make sure that the Show package details box is checked.
  • Under the Android 14 section, install the Android XR ARM 64 v8a or Android XR Intel x86_64 emulator image. Images can only run on machines with the same architecture (x86/ARM) as themselves.

Create an Android XR virtual device

  1. Open device manager, select XR under Form Factor on the left side of the window. Select XR device hardware profile from the list and click Next.
  2. On the next page, select the system image you installed previously. Creating the AVD by clicking Finish.
  3. Run the app on the AVD you just created.

Conclusion

Jetpack Compose for XR is a powerful and developer-friendly way for Android developers to create immersive XR experiences. It lowers the barrier to entry for XR development by building on established Android tools and principles, making it an exciting avenue for innovation.

For more detailed information:

https://developer.android.com/xr

https://developer.android.com/design/ui/xr/guides/foundations#modes