Generate a production-grade Kotlin Multiplatform app (Compose, Room 3, Navigation 3, Koin) in one dialog
Starting a Kotlin Multiplatform project has a strange cost curve. The first Hello, world is cheap — the IDE's built-in wizard gives you a shared module and platform targets in a minute. The next two weeks are where the real setup lives: choosing an architecture, wiring a KMP-ready database, getting type-safe navigation working, setting up DI without service-locator soup, structuring Gradle so the build doesn't rot, and writing enough tests to trust any of it.
This article walks through one opinionated answer to that setup — the architecture decisions themselves, and why they're baked the way they are. The vehicle is KMP Project Wizard, an open-source IntelliJ IDEA / Android Studio plugin I built that generates the whole thing from File → New Project, but every decision below stands on its own — you can apply them by hand to any KMP codebase.
The reference-app principle: templates that can't go stale by construction
The most important decision isn't in the generated code at all. The templates aren't hand-authored files that drift out of date — they're generated from kmp-ledger, a real, working, MIT-licensed double-entry ledger app that builds, tests, and runs on all three platforms. At plugin build time, concrete names (app.oreshkov.ledger, Posting, Ledger) are replaced with placeholders; at generation time, your names are substituted back in — package, app name, feature name, entity fields, everywhere including directory names.
The consequence: if the reference app compiles and its tests pass, so does your generated project. "Template rot" — the classic starter-kit disease — becomes a build failure in my pipeline instead of a runtime surprise in your project.
Architecture: Clean layers with a feature API/impl split
The generated project is modular from the first commit:
- Domain / Data / UI layers per feature, with the dependency arrow always pointing inward — UI and Data both depend on Domain; Domain depends on nothing.
-
A feature API/impl split: each feature exposes a small
apimodule (public contracts) and keeps itsimplprivate. Other features depend onapionly, so feature boundaries survive growth and builds stay parallelizable. -
Binary-compatibility validation: the Kotlin BCV plugin's
apiDumpsnapshots every module's public ABI, andapiCheckfails the build when a public contract changes accidentally. (A detail I got wrong the first time: ABI dumps are compiler-output snapshots — symbol names, declaration order, and Compose lambda keys derive from your names, so shipping the reference app's dumps with names substituted can never match. The wizard instead runsapiDumpafter your project's first Gradle sync, so the baseline is generated from your code.)
The stack, and why each piece
- Compose Multiplatform for shared UI across Android, iOS, and Desktop — one rendering model, platform entry points kept thin.
- Navigation 3 for type-safe routing: destinations are serializable types, not string routes, so a refactor that breaks navigation breaks the compile.
- Room 3 for persistence — the KMP-ready generation of Room, meaning the same DAO and entity definitions run on all targets, with SQLite under the hood.
- Koin + KSP annotations for DI: annotation-driven definitions with compile-time processing, no reflective magic on the hot path, and no hand-maintained module lists.
-
Coroutines & Flow end to end — repositories expose
Flow, use cases suspend, UI collects with lifecycle awareness.
Gradle that scales: convention plugins + version catalog
All build logic lives in convention plugins (build-logic/), applied by module type — a feature impl module's build file is a few lines naming its type and dependencies. Versions are centralized in gradle/libs.versions.toml. Adding a fifteenth module costs the same as adding the second, and a dependency bump is a one-line change.
Tests as part of the template
Every generated project ships a passing suite: unit tests for domain and data (including an in-memory Room setup) and Compose UI tests. Not because sample tests are valuable in themselves, but because a testing pattern you can copy beats a blank src/test directory every time you add a feature.
What the one dialog actually asks
Package name, app name, one feature name, one entity with a test value, and target platforms (Android / iOS / Desktop). From that it derives every identifier — PascalCase types, camelCase members, snake_case resources — and renders the whole tree with your names, then triggers Gradle sync and generates the ABI baselines.
The plugin's core is free (all three platforms, the full architecture and tests). A paid Pro tier adds two things to the generated project: Claude Code agent config (CLAUDE.md, .claude/ skills) and GitHub Actions CI (.github/) — flat files, no runtime services, and the same content you can inspect in kmp-ledger.
Links
- Plugin: https://plugins.jetbrains.com/plugin/31786-kmp-project-wizard
- Plugin source (MIT): https://github.com/aoreshkov/kmp-wizard
- Reference app the templates come from: https://github.com/aoreshkov/kmp-ledger
If you try it, I'd genuinely value feedback — the issue tracker is open, and the architecture opinions above are exactly the kind of thing worth arguing about.


Top comments (0)