create-kmp-project
From Shubham Singh Bisht's Android helpers — https://shubhamsinghbisht.com/android-roadmap/#helpers
Overview
There is no first-party CLI that scaffolds a KMP project. The new android CLI (android create) ships only an empty-activity Android template. The JetBrains KMP wizard at kmp.jetbrains.com is a Next.js app that posts a client-built spec JSON to /generateKmtProject — not scriptable without running its bundle.
The reliable CLI path is: download a curated official template zip from a Kotlin/* GitHub repo, then rewrite the project name, package, applicationId, namespaces, and iOS bundle id in place.
When to Use
- User asks to create a new KMP / Kotlin Multiplatform project
- User asks to create a Compose Multiplatform (CMP) project, especially with Web/Wasm and/or Desktop (macOS/Linux/Windows) targets
- User asks to start from
KMP-App-Template, kotlin-wasm-compose-template, or any JetBrains wizard template
- User expects
android create / Android CLI to produce a KMP/CMP project
- User wants to try the Amper build flavor of the template
Do not use for: adding KMP/CMP to an existing Android project (different task — that's a migration, not a scaffold).
Templates
| Choice |
Zip URL |
Targets |
Module layout |
| App, Compose UI shared (default KMP) |
https://github.com/Kotlin/KMP-App-Template/archive/refs/heads/main.zip |
Android + iOS |
androidApp + iosApp + shared |
| App, Amper build tool |
https://github.com/Kotlin/KMP-App-Template/archive/refs/heads/amper.zip |
Android + iOS |
Same as above, Amper config — experimental |
| App, native UI per platform |
https://github.com/Kotlin/KMP-App-Template-Native/archive/refs/heads/main.zip |
Android + iOS |
Compose for Android, SwiftUI for iOS, shared business logic |
| KMP library |
https://github.com/Kotlin/multiplatform-library-template/archive/refs/heads/main.zip |
Multi (for publishable libraries) |
Library module only |
| CMP app for Web + Desktop + Android + iOS |
https://github.com/Kotlin/kotlin-wasm-compose-template/archive/refs/heads/main.zip |
Wasm (Web) + Desktop (JVM) + Android + iOS |
Single composeApp module with commonMain / wasmJsMain / desktopMain / androidMain / iosMain |
Inputs to collect
Before scaffolding, ask the user:
- Template choice (one of the templates above)
- Project name (e.g.
KmpDemo) — becomes rootProject.name
- Package id (e.g.
com.example.kmpdemo) — becomes Android namespace, applicationId, iOS BUNDLE_ID, Kotlin source package
- Target directory (default: the current directory; project goes into
<target>/<projectName>)
Procedure (App template, Compose UI shared — main case)
These commands assume macOS (BSD sed). On Linux use sed -i without ''.
WORK=/path/to/parent # where you'll put things
NAME=KmpDemo # project name
PKG=com.example.kmpdemo # package id
SLUG=KMP-App-Template # template slug (matches GitHub repo)
ZIP="https://github.com/Kotlin/$SLUG/archive/refs/heads/main.zip"
# 1. Download + extract
cd "$WORK"
curl -L --fail -o "$SLUG-main.zip" "$ZIP"
unzip -q "$SLUG-main.zip" -d "$WORK"
cp -R "$SLUG-main" "$NAME"
cd "$NAME"
# 2. Drop upstream-only files
rm -rf .github list.json images
# 3. Rewrite package strings across configs
sed -i '' "s/com\\.jetbrains\\.kmpapp/$PKG/g" \
settings.gradle.kts \
androidApp/build.gradle.kts \
shared/build.gradle.kts \
iosApp/Configuration/Config.xcconfig \
iosApp/iosApp.xcodeproj/project.pbxproj
# 4. rootProject.name
sed -i '' "s/\"$SLUG\"/\"$NAME\"/g" settings.gradle.kts
# 5. iOS bundle + display name
# Note: step 3 turns BUNDLE_ID=com.jetbrains.kmpapp.KMP-App-Template into $PKG.KMP-App-Template
# Trim the trailing slug:
sed -i '' "s/BUNDLE_ID=$PKG\\.$SLUG/BUNDLE_ID=$PKG/" iosApp/Configuration/Config.xcconfig
sed -i '' "s/APP_NAME=KMP App/APP_NAME=$NAME/" iosApp/Configuration/Config.xcconfig
# 6. Android app_name
sed -i '' "s/>KMP App</>$NAME</" androidApp/src/main/res/values/strings.xml
# 7. Rewrite Kotlin package declarations + imports
find shared androidApp -name "*.kt" -exec \
sed -i '' "s/com\\.jetbrains\\.kmpapp/$PKG/g" {} +
# 8. Move package directories. Path segments derive from $PKG.
OLD_DIR=com/jetbrains/kmpapp
NEW_DIR=$(echo "$PKG" | tr '.' '/')
for base in shared/src/commonMain/kotlin shared/src/iosMain/kotlin androidApp/src/main/kotlin; do
[ -d "$base/$OLD_DIR" ] || continue
mkdir -p "$base/$(dirname "$NEW_DIR")"
mv "$base/$OLD_DIR" "$base/$NEW_DIR"
# clean up empty parents from old path
rmdir "$base/com/jetbrains" 2>/dev/null
done
Procedure (CMP / Wasm — Web + Desktop + Android + iOS)
Use this for Kotlin/kotlin-wasm-compose-template. It has a single composeApp module with commonMain, wasmJsMain, desktopMain, androidMain, iosMain source sets. Most Kotlin files have no package declaration (default package), which makes the rename smaller.
WORK=/path/to/parent
NAME=CmpDemo
PKG=com.example.cmpdemo
SLUG=kotlin-wasm-compose-template
ZIP="https://github.com/Kotlin/$SLUG/archive/refs/heads/main.zip"
cd "$WORK"
curl -L --fail -o "$SLUG-main.zip" "$ZIP"
unzip -q "$SLUG-main.zip" -d "$WORK"
cp -R "$SLUG-main" "$NAME"
cd "$NAME"
# 1. Drop upstream docs assets
rm -rf screenshots
# 2. Rewrite package strings (template uses org.example.project)
sed -i '' "s/org\\.example\\.project/$PKG/g" \
composeApp/build.gradle.kts \
iosApp/Configuration/Config.xcconfig \
iosApp/iosApp.xcodeproj/project.pbxproj
# 3. rootProject.name (template name is compose-example)
sed -i '' 's/"compose-example"/"'"$NAME"'"/g' settings.gradle.kts
# 4. iOS BUNDLE_ID trim + APP_NAME
# After step 2 the bundle becomes $PKG.composeexample — trim:
sed -i '' "s/BUNDLE_ID=$PKG\\.composeexample/BUNDLE_ID=$PKG/" iosApp/Configuration/Config.xcconfig
sed -i '' "s/APP_NAME=composeexample/APP_NAME=$NAME/" iosApp/Configuration/Config.xcconfig
# 5. Android app_name
find . -name "strings.xml" -exec sed -i '' "s/>composeexample</>$NAME</" {} +
# 6. ONE Kotlin file lives in old package: composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt
# Move it and rewrite its package line. AndroidManifest references .MainActivity relative to
# namespace, so once package = $PKG, the manifest still resolves.
OLD_DIR=composeApp/src/androidMain/kotlin/org/example/project
if [ -d "$OLD_DIR" ]; then
NEW_PATH="composeApp/src/androidMain/kotlin/$(echo "$PKG" | tr '.' '/')"
mkdir -p "$NEW_PATH"
sed -i '' "s/^package org\\.example\\.project\$/package $PKG/" "$OLD_DIR"/*.kt
mv "$OLD_DIR"/*.kt "$NEW_PATH/"
rmdir "$OLD_DIR" composeApp/src/androidMain/kotlin/org/example composeApp/src/androidMain/kotlin/org 2>/dev/null
fi
# 7. Compose Resources generated-class package — derived from rootProject.name (lowercased,
# hyphens → underscores). App.kt imports `compose_example.composeapp.generated.resources.*`,
# which after the rename must become `<rootname-normalized>.composeapp.generated.resources.*`.
# Compute the normalized form and rewrite imports:
RES_OLD=compose_example
RES_NEW=$(echo "$NAME" | tr '[:upper:]-' '[:lower:]_')
find composeApp/src -name "*.kt" -exec \
sed -i '' "s/${RES_OLD}\\.composeapp\\.generated\\.resources/${RES_NEW}.composeapp.generated.resources/g" {} +
Build commands for CMP template
- Web (Wasm) dev server:
./gradlew :composeApp:wasmJsBrowserDevelopmentRun
- Desktop (macOS/JVM):
./gradlew :composeApp:run
- Android:
./gradlew :composeApp:assembleDebug
- iOS: open
iosApp/iosApp.xcodeproj in Xcode
If the user only wants a subset of targets (e.g. Web + Desktop), do not strip the other source sets automatically — that's a significant build.gradle edit. Just tell them which gradle tasks to use; leave the others dormant.
Verification
After every scaffold, run from the new project root:
# KMP-App-Template:
grep -rln "com.jetbrains.kmpapp" . # must return nothing
grep -rln "KMP-App-Template" . # only README.md + MuseumApi.kt URL allowed
grep -E "namespace|applicationId" androidApp/build.gradle.kts shared/build.gradle.kts
head -1 settings.gradle.kts
cat iosApp/Configuration/Config.xcconfig
# kotlin-wasm-compose-template (CMP):
grep -rln "org.example.project\|compose-example\|composeexample" . | grep -v README.md | grep -v LICENSE
# must be empty
grep -E "namespace|applicationId" composeApp/build.gradle.kts
head -1 settings.gradle.kts
cat iosApp/Configuration/Config.xcconfig
find composeApp/src -name "*.kt" -exec grep "^package " {} + # only the new $PKG should appear
Expected:
- Zero hits for the old package strings
- For
KMP-App-Template only: README.md (upstream link) and MuseumApi.kt (sample data URL pointing at upstream's list.json) keep KMP-App-Template — harmless; tell the user.
namespace = "$PKG", applicationId = "$PKG" in the relevant *.gradle.kts. In KMP-App-Template the shared module is $PKG.shared.
rootProject.name = "$NAME"
BUNDLE_ID=$PKG with no trailing slug, APP_NAME=$NAME
Common Mistakes
| Mistake |
Symptom |
Fix |
| Forgot the iOS bundle id post-trim |
Bundle id is com.foo.bar.KMP-App-Template |
Add the targeted s/BUNDLE_ID=$PKG\.$SLUG/BUNDLE_ID=$PKG/ |
Used GNU sed -i syntax on macOS |
sed: 1: "...": invalid command code |
Use sed -i '' (empty backup ext) on BSD/macOS |
Renamed MuseumApp / MainActivity classes |
Template demo no longer compiles |
Don't. They are template content; the user can rename them later if they want. AndroidManifest references them by .MuseumApp / .MainActivity relative to namespace — that still resolves after package change. |
| Forgot the shared module namespace |
Gradle sync fails on shared lib |
shared/build.gradle.kts uses <pkg>.shared, not bare <pkg> |
Skipped project.pbxproj |
Xcode build warns about stale package refs |
Include iosApp/iosApp.xcodeproj/project.pbxproj in the sed pass |
Tried android create --template kmp |
No such template exists |
The Android CLI only ships empty-activity. Do not attempt. |
| Tried to script the JetBrains web wizard via curl |
404 / requires JS-built spec |
Skip the wizard for CLI work; download a template zip instead. |
| CMP template: assumed Kotlin sources have packages |
Files in kotlin-wasm-compose-template are mostly in the default package — no package line, no directory to rename |
The exception is composeApp/src/androidMain/kotlin/org/example/project/MainActivity.kt — that one needs both the package line rewrite and a directory move. Grep for ^package to find any stragglers. |
| CMP template: stripped Android/iOS source sets without editing gradle |
Build fails: dangling dependencies / plugin config |
If the user wants Web + Desktop only, leave the other source sets and just use the Web/Desktop gradle tasks. Stripping requires also editing composeApp/build.gradle.kts. |
| CMP template: forgot to rewrite the Compose Resources generated package |
compileKotlinDesktop fails: Unresolved reference 'compose_example' and Unresolved reference 'Res' in App.kt |
The generated package is <rootProject.name>.composeapp.generated.resources where rootProject.name is lowercased and - is replaced with _. After step 3, also rewrite the import path. Step 7 in the CMP procedure does this. Verify with: grep -rln "compose_example" composeApp/ — must be empty. |
Optional follow-ups
git init && git add -A && git commit -m "init" inside the new project
- For the library template the rename surface is smaller (no
androidApp/, no iosApp/Configuration/Config.xcconfig) — same logic, fewer files
- For the Amper branch, the
.module.yaml files also need the com.jetbrains.kmpapp rewrite — include *.yaml in step 3
- For
KMP-App-Template-Native, additionally update SwiftUI files under iosApp/iosApp/ if they reference the old package