Most developers who pick up io.github.sceneview for Android AR development hit the same wall almost immediately: AnchorNode errors before they've placed a single 3D object. English-language documentation is decent, but examples often lag behind the current API, and mixing ARCore session management into the mix makes things confusing fast.
SceneView is a higher-level abstraction over ARCore. Since Sceneform was deprecated in 2021, it's become the practical default for Android AR development. When I first tried AR in a personal project, I lost the first couple of days to lifecycle and session-timing errors — so the sections below are ordered roughly by how early you'll trip over each one.
Setup
Add the latest SceneView dependency:
// build.gradle.kts (Module)
dependencies {
implementation("io.github.sceneview:arsceneview:2.3.0")
}Update AndroidManifest.xml for ARCore:
<manifest>
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.ar" android:required="true" />
<application>
<meta-data
android:name="com.google.ar.core"
android:value="required" />
</application>
</manifest>Switching required to optional lets you ship to non-AR devices too, but then a runtime availability check (covered below) becomes mandatory. A good rule: use required when AR is the core experience, optional when it's a bonus feature.
Adding ARSceneView to Your Layout
With Compose, wrap it in AndroidView:
@Composable
fun ARScreen() {
var arSceneView: ARSceneView? = remember { null }
AndroidView(
factory = { context ->
ARSceneView(context).also { sceneView ->
arSceneView = sceneView
}
},
modifier = Modifier.fillMaxSize()
)
}For XML layouts, place it directly:
<io.github.sceneview.ar.ARSceneView
android:id="@+id/arSceneView"
android:layout_width="match_parent"
android:layout_height="match_parent" />Fixing AnchorNode Errors
The most common crash when starting with SceneView:
java.lang.IllegalStateException: AnchorNode must be attached to an ARSceneView
The cause is almost always trying to create or attach an AnchorNode before the ARCore session is fully established.
Wrong approach:
// ❌ Trying to create AnchorNode immediately in onViewCreated
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val anchorNode = AnchorNode(arSceneView.engine, anchor) // crashes
arSceneView.addChild(anchorNode)
}Correct approach:
// ✅ Use the onSessionResumed callback
arSceneView.onSessionResumed = { session ->
// Session is valid — safe to set up AR interactions
setupARContent()
}
private fun setupARContent() {
arSceneView.onTapAr = { hitResult, _ ->
val anchor = hitResult.createAnchor()
val anchorNode = AnchorNode(arSceneView.engine, anchor)
anchorNode.addChild(
ModelNode(
modelInstance = modelLoader.createModelInstance(modelFile),
autoAnimate = true
)
)
arSceneView.addChild(anchorNode)
}
}Wait for onSessionResumed before setting up any AR content. Creating the anchor from a tap's hitResult via createAnchor() also avoids a second class of error: trying to anchor at a coordinate where no plane has been detected yet.
Requesting ARCore Installation Properly
An easy one to miss: the device supports ARCore, but the app still crashes — because ARCore (Google Play Services for AR) isn't installed, or is out of date. Wiring up requestInstall eliminates this entire category of crash.
// Check ARCore availability and prompt installation if needed
private var installRequested = false
private fun ensureArCore(): Boolean {
return try {
when (ArCoreApk.getInstance().requestInstall(requireActivity(), !installRequested)) {
ArCoreApk.InstallStatus.INSTALL_REQUESTED -> {
// Sent the user to the install flow; wait for the next onResume
installRequested = true
false
}
ArCoreApk.InstallStatus.INSTALLED -> true
}
} catch (e: UnavailableUserDeclinedInstallationException) {
// The user declined installation
showFallbackUI()
false
}
}The key is the installRequested flag. After requestInstall sends the user to the install screen, it gets called again on the onResume right after they return. Without the flag — always passing true to force a prompt — you'll loop the dialog forever.
Loading 3D Models
SceneView handles .glb models cleanly:
class ARFragment : Fragment() {
private lateinit var modelLoader: ModelLoader
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val sceneView = view.findViewById<ARSceneView>(R.id.arSceneView)
modelLoader = ModelLoader(sceneView.engine, requireContext())
sceneView.onSessionResumed = { _ ->
setupTapToPlace(sceneView)
}
}
private fun setupTapToPlace(sceneView: ARSceneView) {
sceneView.onTapAr = { hitResult, _ ->
lifecycleScope.launch {
val modelInstance = modelLoader.loadModelInstance("models/robot.glb")
?: return@launch
val anchor = hitResult.createAnchor()
val anchorNode = AnchorNode(sceneView.engine, anchor).apply {
addChild(ModelNode(
modelInstance = modelInstance,
scaleToUnits = 0.5f // scale in meters
))
}
sceneView.addChild(anchorNode)
}
}
}
}Place your .glb file in the assets/ directory and reference it by relative path. Watch the polygon count: AR runs camera tracking and rendering at the same time, so there's far less headroom than desktop 3D. Keep placement models under a few tens of thousands of polygons and compress textures to 1–2K to keep the frame rate steady.
Visualizing Plane Detection to Guide Users
A surprisingly common UX problem in AR: users don't know where they're allowed to tap. SceneView can render detected planes as a point cloud, which shows people exactly where a model can go.
// Render detected planes as dots
arSceneView.planeRenderer.isEnabled = true
arSceneView.onTapAr = { hitResult, _ ->
placeModel(hitResult)
// Hide the guides after placement to reduce visual noise
arSceneView.planeRenderer.isEnabled = false
}Showing the plane guide first and hiding it after the first model is placed makes the experience far more legible. On a real device you'll notice that, without guides, many users freeze and never move the camera to let tracking build up.
Combining with Antigravity AI
One interesting direction: using Antigravity AI to generate AR scene layouts from natural language, then applying them to the SceneView scene graph.
// Generate an AR scene configuration with Antigravity AI
suspend fun generateARSceneFromPrompt(prompt: String): ARSceneConfig {
val client = AntigravityClient(apiKey = "YOUR_API_KEY")
val response = client.generateScene(
prompt = prompt,
outputFormat = "structured_json",
parameters = mapOf(
"include_position" to true,
"include_scale" to true,
"coordinate_system" to "ar_world"
)
)
return ARSceneConfig.fromJson(response.content)
}
// Apply the generated configuration to the AR scene
fun applySceneConfig(config: ARSceneConfig, sceneView: ARSceneView) {
config.objects.forEach { objectConfig ->
val position = Float3(
objectConfig.position.x,
objectConfig.position.y,
objectConfig.position.z
)
// place each object at the AI-specified position
}
}Letting an AI model handle spatial layout decisions and mapping them onto a physical space opens up interesting possibilities for dynamic, content-driven AR. One caveat: the coordinates a model returns won't always land on a detected plane, so snap each one to the nearest plane with a hit test before using it — otherwise you'll get objects floating in mid-air.
Cleaning Up Anchors and Managing Drift
In screens that stay open a while, placed anchors pile up. ARCore keeps tracking the environment for every anchor, so leftover anchors make tracking heavier and make positional "drift" more noticeable. Release anchors you're done with explicitly.
// Clear all placed models
fun clearPlacedModels(sceneView: ARSceneView) {
sceneView.children
.filterIsInstance<AnchorNode>()
.forEach { node ->
node.anchor.detach() // release the anchor from the ARCore session
sceneView.removeChild(node)
}
}Calling detach() on a reset button or screen transition keeps things stable over long sessions. In my own testing, leaving more than ten anchors active while moving around made the older objects visibly drift away from their real-world positions over time.
Lifecycle Management
SceneView is lifecycle-sensitive. Hook into Fragment or Activity callbacks:
override fun onResume() {
super.onResume()
arSceneView.onResume()
}
override fun onPause() {
super.onPause()
arSceneView.onPause()
}
override fun onDestroy() {
super.onDestroy()
arSceneView.destroy() // release engine resources
}Missing destroy() causes memory leaks. The Filament engine that SceneView uses holds substantial native resources that won't be released by the GC alone.
Handling ARCore-Unsupported Devices
Always check for ARCore availability before proceeding:
if (!ArCoreApk.getInstance().checkAvailability(requireContext()).isSupported) {
showFallbackUI()
return
}SceneView abstracts away a lot of the ARCore boilerplate, but device compatibility checks need to happen at the app layer.
Where to Go Next
Once the basics are working, the natural next areas to explore are light estimation (to make placed models blend with real-world lighting) and multi-anchor spatial mapping for more complex scenes. AR development really does require physical device testing from the start — emulators can't replicate ARCore's camera tracking and surface detection. Get one supported device, drive a single tap-to-place model end to end first, and build out from there.