This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A Kotlin Multiplatform (Android + iOS) sample app demonstrating KStateMachine in a Compose UI context. It simulates a 2D hero character with states (Standing, Jumping, Ducking, AirAttacking, Shooting) controlled via on-screen buttons.
# Build all targets
./gradlew build
# Android debug APK
./gradlew :composeApp:assembleDebug
# Run Android unit tests
./gradlew :composeApp:testDebugUnitTest
# Run a single test class
./gradlew :composeApp:testDebugUnitTest --tests "org.example.project.MyTest"iOS builds require Xcode — open iosApp/iosApp.xcodeproj or use ./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64.
The app uses MVI with KStateMachine for state logic, Voyager for navigation/screen models, and Koin for DI.
UI (StickManGameScreen)
→ sendEvent(ControlEvent) → StickManGameScreenModel
→ machine.processEvent(event) [KStateMachine]
→ onTransitionComplete / onStateEntry callbacks
→ intent { state { ... } / sendEffect(...) }
→ MviModel.stateFlow / effectFlow
→ UI observes and redraws
-
StateControl.kt— defines all domain types:ControlEvent(sealed interface of input events) andHeroState(sealed class extending KStateMachine'sDefaultState).HeroStateinstances are used directly as KStateMachine state nodes. -
StickmanGameScreenModel.kt— VoyagerScreenModelthat constructs the KStateMachine withcreateStateMachineBlocking. The machine hasChildMode.PARALLELat the root with two parallel regions:"Movement"(Standing/Jumping/Ducking/AirAttacking) and"Fire"(NotShooting/Shooting). Transition callbacks callintent { }to updateMviModelstate and emit effects. -
Mvi.kt— Generic MVI infrastructure.MviModel<State, Effect>holds aStateFlowfor state and aChannel-backed flow for one-shot effects.MviModelHostprovidesintent { }(launches coroutine on model scope) andstateshorthand. -
ModelConst.kt— Game constants (JUMP_DURATION_MS,SHOOTING_INTERVAL_MS,INITIAL_AMMO) and data types:ModelData(state snapshot) andModelEffect(sealed interface of effects). -
Timers.kt—singleShotTimerandtickerFlowcoroutine utilities used inside state machineonEntry/onExitblocks. -
StickManGameScreen.kt— ComposeScreen(Voyager). UsesMutableInteractionSourceto detect press/release for Duck and Fire buttons, which map to pairedControlEvents. Hero sprite is selected by checking the active state list for combinations of movement + fire states via theList<HeroState>.hasState<T>()extension. -
App.kt— Root@Composablethat wrapsStickManGameScreenin a VoyagerNavigator. -
KoinModule.kt(androidMain) — Koin module registeringStickManGameScreenModelas a singleton. Note: this file has no package declaration and lives at the root ofandroidMain/kotlin/.
States are declared as HeroState subclass objects/classes and added with addState/addInitialState. Transitions are configured with transition<EventType> and transitionOn<EventType> (for conditional targets). The machine is polled via machine.processEvent(event) called from the screen model.
buildCreationArguments { doNotThrowOnMultipleTransitionsMatch = true } is set because both parallel regions can match the same event simultaneously.
class vs object for states: AirAttacking and Shooting are class (not object) because they hold mutable instance state — isDuckPressed: Boolean and shootingTimer: Job respectively. All other HeroState subclasses are object singletons.
activeStates always has exactly 2 entries: Because the machine root uses ChildMode.PARALLEL, ModelData.activeStates always contains one state from the "Movement" region and one from the "Fire" region. The UI's heroDrawable() function checks combinations of both to pick the correct sprite.
Koin initialization via androidx.startup: KoinInitializer implements Initializer<KoinApplication> and is registered in AndroidManifest.xml. Koin starts automatically — there is no custom Application subclass.