Builds Android and Kotlin Multiplatform features the way the AWL kmp-template does - shared repositories behind …Providing/…Handling interfaces that return AppResult, BaseViewModel with AppDispatchers and combine().stateIn() state, Compose screens on Navigation 3, Koin modules, Kermit logging, BuildKonfig flavors, and SKIE-bridged @Observable iOS ViewModels. Use whenever you add or change a screen, ViewModel, repository, interface, Koin binding, or error path in a KMP or Android project, for example “create viewmodel”, “build screen”, “add repository”, “wire koin”, “handle errors”, “expose to iOS”, “kmp feature”, “compose ui”. NOT for writing or fixing tests - use the testing-android skill for that.
Feature development for the AWL KMP template: shared/ owns logic, repositories, DI and the error model. androidApp/ owns Compose UI, ViewModels and navigation. iosApp/ is a thin SwiftUI shell over the Shared framework. Nothing UI-related is shared.
Find the layer. Shared logic goes to shared/src/commonMain, Android UI and ViewModels to androidApp/src/main, iOS-only accessors to shared/src/iosMain. In the Compose Multiplatform variant (cmp-template) UI and ViewModels also live in shared/commonMain.
Pick the pattern from the table below and read its reference before writing code.
Wire DI. Register repositories in sharedModule, ViewModels in appModule, iOS accessors in KoinHelper.
Compile-check with ./gradlew :androidApp:compileDevDebugKotlin (or :shared:compileKotlinIosSimulatorArm64 when the shared API changed) and hand tests to the testing-android skill.
Task
Pattern
Read when
New screen
Screen + internal Content composable, Screen key in Screens.kt, entry in AppNavigation
references/compose-ui.md for previews, Navigation 3, snackbars, events
New ViewModel
Extend BaseViewModel(dispatchers), single StateFlow via combine().stateIn()
references/viewmodel.md for BaseViewModel internals, local state, iOS @Observable ViewModels
New repository or service
…Providing (queries) and …Handling (commands) interfaces, one implementation, binds in Koin
references/interfaces.md for naming, Koin bindings, iOS accessors
Anything that can fail
Return AppResult<T>, wrap with appResultOf { }
references/error-handling.md for AppError cases, combinators, SKIE on iOS
Diagnostics
Kermit Logger
references/logging.md for severities and what not to log
Never throw across the iOS bridge. Public shared API returns AppResult<T> from com.appswithlove.kmp.error; exceptions thrown from Kotlin crash or vanish in Swift. Reserve @Throws for programmer errors.
Wrap failures with appResultOf { }, not try/catch or runCatching. It rethrows CancellationException so coroutine cancellation still works, and maps everything else through Throwable.toAppError().
ViewModels take AppDispatchers, not a raw CoroutineDispatcher, so tests can pass AppDispatchers(default = testDispatcher, io = testDispatcher).
Launch on catchingCoroutineScope, never viewModelScope. The scope carries the dispatcher and the exception handler that funnels crashes into submitError.
Depend on interfaces. A ViewModel takes GreetingProviding, not GreetingRepository, so tests can swap in an in-memory fake.
One StateFlow per ViewModel, built with combine(...).stateIn(...); local screen state lives in private MutableStateFlows that feed the combine.
kotlin.uuid.Uuid, never java.util.UUID; both modules opt in to ExperimentalUuidApi.
Per-flavor constants (BASE_URL) come from BuildKonfig in shared/build.gradle.kts; Android-only build info comes from BuildConfig.
Match ktlint via .editorconfig (200-char lines, trailing commas in multiline parameter lists, no wildcard imports).