Develop

FeatureDevSample

Updated: May 8, 2026

Overview

This sample demonstrates how to build reusable SpatialFeature modules as separate Android library Gradle modules. Unlike other samples that define features inline, this is the only sample in the Spatial SDK Samples corpus that packages custom features as standalone libraries for reuse across multiple projects. You learn how to structure feature modules, define custom components, and integrate both pure Kotlin and native C++ code.

Learning objectives

Complete this guide to learn how to:
  • Build reusable SpatialFeature modules as separate Android library Gradle modules
  • Implement the SpatialFeature interface with custom components and systems
  • Define custom ECS components via XML schemas
  • Integrate native C++ code via JNI in a feature module
  • Publish feature modules with maven-publish for distribution across projects

Requirements

  • A Meta Quest headset running Horizon OS
  • Android Studio with a working development environment
  • Android NDK installed (required to build the nativefeature module with native C++ code)
For detailed build prerequisites, see the sample’s README.

Get started

Clone the Meta Spatial SDK Samples repository and open the FeatureDevSample directory in Android Studio. Build and deploy to your Meta Quest device. You see two animated entities: a basketball bobbing up and down using native C++ calculations, and a robot pulsing in scale using pure Kotlin calculations.

Explore the sample

The sample is organized into three Gradle modules: :app (main application), :kotlinfeature (pure Kotlin feature library), and :nativefeature (Kotlin + JNI feature library).
File / ModuleWhat it demonstratesKey APIs / patterns
settings.gradle.kts
Multi-module project structure
:app, :nativefeature, :kotlinfeature
app/FeatureDevSampleActivity.kt
Feature registration and scene composition loading
registerFeatures(), glXFManager.inflateGLXF()
app/Panels.kt
Compose UI with Horizon OS UI Set
SpatialTheme, darkSpatialColorScheme()
kotlinfeature/PulsingFeature.kt
Pure Kotlin SpatialFeature implementation
componentsToRegister(), systemsToRegister()
kotlinfeature/PulsingSystem.kt
ECS system with per-frame animation
Query.where { has(...) }.eval(), Scale(Float)
kotlinfeature/components.xml
Component schema definition
FloatAttribute, ComponentSchema.xsd
kotlinfeature/build.gradle.kts
Library module configuration
android.library, registrationsClassName, maven-publish
nativefeature/NativeBobbingFeature.kt
JNI-integrated SpatialFeature
preRuntimeOnCreate(), loadLibrary()
nativefeature/NativeBobbingSystem.kt
JNI-based animation system
external fun, delete(entity) cleanup
nativefeature/native_bobbing.cpp
Native C++ calculation via JNI
JNI naming convention, JNI_OnLoad
nativefeature/CMakeLists.txt
CMake build for native library
C++17, libnative_bobbing.so
nativefeature/build.gradle.kts
Library module with CMake
externalNativeBuild.cmake, maven-publish

Runtime behavior

When you run the sample on your Meta Quest device, you see a scene with a basketball entity bobbing up and down (position offset calculated by native C++ code) and a robot entity pulsing in scale (calculated by pure Kotlin code). The scene includes a skybox and environment lighting. A UI panel displays information about the sample features.

Key concepts

Packaging features as library modules

The feature modules use the android.library plugin combined with meta.spatial.plugin. Feature modules depend only on meta-spatial-sdk-base and meta-spatial-sdk-toolkit, avoiding app-specific dependencies. The registrationsClassName setting in build.gradle.kts controls the name of the generated component registration class, preventing classpath conflicts when multiple modules define components.
// kotlinfeature/build.gradle.kts
spatial {
    components {
        registrationsClassName = "PulsingFeatureComponentRegistrations"
    }
}
For the full build configuration, see kotlinfeature/build.gradle.kts.

Component definition via XML

Components defined in src/main/components/components.xml are processed by the Gradle plugin at build time into generated Kotlin data classes and a registration object. The generated object has an all() method returning List<ComponentRegistration>.
<!-- kotlinfeature/components.xml -->
<Component name="Pulsing">
    <FloatAttribute name="minScale" defaultValue="0.8f" />
    <FloatAttribute name="maxScale" defaultValue="1.2f" />
    <FloatAttribute name="frequency" defaultValue="1.0f" />
</Component>
The feature class returns the generated registrations in componentsToRegister():
// PulsingFeature.kt
override fun componentsToRegister(): List<ComponentRegistration> {
    return PulsingFeatureComponentRegistrations.all()
}
For the complete XML schema and feature implementation, see kotlinfeature/components.xml and PulsingFeature.kt.
Learn more: Components

Pure Kotlin vs JNI features

PulsingFeature is a pure Kotlin implementation. It overrides componentsToRegister() and systemsToRegister() without requiring native libraries. In contrast, NativeBobbingFeature uses preRuntimeOnCreate() to load a native library and includes a CMake build for the C++ code.
// NativeBobbingFeature.kt
override fun preRuntimeOnCreate(savedInstanceState: Bundle?) {
    loadLibrary("native_bobbing")
}
The native C++ implementation calculates the bobbing offset using std::sin. For the complete native implementation, see native_bobbing.cpp.

ECS Query pattern

Both systems use Query.where { has(...) }.eval() to find entities that have the specified components. The query returns a Sequence<Entity> that the system iterates to apply logic.
// PulsingSystem.kt
val query = Query.where { has(Pulsing.id) }
for (entity in query.eval()) {
    val pulsing = entity.getComponent<Pulsing>()
    entity.setComponent(Scale(currentScale))
}
For the complete system implementations, see PulsingSystem.kt and NativeBobbingSystem.kt.

Maven publishing for distribution

Both feature modules configure maven-publish with publishToMavenLocal support. This allows you to publish features to a Maven repository and consume them from other projects.
// kotlinfeature/build.gradle.kts
register<MavenPublication>("release") {
    artifactId = "pulsing-feature"
    afterEvaluate { from(components["release"]) }
}
Publish to your local Maven cache with ./gradlew :kotlinfeature:publishToMavenLocal.
For the full publishing configuration, see kotlinfeature/build.gradle.kts.

Extend the sample

  • Add a third feature module (e.g., a rotation or color-shifting feature) and register it in the app
  • Publish the feature modules to a shared Maven repository and consume them from another Spatial SDK app
  • Override getDependencies() to declare that one feature requires another, ensuring correct initialization order
  • Use earlySystemsToRegister() or lateSystemsToRegister() to control system execution order relative to other systems