When to Use
Use this skill when working on:
- •Implementing new features following vertical slice architecture
- •Fixing bugs in existing feature modules (api, data, presentation, ui, wiring)
- •Refactoring code within feature boundaries
- •Adding or modifying repositories, ViewModels, and data layers
- •Creating or updating Compose UI screens
- •Writing or updating tests (Kotest in androidUnitTest/)
- •Adding new dependencies via version catalog
- •Making changes to Koin DI wiring modules
Do NOT use this skill for:
- •Product/PRD decisions → switch to Product Design Mode
- •Visual design/animations → switch to UI/UX Design Mode
- •SwiftUI iOS screens → switch to SwiftUI Screen Implementation Mode
- •Test planning/strategy → switch to Testing Strategy Mode
- •Ktor backend changes → switch to Backend Development Mode
Essential Workflows
Workflow 1: Implement New Feature (Vertical Slice)
To add a complete feature module:
- •
Create feature directory structure in
features/<feature>/with layers:- •
api/- interfaces, domain models, navigation routes - •
data/- API services, DTOs, mappers, repository implementations - •
presentation/- ViewModels, UI state classes - •
ui-material/- Material Design 3 Compose screens - •
ui-unstyled/- Compose Unstyled screens - •
wiring/- Koin modules for repos and ViewModels - •
wiring-ui-material/- Material navigation registration - •
wiring-ui-unstyled/- Unstyled navigation registration
- •
- •
Define repository interface in
:api:kotlin// features/<feature>/api/src/commonMain/.../<Feature>Repository.kt interface <Feature>Repository { suspend fun getData(): Either<RepoError, List<Data>> } - •
Implement repository in
:datawith Either boundary:kotlin// features/<feature>/data/src/commonMain/.../<Feature>RepositoryImpl.kt internal class <Feature>RepositoryImpl( private val api: <Feature>ApiService ) : <Feature>Repository { override suspend fun getData(): Either<RepoError, List<Data>> = Either.catch { api.getData().map { it.toDomain() } } .mapLeft { it.toRepoError() } } - •
Create factory function in
:data:kotlinfun <Feature>Repository(api: <Feature>ApiService): <Feature>Repository = <Feature>RepositoryImpl(api) - •
Implement ViewModel with lifecycle awareness in
:presentation:kotlinclass <Feature>ViewModel( private val repository: <Feature>Repository, private val savedStateHandle: SavedStateHandle, viewModelScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) ) : ViewModel(viewModelScope), DefaultLifecycleObserver, UiStateHolder<<Feature>UiState, <Feature>UiEvent> { // Use by saved delegate for state persistence // Override onStart() for initialization (NO init block work) } - •
Create Compose UI screens in
:ui-materialand:ui-unstyledwith @Preview annotations - •
Wire dependencies in
:wiringmodule:kotlinval <feature>Module = module { factory { <Feature>ApiService(httpClient = get()) } factory<<Feature>Repository> { <Feature>Repository(get()) } factory<<Feature>ViewModel> { <Feature>ViewModel(get(), get()) } } - •
Register navigation in
:wiring-ui-materialand:wiring-ui-unstyled:kotlinval <feature>NavigationModule = module { scope<MaterialScope> { navigation<<Feature>Route> { route -> <Feature>Screen(viewModel = koinViewModel(), onBack = { navigator.goBack() }) } } } - •
Write tests in
androidUnitTest/:- •Repository tests with success + all error paths
- •ViewModel tests with state transitions using Turbine
- •Property-based tests for mappers (30-40% target)
- •
Validate:
./gradlew :composeApp:assembleDebug test --continue
Workflow 2: Fix Bug in Existing Feature
To diagnose and fix bugs:
- •
Identify the affected layer from error symptoms:
- •UI issues → check
:uimodule screens - •Data/loading issues → check
:presentationViewModel - •Network/parsing errors → check
:datarepository implementation - •DI/runtime errors → check
:wiringmodule configuration
- •UI issues → check
- •
Locate relevant test file in
androidUnitTest/:- •Repositories →
:data/src/androidUnitTest - •ViewModels →
:presentation/src/androidUnitTest - •Mappers →
:data/src/androidUnitTest
- •Repositories →
- •
Reproduce the failing test case
- •
Fix the implementation following canonical patterns:
- •ViewModels: pass
viewModelScopeto constructor, useonStart()notinit - •Repositories: return
Either<RepoError, T>, useEither.catch { }.mapLeft { } - •Impl+Factory: internal implementation class, public factory function
- •ViewModels: pass
- •
Run tests to verify fix:
bash./gradlew :composeApp:assembleDebug test --continue
- •
Add regression test for the bug fix
Workflow 3: Add or Update Tests
To maintain test coverage requirements (100% for mappers, 30-40% property tests):
- •
For mapper tests (REQUIRED - 100% property coverage):
kotlinclass <Feature>MapperSpec : FreeSpec({ "dto to domain preserves all properties" { checkAll(Arb.<Feature>Dto()) { dto -> val domain = dto.toDomain() domain.id shouldBe dto.id // verify all properties } } }) - •
For ViewModel tests (REQUIRED - Turbine for flows):
kotlinclass <Feature>ViewModelSpec : FreeSpec({ lateinit var repository: <Feature>Repository lateinit var testScope: TestScope lateinit var viewModel: <Feature>ViewModel beforeTest { repository = mockk() testScope = TestScope() viewModel = <Feature>ViewModel(repository, testScope) } "state transitions correctly on success" { val data = listOf(<Feature>Data(...)) coEvery { repository.getData() } returns data.right() viewModel.uiState.test { awaitItem() shouldBe <Feature>UiState.Loading viewModel.onStart(owner) testScope.advanceUntilIdle() val content = awaitItem().shouldBeInstanceOf<<Feature>UiState.Content>() content.data shouldHaveSize 1 } } }) - •
For repository tests (REQUIRED - all error paths):
kotlinclass <Feature>RepositorySpec : FreeSpec({ "returns Network error on IOException" { coEvery { api.getData() } throws IOException("network error") repository.getData().fold( ifLeft = { error -> error shouldBeInstanceOf RepoError.Network::class }, ifRight = { fail("Expected left") } ) } }) - •
Run tests:
bash./gradlew :composeApp:assembleDebug test --continue
Critical Guardrails
- •
NEVER do work in ViewModel
initblock → overrideonStart(owner: LifecycleOwner)instead (lifecycle-aware) - •
NEVER store
CoroutineScopeas field → passviewModelScopeto constructor with default value - •
NEVER return nullable or
Resultfrom repositories → returnEither<RepoError, T>withEither.catch { }.mapLeft { } - •
NEVER export
:data,:ui,:wiringto iOS → only:apiand:presentationare exported via:sharedframework - •
NEVER use star imports → always use explicit imports (enforced by .editorconfig)
- •
NEVER skip tests when adding code → every production file requires a test file in
androidUnitTest/ - •
NEVER create empty use cases → call repositories directly from ViewModels when no orchestration needed
- •
NEVER swallow
CancellationException→Either.catchrespects cancellation automatically
Quick Reference
| Command | Purpose | When to Run |
|---|---|---|
./gradlew :composeApp:assembleDebug test --continue | Primary validation (Android build + all tests) | Always, before committing |
./gradlew :composeApp:run | Run desktop app | Local development |
./gradlew dependencyUpdates | Check for dependency updates | Periodically |
./gradlew recordRoborazziDebug | Record screenshot baselines | When adding UI tests |
./gradlew verifyRoborazziDebug | Verify screenshots match baselines | Running UI tests |
./gradlew :features:<feature>:<layer>:testDebugUnitTest | Run specific module tests | Focused testing |
Cross-References
| Document | Purpose | Link |
|---|---|---|
| Architecture + conventions | Master reference for architecture, modules, DI | conventions.md |
| Critical patterns | 6 core patterns (ViewModel, Either, Impl+Factory, Navigation, Testing, Convention Plugins) | critical_patterns_quick_ref.md |
| Testing strategy | Kotest, MockK, Turbine, property tests | testing_strategy.md |
| iOS integration | SwiftUI + KMP ViewModels Direct Integration | ios_integration.md |
| Navigation 3 | Modular navigation architecture | navigation.md |
| Dependency injection | Koin patterns and troubleshooting | dependency_injection.md |
| Product requirements | Feature acceptance criteria | prd.md |
| User flows | User journeys and sequences | user_flow.md |
Reference Implementation: pokemonlist feature demonstrates all patterns:
- •API • Data • Presentation • UI • Wiring