Why Kotlin, and how we use it throughout Hidden Realms.
Kotlin 1.9 is the only programming language used across the entire Hidden Realms codebase — 128 source files, zero Java. We chose Kotlin for a specific set of features that directly solve real game development problems.
The most critical use of Kotlin in Hidden Realms is Coroutines. Our 60fps battle engine runs as a coroutine on a dedicated game loop dispatcher, ticking every 16 milliseconds to update player HP, boss AI state, combo timers, stagger meters, and visual effect queues — all without blocking the main UI thread. Without coroutines, this would require complex threading code. With Kotlin, it's a simple while(isBattleActive) loop in a suspend function.
Kotlin Flow powers all live data in the game. The player's HP, XP, level, and inventory are all exposed as Flow streams from the Room database. Every screen observes these flows via collectAsStateWithLifecycle() — meaning if you gain XP from the AR screen, the Map screen's HUD automatically updates the moment the database write completes, with no manual refresh or polling needed.
Every screen's UI state is modelled as a Kotlin sealed class. For example, BattleState holds every variable of the combat engine (player HP, boss HP, combo count, phase, cooldowns) as a single immutable data class. Each 16ms tick, a new copy is generated via .copy() with updated values, and the UI recomposes only the changed portions. This is pure functional state management, made clean by Kotlin's data class and copy() built-ins.
Our LootGenerator, SkillRegistry, and DamageSystem are all Kotlin object singletons — stateless, thread-safe, and available anywhere. Extension functions keep domain logic readable: String.toBattleResultPayload() parses navigation arguments; SavedStateHandle.toTreasureFallback() extracts fallback treasure data. No utility class boilerplate required.
Zero XML. Every screen in Hidden Realms is built with Jetpack Compose.
Jetpack Compose is Android's modern declarative UI framework. Instead of inflating XML layouts and finding views by ID, every screen is a @Composable function that describes what the UI should look like given the current state. When the state changes, Compose automatically recomposes only the parts that changed.
Every screen — Splash, Home, Map, AR Camera, Battle, Inventory, Profile, Settings — is written as a pure Composable function. There is not a single XML layout file in the project. This gives us:
▸ The Battle Screen HUD updates HP bars, combo counters, and skill cooldowns in real time at 60fps, driven by the BattleState StateFlow. Only the values that actually changed recompose — not the entire screen.
▸ The Map Screen renders the player's XP bar, coin count, and nearby node list from live Flow data. When a treasure is collected, the XP bar animates to its new value automatically.
▸ The AR Screen overlays a Compose UI (Radar HUD, directional arrows, TARGET LOCKED indicator, ambient orb collection buttons) on top of the SceneView camera feed in real time.
▸ The Inventory Screen is a LazyVerticalGrid with filter chips and category tabs — all in Compose, with animated transitions between filter states.
We use Material 3 (Material You) with a fully custom dark fantasy colour palette. Primary colours, typography, shape systems, and elevation overlays are all defined in our Theme.kt and applied globally through MaterialTheme. This ensures visual consistency across all screens without repeating style definitions.
The BOM (Bill of Materials) ensures all Compose library versions are perfectly compatible with each other — no version conflict headaches. One BOM version pins all related Compose libraries (foundation, material3, animation, navigation) to tested, compatible releases.
Compose's animation APIs power:
▸ The 28-particle ember animation on the Home screen (Canvas + LaunchedEffect)
▸ HP bar smooth fill transitions in battle (animateFloatAsState)
▸ The Level-Up popup scale + fade (AnimatedVisibility + scaleIn/fadeOut)
▸ Screen-to-screen slide transitions (NavHost animated content)
A disciplined 3-layer architecture that keeps 128 Kotlin files manageable and testable.
MVVM (Model-View-ViewModel) with Clean Architecture principles separates the codebase into three strict layers, each with a single responsibility. No screen directly touches the database. No ViewModel knows what a Composable looks like.
All @Composable screen functions. They observe StateFlow from their ViewModel using collectAsStateWithLifecycle(). They send user events (button taps, swipe gestures) up to the ViewModel. They never contain business logic — only rendering and user interaction.
Example: BattleScreen observes BattleViewModel.uiState and renders the HP bars, combo counter, and action buttons. When the player taps Attack, it calls viewModel.onAttack() — that's the entire responsibility of the screen.
Each feature module has a dedicated @HiltViewModel:
▸ MapViewModel — manages GPS tracking, node spawning, proximity detection, treasure collection
▸ BattleViewModel — runs the 60fps game loop, manages BattleState, resolves rewards
▸ ARViewModel — handles AR node detection, radar signal processing, capture events
▸ InventoryViewModel — handles item filtering, equip logic, stat recalculation
▸ GlobalLevelUpViewModel — observes player level globally to fire level-up popups on any screen
Each ViewModel exposes a single UiState as a StateFlow and accepts typed events from the UI. Business logic lives here — calculation, validation, orchestration.
All database reads and writes go through Repository interfaces:
▸ PlayerRepository — player stats, XP, level, skills
▸ TreasureRepository — node spawning, collection, respawn tracking
▸ InventoryRepository — item storage, equip state, catalog
▸ PortalRepository — portal node management
The Repository pattern means ViewModels depend on interfaces, not concrete implementations. This makes the code testable (mock repositories) and keeps the database completely hidden from the UI layer.
Hilt automatically wires together every component in Hidden Realms — zero manual instantiation.
Dagger Hilt is a compile-time dependency injection framework. It generates the code needed to construct and inject every class at build time — meaning there is no runtime reflection, no performance overhead, and no null-pointer surprises from incorrectly initialised dependencies.
Every ViewModel in the game is annotated with @HiltViewModel and has its dependencies injected via @Inject constructor:
▸ BattleViewModel receives PlayerRepository, InventoryRepository, and AudioController injected automatically.
▸ MapViewModel receives TreasureRepository, PortalRepository, PlayerRepository, and LocationClient — all injected, none manually constructed.
▸ ARViewModel receives PortalRepository and PlayerRepository injected at screen launch.
The AppDatabase (Room) is provided as a @Singleton via a Hilt Module — one database instance shared across the entire app. The GameStateManager (which tracks Demo vs Real game mode) is similarly a @Singleton, ensuring all ViewModels always see the same game mode state.
Without Hilt, every screen would need to manually construct its ViewModel's dependencies, passing them down through constructor chains. With Hilt, the screen simply asks for the ViewModel with hiltViewModel() and Hilt handles everything. 128 files. Zero manual wiring.
All game data — player progress, inventory, spawned nodes, battle history — lives in a local Room database. No server. No internet. 100% offline.
Room is Android's official SQLite abstraction library. It provides compile-time verified SQL queries, reactive Flow-based queries that auto-update the UI, and type-safe data access through DAO (Data Access Object) interfaces.
▸ player — Level, XP, HP, ATK, DEF, Speed, Coins, Rank title, unlocked skills
▸ inventory_items — All collected loot items, their rarity, category, isEquipped, equipSlot
▸ treasure_catalog — Treasure item definitions and base stat values
▸ treasures — Spawned treasure nodes (GPS coordinates, rarity, collected status)
▸ treasure_spawn_cells — GPS grid cells that have been seeded with treasure nodes
▸ portals — Spawned portal nodes (GPS coordinates, rarity, boss assignment)
▸ portal_spawn_cells — GPS grid cells that have been seeded with portals
▸ battle_results — History of every battle (boss name, win/loss, XP earned, timestamp)
▸ xp_log — Record of every XP gain event and its source
▸ achievements — Achievement tracking (conditions, unlock status, rewards)
Every DAO query that returns live data returns a Flow<T>. This means:
▸ Player stats shown on the Profile screen update automatically when XP is added from the Map screen.
▸ The Inventory screen's item grid refreshes automatically when a new item is dropped after a battle.
▸ The Level-Up popup fires globally the moment the player's level column in the player table changes.
No manual refresh. No polling. One database write cascades to every observing screen simultaneously.
The database is currently at version 13. We maintain a Migrations.kt file with versioned migration scripts for every schema change — meaning player data is never lost when the app updates. Existing saves from v1 can be migrated forward through each version step automatically.
A single, type-safe navigation graph connecting all 8 screens of Hidden Realms.
Navigation Compose provides a NavHost — a single composable that manages the entire back stack and screen transitions for the app. Every screen is a composable destination with a string route, and navigation arguments are passed directly in the route URL (similar to web routing).
Hidden Realms has 8 navigable screens, all defined in AppNavigation.kt:
▸ splash → Loading screen, initialises DB and player
▸ home → Main menu with ember particle animation
▸ settings → Game settings (sound, haptics, demo mode)
▸ map → Live GPS world map (the core hub)
▸ inventory → Item grid, filter, equip screen
▸ profile → Character stats, rank, battle history
▸ battle/{portalId}/{bossName}/{difficulty}/{nodeRarity} → Boss fight screen
▸ ar/{nodeId}/{poiType}/{rarity}/{distanceMeters} → AR Camera screen
Battle and AR screens receive parameters through their route strings. For example, navigating to a battle passes the portal ID, boss name, difficulty level, and node rarity directly in the URL path:
"battle/portal_abc/Shadow%20Drake/3/EPIC"
The destination screen reads these arguments from SavedStateHandle — type-safe, back-stack aware, and automatically restored if the process is killed and the user returns.
When a battle ends, the result (WIN/LOSE, XP earned, loot dropped) is passed back to the Map screen via SavedStateHandle as a string payload. The Map screen reads this result when it becomes the active destination again, triggers the reward animation, and updates the player's stats — all without requiring a shared ViewModel or event bus.
All screen transitions use Compose's built-in animation system — slide-in/slide-out for most screens, fade for the battle screen — configured directly in the NavHost composable definitions.
Precise, battery-efficient real-world positioning powering the entire Hidden Realms game world.
Google Play Services' Fused Location Provider API is the gold standard for Android location. Rather than directly querying GPS hardware (slow, power-hungry), it intelligently fuses three signal sources:
▸ GPS satellite data — highest accuracy outdoors, ~3–10m precision
▸ Wi-Fi positioning — accurate indoors and in urban canyons
▸ Cell tower triangulation — fallback for areas with no GPS or Wi-Fi
The fusion algorithm automatically selects the best available source and blends them for the smoothest, most accurate result — while minimising battery drain.
The MapViewModel registers a location callback with the Fused Location Provider when the Map screen is active. We request updates with a 2-second interval and a minimum displacement of 5 metres — meaning the system only wakes our app when you've actually moved, saving battery during idle moments.
Every location update fires the WorldSpawnerSystem. The system checks: has the player moved far enough from the last spawn origin to warrant generating new nodes? If yes, it uses the new GPS coordinates as the seed for deterministic node placement. Your real GPS coordinates directly create the fantasy world around you.
Total walking distance is accumulated in the player's Room database record. Every 500 metres crossed triggers a 25 XP reward — tracked by comparing the GPS distance delta on each location update. Walk 5 km in a real session and you'll earn a meaningful passive XP bonus, separate from battle and collection rewards.
When a Portal node is tapped, the current GPS coordinates and the node's GPS position are passed to the AR screen. The AR system uses this real-world offset to determine where in physical space the portal should appear — so the 3D portal object is anchored to a direction that genuinely points toward the GPS node's location relative to where you're standing.
We request ACCESS_FINE_LOCATION (GPS-level precision) and ACCESS_COARSE_LOCATION (cell tower fallback). On Android 10+, we also request FOREGROUND_SERVICE_LOCATION to keep location tracking alive while the map is in focus.
Precise, battery-efficient real-world positioning powering the entire Hidden Realms game world.
Google Play Services' Fused Location Provider API is the gold standard for Android location. Rather than directly querying GPS hardware (slow, power-hungry), it intelligently fuses three signal sources:
▸ GPS satellite data — highest accuracy outdoors, ~3–10m precision
▸ Wi-Fi positioning — accurate indoors and in urban canyons
▸ Cell tower triangulation — fallback for areas with no GPS or Wi-Fi
The fusion algorithm automatically selects the best available source and blends them for the smoothest, most accurate result — while minimising battery drain.
The MapViewModel registers a location callback with the Fused Location Provider when the Map screen is active. We request updates with a 2-second interval and a minimum displacement of 5 metres — meaning the system only wakes our app when you've actually moved, saving battery during idle moments.
WORLD SPAWNER TRIGGER:
Every location update fires the WorldSpawnerSystem. The system checks: has the player moved far enough from the last spawn origin to warrant generating new nodes? If yes, it uses the new GPS coordinates as the seed for deterministic node placement. Your real GPS coordinates directly create the fantasy world around you.
DISTANCE MILESTONE XP:
Total walking distance is accumulated in the player's Room database record. Every 500 metres crossed triggers a 25 XP reward — tracked by comparing the GPS distance delta on each location update. Walk 5 km in a real session and you'll earn a meaningful passive XP bonus, separate from battle and collection rewards.
AR SCREEN HANDOFF:
When a Portal node is tapped, the current GPS coordinates and the node's GPS position are passed to the AR screen. The AR system uses this real-world offset to determine where in physical space the portal should appear — so the 3D portal object is anchored to a direction that genuinely points toward the GPS node's location relative to where you're standing.
We request ACCESS_FINE_LOCATION (GPS-level precision) and ACCESS_COARSE_LOCATION (cell tower fallback). On Android 10+, we also request FOREGROUND_SERVICE_LOCATION to keep location tracking alive while the map is in focus.
Real-world Augmented Reality — 3D portal objects rendered over your live camera feed.
SceneView is a high-level Jetpack Compose-compatible wrapper over Google ARCore. ARCore is Google's Augmented Reality platform — it uses the phone's camera and IMU sensors to understand the physical geometry of the real world in real time, detecting surfaces, tracking motion, and maintaining a persistent coordinate system that doesn't drift as you move.
When the AR screen launches, SceneView initialises ARCore's tracking session. We create an AR anchor at a world-space position calculated from the GPS offset between the player and the target portal node. A 3D glowing portal model is attached to this anchor and rendered in SceneView's OpenGL scene — overlaid on the live camera feed. The portal stays fixed in world space as you move and rotate your phone.
The AR screen reads the angular difference between your camera's current facing direction and the direction toward the portal's world-space anchor. This angular offset drives the animated directional arrows on the Radar HUD — arrows that tighten as you rotate toward the portal and converge when it enters your camera frame directly.
When the portal's world-space anchor is within the camera's view frustum (i.e., visible in frame), the system fires the "TARGET LOCKED" state — pulsing cyan rings expand outward from the portal's screen position, and a confirmation tone plays. Hit CAPTURE and the portal-tear cinematic triggers.
Ambient floating orbs are spawned as 3D SceneView nodes placed at randomised positions in AR space around the player. These are not GPS-anchored — they float freely in the physical space immediately around you, visible through the camera. Collecting them by tapping their on-screen hit area rewards real XP and Gold.
ARCore renders on its own OpenGL thread, while Compose UI runs on the main thread. To communicate between them without race conditions, we use an AtomicReference<NodeSignal> — a lock-free, thread-safe reference that the ARCore thread writes to and the Compose thread reads from. This guarantees zero frame drops or crashes during the critical node-detection window.
The build system that compiles, packages, and configures Hidden Realms — written entirely in Kotlin.
Gradle is Android's standard build system. The Kotlin DSL (Domain Specific Language) variant replaces the traditional Groovy .gradle files with .gradle.kts files written in Kotlin. This means the same language used to write the app is also used to write the build configuration — giving us IDE autocomplete, compile-time type checking, and refactoring support inside build files.
Gradle manages two build variants:
▸ debug — used during development. Includes logging, debug symbols, no code shrinking.
▸ release — production build. Enables R8 code shrinking, resource optimisation, and ProGuard rules. Produces the final APK for distribution.
We use KSP as the annotation processor for both Room and Hilt. KSP is the modern replacement for KAPT — it runs annotation processing natively in Kotlin, making build times significantly faster. Room generates its DAO implementation classes at compile time through KSP. Hilt generates its dependency injection factories through KSP. Both happen in a single build step.
All library versions are centralised in the Gradle build files using the BOM (Bill of Materials) pattern for Compose, and explicit version pinning for all other libraries. This prevents dependency conflicts and makes upgrading libraries a single-line change.
Current build times: ~49 seconds for incremental debug builds (most tasks cached), ~1m 57s for full clean builds. The project compiles 128 Kotlin source files, processes 10 Room table entities, and generates Hilt factories for every @HiltViewModel in a single Gradle run.
Hidden Realms runs on Android 8.0 Oreo and above — covering ~95% of active Android devices worldwide.
API 26 was chosen as the minimum for a specific technical reason: it is the lowest API level that fully supports all core features Hidden Realms requires:
▸ ARCore (Google's AR platform) requires a minimum of Android 7.0 (API 24), but SceneView's full feature set is stable from API 26 onward.
▸ Foreground Services with proper notification channels (required for background GPS tracking) became mandatory from API 26.
▸ Kotlin Coroutines and Flow work identically across all supported API levels.
▸ Room database with reactive Flow queries is supported from API 21 onward — well within our range.
▸ Jetpack Compose requires minimum API 21.
By targeting API 26 as minimum, we exclude only the oldest, least capable devices — those running Android 5 or 6 from 2015–2016 — while ensuring our game reaches the vast majority of Android users without compatibility workarounds.
When the app launches, we verify ARCore availability on the device. If the device does not support ARCore (older hardware without the required camera and sensor capabilities), the AR Camera feature gracefully degrades — Portal nodes can be captured from the Map screen directly, without the AR experience. The rest of the game runs identically.
Hidden Realms is fully optimised for Android 14 — the latest Android release at time of development.
Targeting API 34 means Hidden Realms is built and tested against Android 14's strictest behaviour rules, receiving access to the latest platform APIs and performance improvements.
▸ FOREGROUND SERVICE TYPES — Android 14 requires apps to explicitly declare the type of foreground service they use. We declare our GPS tracking service as type="location" in the AndroidManifest, satisfying Android 14's strict foreground service requirements.
▸ PHOTO/MEDIA PERMISSIONS — Android 14 introduced granular media permissions. Our app correctly requests only the specific permissions it needs (camera for AR, location for GPS) and handles the new permission model introduced in API 33–34 without breaking on older devices.
▸ PREDICTIVE BACK GESTURE — Android 14 fully enables predictive back navigation. Our Navigation Compose setup correctly responds to the system back gesture, providing smooth back-navigation animations across all screens.
▸ PERFORMANCE — Apps targeting API 34 receive automatic access to Android 14's memory and CPU scheduling improvements. On modern devices, this contributes to the smooth 60fps battle engine performance.
▸ GOOGLE PLAY REQUIREMENT — As of 2024, Google Play requires all new app submissions to target at least API 33. Targeting API 34 ensures Hidden Realms is fully compliant for Play Store distribution.
Hidden Realms is a full-scale Android project — 128 Kotlin source files organised across 8 feature modules.
The codebase is structured following Clean Architecture's feature-module pattern. Every feature of the game has its own package with clearly separated layers:
▸ feature/progression — Player stats, XP system, levelling, rank titles, profile screen
▸ feature/battle — Combat engine, boss AI, skill system, damage calculation, reward pipeline
▸ feature/map — GPS tracking, world spawner, treasure collection, map screen
▸ feature/ar — AR camera, radar HUD, ambient orb system, node capture
▸ feature/inventory — Loot generation, item storage, equipment system, inventory screen
▸ feature/treasure — Treasure node data, spawn cell management, collection logic
▸ feature/portal — Portal node data, boss assignment, portal spawn management
▸ feature/settings — Settings screen, toggle persistence
▸ core/database — Room AppDatabase, all DAOs, migrations, converters
▸ core/sensory — AudioController (BGM + SFX), HapticEngine
▸ core/state — GameStateManager (Demo/Real mode)
▸ core/model — Shared data models (Rarity, ItemCategory, etc.)
▸ core/designsystem — Shared Compose components (buttons, floating XP text, overlays)
▸ navigation — AppNavigation NavGraph, Screen sealed class, route definitions
128 Kotlin files is comparable to a production-grade Android application. The project includes custom game engine code (BattleEngine, WorldSpawnerSystem), a full AR integration (SceneView + ARCore), real-time GPS tracking, a 10-table SQLite database, reactive state management, and a complete UI layer — all in a single-module Android app.
A fully designed relational database schema storing every piece of persistent game data — currently at schema version 13.
The single most important table. One row per installation.
Columns: id, level, currentXp, maxHp, attackPower, defense, speed, coins, title (rank),
unlockedSkillsStr, equippedSkillsStr, availableSkillPoints,
battlesWon, battlesLost, bossesDefeated, treasuresCollected,
portalsClosed, damageDealt, totalDistance
Every item ever collected. Each row is one item instance.
Columns: id, itemId, displayName, category, rarity, isEquipped, equipSlot, acquiredAt
Item definitions — the blueprint for what items can exist and their base stat values.
Every GPS-positioned treasure node that has been spawned in the real world.
Columns: id, latitude, longitude, type, rarity, rewardId, isCollected, spawnedAt
GPS grid cells (50m × 50m) that have been seeded. Tracks which areas of the real
world already have nodes, preventing duplicate spawning in the same grid cell.
Every GPS-positioned portal node. Same structure as treasures but linked to boss encounters.
Columns: id, latitude, longitude, rarity, bossId, isClosed, spawnedAt
GPS grid cells seeded with portal nodes. Same purpose as treasure_spawn_cells.
Full history of every battle ever fought.
Columns: id, bossName, isVictory, xpEarned, coinsEarned, lootItemId, foughtAt
Every XP gain event with source tracking.
Columns: id, amount, source ("Battle", "Treasure", "AR Collection", "Exploration"), timestamp
Achievement definitions and unlock state.
Columns: id, name, description, condition, isUnlocked, unlockedAt
The database has gone through 13 schema versions during development. Every version change is accompanied by a migration script in Migrations.kt that safely transforms existing data to the new schema. Players who installed the game at v1 have their data automatically migrated through each version — no progress is ever lost on an app update.
The schema is exported to a JSON file (exportSchema = true) which serves as a version-controlled record of every schema change, making it easy to audit what changed between any two versions.