<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pavel Pristalov</title>
    <description>The latest articles on DEV Community by Pavel Pristalov (@pavel_pristalov).</description>
    <link>https://dev.to/pavel_pristalov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4030184%2F1f4ee3ff-6ce3-4f63-93f1-168905ae1f53.jpg</url>
      <title>DEV Community: Pavel Pristalov</title>
      <link>https://dev.to/pavel_pristalov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pavel_pristalov"/>
    <language>en</language>
    <item>
      <title>The Best Architecture for KMP Apps, and When Not to Use It (Part 1)</title>
      <dc:creator>Pavel Pristalov</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:51:08 +0000</pubDate>
      <link>https://dev.to/pavel_pristalov/the-best-architecture-for-kmp-apps-and-when-not-to-use-it-part-1-3l6f</link>
      <guid>https://dev.to/pavel_pristalov/the-best-architecture-for-kmp-apps-and-when-not-to-use-it-part-1-3l6f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faqxy8lueprq50bhizrdt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faqxy8lueprq50bhizrdt.png" alt="KMP: where to draw the boundary. Choosing a stack and architecture for a product that will grow across Android, iOS, desktop, web, and backend" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In KMP and CMP app architecture, the main question is not how much code you can share. It is where you should stop.&lt;/p&gt;

&lt;p&gt;I will show two approaches that work. The first is an architecture for a large mobile product: a shared Kotlin layer for business logic and a native UI on every platform. The second is the startup path, where Compose Multiplatform helps you quickly launch mobile products, web and desktop applications, and even a backend built on shared data models.&lt;/p&gt;

&lt;p&gt;Let us discuss when you should and should not use the "best" architecture.&lt;/p&gt;




&lt;p&gt;This is the first, theoretical article in a two-part series. In the second part, we will move on to practice, examine real implementation problems, and look at the additional benefits this architecture gives us.&lt;/p&gt;

&lt;p&gt;I am Pavel Pristalov, a Staff Engineer with 20 years of experience in IT. I have designed and developed mobile services for small startups and multinational companies.&lt;/p&gt;

&lt;p&gt;TL;DR: the template repository for the architecture described here is &lt;a href="https://github.com/pristalovpavel/kmp-clean-template" rel="noopener noreferrer"&gt;github.com/pristalovpavel/kmp-clean-template&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I agree that the title is somewhat clickbaity. But the architecture I am going to describe really is the best one I have encountered in 15 years of mobile development. It gives you the skeleton of a large application, one to which you can add feature after feature simply and predictably.&lt;/p&gt;

&lt;p&gt;Its main advantages are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple.&lt;/strong&gt; It is based on KISS and YAGNI. This reduces onboarding time, code review time, and the number of possible mistakes and misunderstandings within the team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Powerful.&lt;/strong&gt; It allows you to implement every native capability of each platform. No bridges, reflection, questionable workarounds, or performance compromises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost-effective.&lt;/strong&gt; It reuses as much code as possible while compiling it with each platform's own toolchain: JVM bytecode for Android, a native binary for iOS, and JS or Wasm for the web. This reduces development and testing time several times over and makes problems easier to locate and fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensible.&lt;/strong&gt; We can add support for an entirely new platform fairly easily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practical.&lt;/strong&gt; It solves the basic application problems we face every day: unified error handling, pagination, offline mode and cache updates, dependency injection, authentication and token refresh, shared analytics logic, background synchronization, network retries, logging, and remote feature management.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The proposed architecture: overview and principles
&lt;/h2&gt;

&lt;p&gt;What should we use as the foundation of our architecture? Clean Architecture, of course. It is a relatively simple, understandable, and well-proven approach to building even complex applications.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5cw3n933kryycph1ym3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5cw3n933kryycph1ym3.png" alt="Clean Architecture in KMP: Data and Domain live in shared, while Presentation remains native" width="800" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will not explain Clean Architecture itself, its principles, or its advantages. They are covered in detail in the classic articles by &lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;Robert C. Martin&lt;/a&gt; and &lt;a href="https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/" rel="noopener noreferrer"&gt;Fernando Cejas&lt;/a&gt;. Google's official &lt;a href="https://developer.android.com/topic/architecture" rel="noopener noreferrer"&gt;Guide to app architecture&lt;/a&gt; recommends the same broad layers: data, domain, and UI. Here, we will focus on the practical details of applying this architecture to KMP projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shared layer and ViewModel
&lt;/h2&gt;

&lt;p&gt;Let us start with the most important rule: limit the KMP shared module to the data and domain layers. Do not move ViewModels into it, let alone the UI.&lt;/p&gt;

&lt;p&gt;Yes, I advise against using CMP as the foundation of cross-platform applications that you expect to develop and maintain for years. CMP is an excellent solution for Android. It can and should be used for web and desktop when you want to reuse an existing UI there. But iOS differs significantly, and in a large project an attempt to share ViewModels and UI will almost certainly create a pile of problems.&lt;/p&gt;

&lt;p&gt;Here are some of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jetpack ViewModel was designed around the Android lifecycle. On iOS, you have to &lt;a href="https://developer.android.com/kotlin/multiplatform/viewmodel" rel="noopener noreferrer"&gt;provide a &lt;code&gt;ViewModelStoreOwner&lt;/code&gt; manually&lt;/a&gt;, which is foreign to an iOS developer. In the iOS world, a ViewModel is a snapshot of state for a view, not a lifecycle-management mechanism.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;StateFlow&lt;/code&gt; crosses the Objective-C interop boundary, its generic type is lost, and SwiftUI cannot observe it directly. You need wrappers such as &lt;code&gt;CommonStateFlow&lt;/code&gt;, an adapter such as &lt;code&gt;KotlinViewAdapter&lt;/code&gt;, and a manual binding between a &lt;code&gt;CoroutineScope&lt;/code&gt; and the view lifecycle. In practice, "sharing a ViewModel" means writing a glue layer that does not exist in a native approach.&lt;/li&gt;
&lt;li&gt;Nested ViewModels are not supported by the Jetpack library.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the minimum glue required to share a ViewModel with native SwiftUI. The code is based on &lt;a href="https://medium.com/@tristanferre/using-a-kotlin-view-model-in-an-ios-view-643f60497f42" rel="noopener noreferrer"&gt;Tristan Ferre's article&lt;/a&gt;. First, a wrapper in shared code, because Objective-C exposes &lt;code&gt;StateFlow&lt;/code&gt; to Swift without its generic type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommonStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;StateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;flow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;startCollect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CoroutineScope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;onEach&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;CommonCancellable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;job&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;onEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CommonCancellable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a generic adapter on the Swift side. It follows the view lifecycle and exposes the state through &lt;code&gt;@Published&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;KotlinViewAdapter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;State&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AnyObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;VM&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;ObservableObject&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@Published&lt;/span&gt; &lt;span class="kd"&gt;private(set)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;State&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;VM&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CommonCancellable&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CoroutineScopeFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;viewModel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;VM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stateFlow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CommonStateFlow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;State&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;viewModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as!&lt;/span&gt; &lt;span class="kt"&gt;State&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stateFlow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startCollect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;coroutineScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;newState&lt;/span&gt; &lt;span class="k"&gt;as!&lt;/span&gt; &lt;span class="kt"&gt;State&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;deinit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;subscription&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// Manually cancel both the subscription&lt;/span&gt;
        &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;           &lt;span class="c1"&gt;// and the Kotlin scope, or they will leak.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Only after all that can SwiftUI use it:&lt;/span&gt;
&lt;span class="c1"&gt;// @StateObject var adapter = KotlinViewAdapter(viewModel: ..., stateFlow: ...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is only the skeleton. Real glue code grows factories for scopes, reverse event propagation, and one-off side-effect handling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0hakitk4nnxjrl27qqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff0hakitk4nnxjrl27qqp.png" alt="Sharing the ViewModel splits Presentation in two; exposing a UseCase keeps it native" width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://touchlab.co/kmp-view-models" rel="noopener noreferrer"&gt;this article&lt;/a&gt;, Touchlab gives what I consider the right recommendation: share ViewModels when using Compose Multiplatform; with a native iOS UI or a team with a strong iOS culture, keep the Presentation layer platform-specific.&lt;/p&gt;

&lt;p&gt;There are, of course, exceptions. Backend-driven UI is a natural fit for CMP, as is any sufficiently isolated feature that does not require completely native UI and UX.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why else do we not share ViewModels?
&lt;/h2&gt;

&lt;p&gt;I waited a long time for the ability to reuse a ViewModel across platforms. The idea appears perfectly logical: a ViewModel contains the business logic of the Presentation layer. If it is business logic, then by our own criteria for good architecture, it should be written once for every platform. But the devil is in the details.&lt;/p&gt;

&lt;p&gt;Sharing a ViewModel artificially splits the Presentation layer in two simply because doing so appears convenient. It also forces us to unify the architecture at the topmost layer, which is not always desirable.&lt;/p&gt;

&lt;p&gt;Android and iOS differ in both capabilities and UX. To build a genuinely complex application that uses the full range of native platform features, you would constantly have to work around these differences: empty &lt;code&gt;actual&lt;/code&gt; declarations, interface implementations, and platform-specific code pulled into common code. Just keep the entire Presentation layer native.&lt;/p&gt;

&lt;p&gt;Navigation remains native as well: &lt;code&gt;NavigationStack&lt;/code&gt; on iOS and Navigation Compose on Android. Consistency between platforms is provided not by a shared navigator but by shared data and domain layers. Screens call the same UseCases and receive the same results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about iOS?
&lt;/h2&gt;

&lt;p&gt;This approach quietly removes the main obstacle to adopting KMP code: resistance from iOS developers. They can build the UI layer however they want, without being tied to an Android ViewModel architecture. They simply retrieve data from a UseCase in the shared layer instead of Alamofire, while getting all the data-handling capabilities of that layer. In my experience, this significantly reduces both development time and the time needed to adopt KMP.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffldxhg9u9ipgkq2u6tht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffldxhg9u9ipgkq2u6tht.png" alt="On iOS, data comes from a shared UseCase while SwiftUI and its ViewModel remain native" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Moreover, because the shared layer contains nothing from the Android framework, iOS developers can write its code themselves. Let us see how difficult that code really is. It contains data models, API interfaces, a SQL database, repositories, repository implementations, and UseCases. In simple cases, these are straightforward entities that are easy to write or generate from an existing example. More difficult cases exist, but even then an iOS developer can understand and maintain the code because it is based on language-independent principles rather than Android-specific concepts.&lt;/p&gt;

&lt;p&gt;That is why a project may need only three technical onboarding documents: an architecture description, a description of the file structure, and a document titled "How to create a new feature or screen." Simpler is better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testability
&lt;/h2&gt;

&lt;p&gt;There is another consequence of keeping only data and domain in shared: this code is easy to test. It contains no Android framework, UIKit, or lifecycle coupling, only pure Kotlin. The application's business logic can therefore be covered by regular unit tests that run on the JVM in seconds, without an emulator, simulator, or instrumented tests.&lt;/p&gt;

&lt;p&gt;In fully native implementations, the most valuable logic, including data access, error handling, caching, and authentication, is tested separately on each platform. Here, it lives in one place and is covered once for all of them. Write a test for a UseCase, and you exercise the logic that will run on Android, iOS, desktop, and the web.&lt;/p&gt;

&lt;p&gt;The UI-specific behavior is tested with the platform's native tools, such as XCTest and Compose UI tests, just as it would be in any native application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrvwf6ibm4wtolypwgan.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrvwf6ibm4wtolypwgan.png" alt="The shared business logic is tested once; each platform tests only its own UI" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will show what this looks like in code in the second, practical part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bonuses
&lt;/h2&gt;

&lt;p&gt;When data and business logic live in a multiplatform shared module from day one, adding another target is relatively inexpensive. We get three useful bonuses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgvf44frid6idu2kxl8tp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgvf44frid6idu2kxl8tp.png" alt="A single shared core can support Android, iOS, desktop, backend, and web targets" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Desktop.&lt;/strong&gt; An Android application built with Compose gives us a desktop version almost for free. Some work remains: selecting the right libraries, packaging the application, and adapting layouts for larger screens. Modern Android applications should already avoid assumptions about orientation and screen size because designers and developers must account for tablets and foldable devices.&lt;/p&gt;

&lt;p&gt;Creating installers for macOS, Windows, and Linux takes some additional time. But this is still far cheaper than building a separate desktop application while making the product available on all three platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A backend built on shared models.&lt;/strong&gt; The shared module already contains DTOs annotated with &lt;code&gt;@Serializable&lt;/code&gt;. Nothing prevents us from adding them as a regular dependency to a Ktor server, which is just another JVM target. That is exactly what the template repository does: it contains a mock or proxy backend that responds with the same types the application parses. The API contract is shared and cannot silently diverge. Change a model, and the client and server are rebuilt together, so a mismatch is found during compilation instead of in production.&lt;/p&gt;

&lt;p&gt;More importantly, such a backend unblocks mobile development and testing while the real backend is still under construction. The teams need only agree on the API contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web.&lt;/strong&gt; There are &lt;a href="https://kotlinlang.org/docs/web-overview.html" rel="noopener noreferrer"&gt;two paths&lt;/a&gt;, and their maturity differs significantly.&lt;/p&gt;

&lt;p&gt;The first is Kotlin/JS: shared logic as a library for a web team. The same module compiles into a JavaScript library with TypeScript declarations. We mark the public API with &lt;code&gt;@JsExport&lt;/code&gt;, and the web team connects our UseCases to React, Vue, or Angular as an ordinary npm package. It is exactly the same pattern as on iOS: a platform-owned UI and shared business logic.&lt;/p&gt;

&lt;p&gt;JetBrains presents this as the primary Kotlin/JS scenario and maintains an official example, &lt;a href="https://github.com/Kotlin/kmp-logic-sharing-simple-example" rel="noopener noreferrer"&gt;kmp-logic-sharing-simple-example&lt;/a&gt;, with Android, iOS, and React clients. This pattern also works in production: since 2019, Quizlet has used a shared answer-checking engine across web and mobile applications with a combined install count in the hundreds of millions (&lt;a href="https://quizlet.com/blog/shared-code-kotlin-multiplatform" rel="noopener noreferrer"&gt;case study&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;To be fair, the developer experience is still worse here than at the iOS boundary. Parts of the export functionality remain experimental, and some Kotlin idioms are lost: &lt;code&gt;List&lt;/code&gt; and &lt;code&gt;Flow&lt;/code&gt; require explicit facades, while exporting suspend functions and &lt;code&gt;Long&lt;/code&gt; has only recently appeared behind experimental flags. The resulting library can also be several times larger than handwritten JavaScript. &lt;a href="https://www.doist.dev/filterist-kotlin-multiplatform-javascript-exploration/" rel="noopener noreferrer"&gt;Doist reported&lt;/a&gt; approximately 41 KB gzipped versus 10 KB for its JavaScript implementation.&lt;/p&gt;

&lt;p&gt;The second path is Kotlin/Wasm with Compose Multiplatform: the entire shared application runs in the browser, with Compose rendering to a canvas through WebAssembly. This is already practical for admin panels and internal tools. For public-facing products, canvas rendering still creates issues with SEO and accessibility, and the initial bundle remains heavy.&lt;/p&gt;

&lt;p&gt;Despite the progress in Kotlin for Web, there is no free lunch and no "SKIE for Web" that hides all of these differences. A small web team can consume shared logic through Kotlin/JS. A quick internal tool can be built with CMP and Wasm while reusing Ktor, SQLDelight, Koin, and other multiplatform libraries.&lt;/p&gt;

&lt;p&gt;The template repository implements the first path: a Kotlin/JS web application on top of the shared UseCases, plus an &lt;code&gt;@JsExport&lt;/code&gt; facade with TypeScript declarations for an external web team.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not Flutter or React Native?
&lt;/h2&gt;

&lt;p&gt;When you expect to actively develop and maintain a complex product for years, do not use PWA, Flutter, or React Native. Use KMP and CMP. If the project grows and needs a more rigorous architecture, KMP gives you a gradual migration path.&lt;/p&gt;

&lt;p&gt;Adding a native feature to a KMP application is almost painless: create a SwiftUI screen and add the corresponding shared logic. There are no platform channels. Adding a native feature to a Flutter application means platform channels such as MethodChannel or Pigeon, separate Android and iOS implementations, and embedding native views through PlatformView.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihskerf58l2c2putk6rk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihskerf58l2c2putk6rk.png" alt="Flutter and React Native tend toward a rewrite, while KMP can evolve toward native UI incrementally" width="799" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my experience, after a year and a half or two years, many actively developed Flutter and React Native projects arrive with the request, "Rewrite it natively." Most of the application then has to be rewritten from scratch, which is expensive and painful. KMP grows through evolution and gradual legacy refactoring. There is no stack replacement and no need to maintain two unrelated stacks, only controlled and predictable refactoring.&lt;/p&gt;

&lt;p&gt;The opposite situation is also common: we already have two native applications and want to share their logic. The same approach works from that direction. Do not rewrite anything. Choose one new feature, move its data and domain code into a shared module, and connect it to both applications. Measure development speed and the number of defects. If the results are good, move features into shared one by one and refactor legacy code only when necessary. If the experiment fails, you can stop at any time.&lt;/p&gt;

&lt;h2&gt;
  
  
  When not to use this architecture
&lt;/h2&gt;

&lt;p&gt;So we have a great architecture. When should we not use it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 1: a startup without clear requirements
&lt;/h3&gt;

&lt;p&gt;If you do not have detailed implementation requirements, the team is very small, and the budget is extremely limited, do not spend resources on layers, extensive documentation, and boilerplate. Write the code quickly, for every platform at once, with KMP and CMP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 2: there is nothing meaningful to share
&lt;/h3&gt;

&lt;p&gt;The second case is an application with almost nothing to move into the shared layer: Android and iOS widgets with little network traffic, for example, or small utilities. Online-only applications without a cache can also fall into this category. In these cases, entire data and domain layers may become unnecessary boilerplate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 3: the application will barely evolve
&lt;/h3&gt;

&lt;p&gt;The third case is an application that will require very little future development and maintenance. Consider a mobile application for a small restaurant chain with a fixed feature set: book a table, view the menu, create a loyalty card, and display it when paying. Such an application may have an offline mode and nontrivial data processing, but if no new features are planned, an elaborate architecture may not be worth the cost.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;Share logic?&lt;/th&gt;
&lt;th&gt;Share UI?&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Large product with long-term maintenance&lt;/td&gt;
&lt;td&gt;Yes: data + domain&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Clean Architecture in KMP: shared core + native UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Startup hypothesis, small team&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, entirely&lt;/td&gt;
&lt;td&gt;KMP + CMP without elaborate layers: speed matters more than structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolated feature or BDUI inside a large product&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, selectively&lt;/td&gt;
&lt;td&gt;CMP screens embedded in a native application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widgets, small utilities, online-only apps without a cache&lt;/td&gt;
&lt;td&gt;Almost nothing to share&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Pure native development in the platform language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A mostly finished application with a fixed scope&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Depends on the team&lt;/td&gt;
&lt;td&gt;KMP + CMP: inexpensive now, with a growth path if the plans change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If I had to compress the entire article into a single thought, it would be this: KMP is not "write once, run everywhere." It is about sharing exactly what makes sense to share.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a large product, use Clean Architecture in KMP: shared data and domain, native UI.&lt;/li&gt;
&lt;li&gt;In a small project or startup experiment, use CMP throughout to launch quickly on every platform.&lt;/li&gt;
&lt;li&gt;If the application is mostly UI, without complex data, offline behavior, or networking, use the platform's native stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no chasm between these approaches. There is a staircase. As the project grows, add the missing layers and gradually remove CMP from iOS without rewriting everything from scratch. The question is not whether to use KMP. It is where your project is on that staircase today. Look at your product and either start moving or deliberately stay where you are. Both choices are valid when they are made honestly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsri4qdknwy7ss7zp992v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsri4qdknwy7ss7zp992v.png" alt="KMP and CMP can be introduced at any stage of a mobile product's evolution" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Part 2, we will examine the architecture's core in code: UseCase contracts, SKIE, error handling, dependency injection, modularity, and testing.&lt;/p&gt;

&lt;p&gt;Template repository: &lt;a href="https://github.com/pristalovpavel/kmp-clean-template" rel="noopener noreferrer"&gt;github.com/pristalovpavel/kmp-clean-template&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;Architecture and approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;The Clean Architecture, Robert C. Martin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/" rel="noopener noreferrer"&gt;Architecting Android... The clean way?, Fernando Cejas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/topic/architecture" rel="noopener noreferrer"&gt;Google: Guide to app architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why the Presentation layer stays native:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://touchlab.co/kmp-view-models" rel="noopener noreferrer"&gt;Touchlab: Is AndroidX ViewModel the best choice for KMP projects?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@tristanferre/using-a-kotlin-view-model-in-an-ios-view-643f60497f42" rel="noopener noreferrer"&gt;Tristan Ferre: Using a Kotlin ViewModel in an iOS view&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/touchlab/KaMPKit/discussions/170" rel="noopener noreferrer"&gt;KaMPKit discussion: Why is the ViewModel shared code here?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.android.com/kotlin/multiplatform/viewmodel" rel="noopener noreferrer"&gt;Google: Set up ViewModel for KMP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Web targets and sharing logic with the browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://kotlinlang.org/docs/web-overview.html" rel="noopener noreferrer"&gt;Kotlin for web: Kotlin/JS and Kotlin/Wasm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.jetbrains.com/kotlin/2025/05/present-and-future-kotlin-for-web/" rel="noopener noreferrer"&gt;JetBrains: Present and Future of Kotlin for Web&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Kotlin/kmp-logic-sharing-simple-example" rel="noopener noreferrer"&gt;Official Android + iOS + React logic-sharing sample&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://quizlet.com/blog/shared-code-kotlin-multiplatform" rel="noopener noreferrer"&gt;Quizlet case study&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.doist.dev/filterist-kotlin-multiplatform-javascript-exploration/" rel="noopener noreferrer"&gt;Doist's Kotlin/JS experiment&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kotlin</category>
      <category>multiplatform</category>
      <category>architecture</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
