Skip to content

kmp-project-setup

Type Skill
Plugin awl-android · v0.0.19
Invoke /awl-android:kmp-project-setup
Source plugins/awl-android/skills/kmp-project-setup/SKILL.md

Starts a new Kotlin Multiplatform app from the AWL kmp-template (shared Kotlin logic, native Compose and SwiftUI UIs) or cmp-template (shared Compose Multiplatform UI), then renames package, application ids, Tuist scheme names, fastlane and CI settings, bootstraps the toolchain (mise, Brewfile, 1Password signing) and briefs a new dev on the template conventions. Use for “new kmp project”, “start from the template”, “set up a kotlin multiplatform app”, “rename the template”, “which template”, “compose multiplatform or native”, “kmp template conventions”, “bootstrap the kmp toolchain”. NOT for feature code (use kotlin-expert), pipeline tuning (use cicd-pipeline-setup), coverage (use code-coverage) or the debug SDK (use debug-sdk-integration).

Trigger phrases: new kmp project · start from the template · set up a kotlin multiplatform app · rename the template · which template · compose multiplatform or native · kmp template conventions · bootstrap the kmp toolchain

Two starters live under native-infrastructure on gitlab.appswithlove.net:

Template Repo UI Snapshot this skill was written against
kmp-template native-infrastructure/kmp-template Jetpack Compose on Android, SwiftUI on iOS, shared Kotlin module for logic 5387943 (2026-09-10)
cmp-template native-infrastructure/cmp-template One Compose Multiplatform UI in shared/commonMain for both platforms 094e36d (2026-09-09)

The old android-infra/template has been dead since 2023. Do not start a project from it, because its Gradle, AGP and Kotlin setup no longer builds with the AWL libraries.

Choose When
kmp-template The iOS app needs native SwiftUI feel, platform APIs or an existing Swift team. Swift consumes shared types through SKIE.
cmp-template One UI team ships both platforms, UI parity matters more than platform feel, and nobody writes Swift beyond the two-file host.

Read references/cmp-vs-kmp.md when the choice is unclear, when someone asks “what is different” or when a project wants to move from one to the other.

  1. Clone the template and detach it from its history, because a fork keeps the template’s CI_PROJECT_PATH rule and commits:

    Terminal window
    git clone ssh://git@gitlab.appswithlove.net:22222/native-infrastructure/kmp-template.git <project>
    cd <project> && rm -rf .git && git init && git add -A && git commit -m "chore: bootstrap from kmp-template 5387943"
  2. Push to the new GitLab project. The iOS deploy-staging and deploy-production jobs in .gitlab-ci.yml are skipped only when CI_PROJECT_PATH equals native-infrastructure/kmp-template, so they become active in the new repo as soon as the CI variables from step 5 exist.

Everything in the template is named com.appswithlove.template, KmpTemplate (Tuist target), KMP Template (display name) and kmp-template (Gradle root). Rename them together, because Koin, BuildKonfig and the Swift import Shared all resolve by package or target name.

What Where Value in template
Kotlin package + directories shared/src/*/kotlin/com/appswithlove/template, androidApp/src/*/kotlin/com/appswithlove/template com.appswithlove.template
Android namespace, applicationId androidApp/build.gradle.kts, shared/build.gradle.kts com.appswithlove.template, com.appswithlove.template.shared
BuildKonfig packageName shared/build.gradle.kts com.appswithlove.template
App display names per flavor androidApp/build.gradle.kts resValue("string", "app_name", …) KMP Template Dev, KMP Template
Gradle root name settings.gradle.kts kmp-template
Tuist app name and folders iosApp/Project.swift appName, iosApp/KmpTemplate/, iosApp/Tuist/Package.swift KmpTemplate
Bundle ids, display names iosApp/Configurations/*.xcconfig com.appswithlove.template[.dev]
fastlane fastlane/Appfile, Matchfile, Gymfile, Fastfile (IOS_WORKSPACE), .env.* bundle ids, KmpTemplate.xcworkspace
Lint paths .swiftlint.yml included: iosApp/KmpTemplate/Sources, Tests

Read references/rename-checklist.md for the full file-by-file list with the exact strings to replace, including the CMP variant and the verification commands. Do the rename with the IDE refactoring for the Kotlin package and sed for the rest, then grep for the old strings before the first commit.

No GitLab token or private Maven registry is needed since 2026-09-09; com.appswithlove.kmp and com.appswithlove.debug resolve from Maven Central.

Terminal window
brew bundle # xcodes, mise, xcbeautify, aria2 from /Brewfile
mise install # tuist 4.181.0, ruby 3.4.2, swiftlint 0.63.2, swiftformat 0.61.0 from /.mise.toml
bundle install # fastlane ~> 2.231 + xcov + increment_version_code plugin
./gradlew :shared:testAndroidHostTest # first Gradle sync; the wrapper downloads Gradle 9.7.0 and a JDK 21 daemon toolchain
cd iosApp && tuist install && tuist generate # SPM deps (Firebase), then the workspace opens

Xcode is pinned in .xcode-version (26.4). Build the iOS app against a concrete Apple Silicon simulator, because the shared framework only targets iosSimulatorArm64. cmp-template ships a checked-in iosApp/iosApp.xcodeproj instead and has no mise, Brewfile or Swift lint config.

Debug builds sign with the committed androidApp/debug-keystore.jks (password android, intentionally public), and release builds stay unsigned locally, so a developer needs nothing from step 5 to run the app.

Secret Where it goes Who sets it
RELEASE_KEYSTORE (file, base64 jks), RELEASE_KEY_ALIAS, RELEASE_STORE_PASSWORD, RELEASE_KEY_PASSWORD GitLab CI variables, masked and protected Project lead after make-android-keystore <project> --import … --account appswithlove.1password.com
LOCO_API_KEY, UPDRAFT_URL_DEV, UPDRAFT_URL_PROD GitLab CI variables Project lead
VAULT/ITEM placeholders keystore.properties.tpl Whoever created the 1Password item
MATCH_GIT_URL, MATCH_PASSWORD, APP_STORE_CONNECT_* GitLab CI variables (iOS, kmp-template only) iOS lead
GoogleService-Info-{dev,prod}.plist iosApp/Configurations/Firebase/ iOS lead, from the Firebase console

Optional local setup: ./scripts/setup-signing.sh injects release signing from 1Password, LOCO_REF='op://…' ./scripts/setup-loco.sh writes locoApiKey to local.properties, and bundle exec fastlane certificates syncs iOS profiles. Read references/ci-and-signing.md for the CI components, the job map and how androidApp/build.gradle.kts resolves each secret.

Terminal window
./gradlew :shared:testAndroidHostTest :androidApp:testDevDebugUnitTest
./gradlew verifyRoborazziDevDebug # goldens live under androidApp/src/test/snapshots; re-record only if HomeContent changed
./gradlew koverPrintCoverageCustom
bundle exec fastlane android build # Prod release AAB, unsigned locally
bundle exec fastlane test --env ci # kmp-template only: Tuist generate + iOS unit tests

Push a branch and open a merge request into main, because the android component runs test, build and SBOM on merge requests and the iOS lint and test jobs run there too.

Topic Convention Where
Modules shared (logic, DI, error model, commonMain + iosMain), androidApp (Compose UI, ViewModels, Nav3), iosApp (SwiftUI shell). cmp-template moves UI into shared/commonMain and adds androidMain. README.md
Versions Single catalog, no versions elsewhere. gradle/libs.versions.toml
Errors Shared APIs return AppResult<T> (Success, Failure(AppError)) from com.appswithlove.kmp:core, never throw across the iOS bridge. Wrap with appResultOf { }; AppError is Network, Http(code), Unknown(cause) and grows only when the UI branches on a new case. shared/…/greeting/GreetingRepository.kt
Interfaces …Providing for queries and flows, …Handling for commands; one repository may implement both and Koin binds it to each. shared/…/di/SharedModule.kt
Dispatchers Inject AppDispatchers from com.appswithlove.kmp.coroutines; tests pass AppDispatchers(dispatcher, dispatcher) with UnconfinedTestDispatcher. BaseViewModel.kt, HomeViewModelTest.kt
DI Koin. sharedModule in shared, appModule with viewModel { } in androidApp. Android calls initKoin { androidContext(…); modules(appModule) }, iOS calls Koin_iosKt.doInitKoin() and resolves through KoinHelper. di/
ViewModels Extend BaseViewModel(dispatchers), launch in catchingCoroutineScope, funnel failures through submitError, expose one StateFlow via combine(…).stateIn(…). Snackbars go through SnackbarStateHolder and SnackbarLauncher from com.appswithlove.kmp:compose. androidApp/…/ui/common/BaseViewModel.kt
Navigation Navigation 3 with @Serializable sealed interface Screen : NavKey. androidApp/…/ui/nav/
Flavors dev (default, .dev suffix, BASE_URL=https://dev.example.com/) and prod. BuildKonfig picks the flavor from the Gradle task name, then local.properties buildkonfig.flavor, then gradle.properties. iOS derives it from the Xcode configuration name in Scripts/embed_kotlin_framework.sh. shared/build.gradle.kts
Logging Kermit Logger.d { }. CLAUDE.md
iOS bridge shared/build.gradle.kts exports awl-kmp-core through the Shared framework and applies SKIE, so switch onEnum(of: result) works in Swift. GreetingViewModel.swift
Debug tooling debugImplementation(libs.awl.debug), releaseImplementation(libs.awl.debug.no.op); DevWrapper wraps App() with the DevSheet. androidApp/…/dev/DevWrapper.kt
Loco, Updraft com.appswithlove.loco (en, de, defLang = "en") and com.appswithlove.updraft (URLs per DevDebug and ProdRelease) are applied on androidApp. androidApp/build.gradle.kts
Tests Fakes over mocks, Turbine, Kotest assertions, Robolectric plus Roborazzi screenshots that only run when a *Roborazzi* task is invoked. CLAUDE.md, HomeScreenshotTest.kt
Style ktlint overrides in .editorconfig (200 columns); SwiftFormat and SwiftLint in the repo root. .editorconfig, .swiftformat
  • kotlin-expert for screens, ViewModels and repositories after the setup is done.
  • cicd-pipeline-setup for the android component’s inputs, Play Store publishing and pipeline tuning.
  • code-coverage for the Kover custom variant and thresholds.
  • debug-sdk-integration, loco-plugin-integration, updraft-plugin-integration when a project needs more than the template’s defaults.