ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-05-03Intermediate

Getting Started with AR on Android Using io.github.sceneview — Working Past AnchorNode Errors

A practical guide to building Android AR features with SceneView (io.github.sceneview): AnchorNode error fixes, ARCore session and install handling, plane visualization, and combining Antigravity AI for dynamic scene generation.

SceneViewARCore2Android27AR development2AnchorNodeAntigravity338app-dev49

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.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-04-02
Building AR Apps with Antigravity: A Beginner's Guide to ARKit & ARCore 2026
Learn how to build augmented reality apps with Antigravity's AI agents, ARKit (iOS), and ARCore (Android). This step-by-step guide covers the 2026 AR market landscape, environment setup, and hands-on code examples.
App Dev2026-06-26
Hand Over Generation and Shipping, but Never the Signing Key — Designing Key Custody and Handover for an AI-Driven Pipeline
Even in an era where AI Studio and Antigravity take over everything from generation to internal-test shipping, the app signing key is in a class of its own. Lose it, and that app can never be updated again. As a solo developer who has run several apps for years, here is how I design key custody — separating the upload key from the app signing key, storing it, and planning the handover for the worst case.
App Dev2026-06-26
Green in the Embedded Emulator, Broken on the First Real Device — Putting a Parity Gate on AI-Generated Compose Apps
AI Studio now generates Kotlin/Compose apps from a prompt, runs them in an embedded emulator, and pushes them to a real device over USB — all from one screen. Yet a screen that passed in the emulator can break the first time it lands on a real phone. As a solo developer running several apps, here is how I put a gate that catches device parity issues before they ship.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →