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 |
When Claude uses it
Abschnitt betitelt „When Claude uses it“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
Definition
Abschnitt betitelt „Definition“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.
Step 1: pick the template
Abschnitt betitelt „Step 1: pick the template“| 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.
Step 2: create the repo
Abschnitt betitelt „Step 2: create the repo“-
Clone the template and detach it from its history, because a fork keeps the template’s
CI_PROJECT_PATHrule and commits:Terminal-Fenster 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" -
Push to the new GitLab project. The iOS
deploy-staginganddeploy-productionjobs in.gitlab-ci.ymlare skipped only whenCI_PROJECT_PATHequalsnative-infrastructure/kmp-template, so they become active in the new repo as soon as the CI variables from step 5 exist.
Step 3: rename
Abschnitt betitelt „Step 3: rename“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.
Step 4: bootstrap the toolchain
Abschnitt betitelt „Step 4: bootstrap the toolchain“No GitLab token or private Maven registry is needed since 2026-09-09; com.appswithlove.kmp and com.appswithlove.debug resolve from Maven Central.
brew bundle # xcodes, mise, xcbeautify, aria2 from /Brewfilemise install # tuist 4.181.0, ruby 3.4.2, swiftlint 0.63.2, swiftformat 0.61.0 from /.mise.tomlbundle 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 toolchaincd iosApp && tuist install && tuist generate # SPM deps (Firebase), then the workspace opensXcode 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.
Step 5: secrets and signing
Abschnitt betitelt „Step 5: secrets and signing“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.
Step 6: verify
Abschnitt betitelt „Step 6: verify“./gradlew :shared:testAndroidHostTest :androidApp:testDevDebugUnitTest./gradlew verifyRoborazziDevDebug # goldens live under androidApp/src/test/snapshots; re-record only if HomeContent changed./gradlew koverPrintCoverageCustombundle exec fastlane android build # Prod release AAB, unsigned locallybundle exec fastlane test --env ci # kmp-template only: Tuist generate + iOS unit testsPush 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.
Conventions a new dev needs on day one
Abschnitt betitelt „Conventions a new dev needs on day one“| 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 |
Related skills
Abschnitt betitelt „Related skills“kotlin-expertfor screens, ViewModels and repositories after the setup is done.cicd-pipeline-setupfor the android component’s inputs, Play Store publishing and pipeline tuning.code-coveragefor the Kovercustomvariant and thresholds.debug-sdk-integration,loco-plugin-integration,updraft-plugin-integrationwhen a project needs more than the template’s defaults.

