Architecture

SkillDev tools

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Architecture skill

What this skill tells your AI

The instructions your AI receives, as published by devatrii/kmp-starter-template in .agents/skills/kmp-starter/architecture/SKILL.md and read by ahel’s review.

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.
@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

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

@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):
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/componentsCupertinoSection, CupertinoSectionRow, PillActionButton, PillActionsContainer, CoilImage, ScrollableColumn, CupertinoDropdownMenu, FormTextField, LoadingButton, SearchTextField, FiltrationChip, etc.
  • starter/ui/layoutsLoadingLayout, EmptyStateWithAction.
  • starter/ui/utilsDimens (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

Signals

GitHub stars
162
Forks
28
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
kmp-starter-architecture
Source
github.com/devatrii/kmp-starter-template