--- name: kmp-starter-architecture description: Clean Architecture module and package layout for KMP Starter Template features — data/domain/presentation layers, core dependency rule, screen-based package organization, the Screen vs Content composable split, and dialog/bottom-sheet (BaseDialog/BaseBottomSheet) conventions. author: DevAtrii license: MIT --- # Architecture ## Layers Every feature follows Clean Architecture with three layers: ``` features/{FeatureName}/ data/ # repositories, data sources, DI domain/ # Logics (use cases), repositories (interfaces), models presentation/ # UI, ViewModels, State, Actions, Events ``` - `data` → implementations of domain interfaces. - `domain` → the blueprint: Logics, repository interfaces, models. - `presentation` → Compose UI + MVI. ## Module map (from `settings.gradle.kts`) - `composeApp` — shared Compose code, glues all modules. - `androidApp` — Android host app. - `features/{feature}/*` — one module per feature, split into layers. - `features/core/*` — shared foundation (data/domain/presentation). - `features/database`, `navigation`, `resources`, `analytics`, `purchases`, `remote_config`, `notifications`, `locale` — infra features. - `starter/*` — reusable starter modules (core, utils, ui utils, ui components, ui layouts, native bindings). ## Core dependency rule All features may depend on `core`. `core` must **never** depend on a feature. ``` features/* → core (correct) core → features/* (wrong) ``` Put shared code in `features/core/*` only when multiple features need it: shared auth, common DataStores, base repos/Logics, splash, onboarding, shared UI, global helpers. ## Feature structure A placeholder module lives at `features/your-feature/`. Rename it to your feature (e.g. `notes`) and develop there. Refer to `https://starter.atherio.dev/fundamentals/12-writing-your-code/` for the rename steps (`settings.gradle.kts` + project accessors + package rename). ## Package organization — by screens Organize by **screen**, not technical category. Mirror the same package structure across layers: ``` presentation/notes/{list,details,edit}/_components/ _utils/ domain/notes/{list,details,edit}/ data/notes/{list,details,edit}/ ``` - Keep related code close together. - Promote code upward only after multiple consumers exist. - Shared infra inside a feature uses `_` prefix: `_components`, `_utils`, `_navigation`, `_mappers`. ## Screen vs Content composable Split every screen into two parts: 1. **Screen Composable** — the "brain": gets data from the ViewModel, handles navigation callbacks, observes UI events, knows where data comes from (DI / nav params). 2. **Content Composable** — display only: takes `state` and callbacks like `onAction`. No ViewModel or data-source knowledge. ```kotlin @Composable fun HomeScreen( viewModel: HomeViewModel = koinViewModel(), onTaskComplete: () -> Unit, ) { val state by viewModel.state.collectAsState() ObserveAsEvents(flow = viewModel.uiEvents) { event -> when (event) { is HomeEvents.ShowSnackbar -> SnackbarController.sendMessage(event.message) HomeEvents.OnTaskComplete -> onTaskComplete() } } HomeScreenContent(state = state, onAction = viewModel::onAction) } @Composable private fun HomeScreenContent(state: HomeState, onAction: (HomeActions) -> Unit) { Scaffold { /* build UI from state + onAction */ } } ``` This makes screens previewable, testable, and reusable. ## Dialogs & bottom sheets Always build on Starter's popup primitives. Do **not** call raw `Dialog` / `ModalBottomSheet` directly. - `starter/ui/utils/.../ui_utils/popups/dialogs/BaseDialog.kt` - `starter/ui/utils/.../ui_utils/popups/bottom_sheets/BaseBottomSheet.kt` ### BaseDialog ```kotlin BaseDialog(onDismiss = { showDialog = false }) { // content; already wrapped in a themed Surface (90% width, rounded corners) } ``` `BaseDialog` wraps `Dialog` + a `Surface` (`fillMaxWidth(0.9f)`, `RoundedCornerShape(20.dp)`, `surfaceContainerLowest`). Default `DialogProperties` dismiss on back press and outside click. ### BaseBottomSheet ```kotlin @OptIn(ExperimentalMaterial3Api::class) @Composable fun MyBottomSheet( sheetState: SheetState, onResult: (T) -> Unit, onDismiss: () -> Unit, ) { BaseBottomSheet(sheetState = sheetState, onDismiss = onDismiss) { // ColumnScope content } } ``` - Requires a `SheetState` — create with `rememberModalBottomSheetState(skipPartiallyExpanded = true)`. - Dismiss cleanly with the `hideProperly` util (runs `onDismiss` only after the hide animation finishes): ```kotlin val scope = rememberCoroutineScope() // inside a click handler: scope.launch { sheetState.hideProperly { onDismiss() } } ``` - In the screen, drive visibility with a boolean: `var showX by rememberSaveable { mutableStateOf(false) }`; `onDismiss = { showX = false }`. Never store sheet/dialog visibility in `State` (see core-rules). ### Placement convention - One-off dialogs → `.../_components/dialogs/` - One-off bottom sheets → `.../_components/sheets/` - Keep the wrapper (popup shell) separate from its `*Content` composable, mirroring the Screen/Content split. Keep transient popup state (text fields, picker state) inside the popup composable, not the ViewModel. ## Reusable Starter UI Reuse these instead of hand-rolling equivalents (Golden Rule): - `starter/ui/components` — `CupertinoSection`, `CupertinoSectionRow`, `PillActionButton`, `PillActionsContainer`, `CoilImage`, `ScrollableColumn`, `CupertinoDropdownMenu`, `FormTextField`, `LoadingButton`, `SearchTextField`, `FiltrationChip`, etc. - `starter/ui/layouts` — `LoadingLayout`, `EmptyStateWithAction`. - `starter/ui/utils` — `Dimens` (spacing/sizing), `VerticalSpacer`, `toActualString()`. Reference the canonical onboarding slice in `features/core/` for layout and composable structure. ## Reference implementation Mirror the onboarding slice in `features/core/`: - `features/core/domain/.../repositories/OnboardingRepository.kt` — interface - `features/core/data/.../repositories/OnboardingRepositoryImpl.kt` — impl - `features/core/domain/.../logics/` — Logics - `features/core/presentation/.../viewmodels/OnboardingViewModel.kt` — MVI