<?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: wang jack</title>
    <description>The latest articles on DEV Community by wang jack (@wang_jack_75a91dea08d2995).</description>
    <link>https://dev.to/wang_jack_75a91dea08d2995</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%2F3990596%2Fd2b3a6be-fca4-4483-b095-f7c2ed746c27.jpg</url>
      <title>DEV Community: wang jack</title>
      <link>https://dev.to/wang_jack_75a91dea08d2995</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wang_jack_75a91dea08d2995"/>
    <language>en</language>
    <item>
      <title>97% of My App's Code Is in commonMain — A Field Report on Shipping 100% Compose Multiplatform</title>
      <dc:creator>wang jack</dc:creator>
      <pubDate>Thu, 18 Jun 2026 09:42:39 +0000</pubDate>
      <link>https://dev.to/wang_jack_75a91dea08d2995/97-of-my-apps-code-is-in-commonmain-a-field-report-on-shipping-100-compose-multiplatform-j8h</link>
      <guid>https://dev.to/wang_jack_75a91dea08d2995/97-of-my-apps-code-is-in-commonmain-a-field-report-on-shipping-100-compose-multiplatform-j8h</guid>
      <description>&lt;p&gt;I shipped a small dev-news reader to Google Play with the &lt;strong&gt;entire client written&lt;br&gt;
in one Compose Multiplatform codebase&lt;/strong&gt; — every screen in &lt;code&gt;commonMain&lt;/code&gt;, no&lt;br&gt;
per-platform UI. This is an honest field report on what that actually costs in&lt;br&gt;
production: the numbers, the native seams, and the parts that still hurt (hi,&lt;br&gt;
iOS). The repo is open source (MIT), so everything here is checkable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — For a content/list/detail app, CMP is comfortably production-ready&lt;br&gt;
on Android. 96.9% of the shared module is &lt;code&gt;commonMain&lt;/code&gt;; the native cost is&lt;br&gt;
concentrated in ~10 &lt;code&gt;expect/actual&lt;/code&gt; seams. iOS compiles and renders, but isn't&lt;br&gt;
polished yet.&lt;/p&gt;
&lt;/blockquote&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%2Fw9g7xhgj2l4lpkqfeqlp.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%2Fw9g7xhgj2l4lpkqfeqlp.png" alt=" " width="800" height="1690"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source set&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;commonMain&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;~7,700&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.9%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;androidMain&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;1.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iosMain&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;127&lt;/td&gt;
&lt;td&gt;1.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All &lt;strong&gt;17 screens&lt;/strong&gt; live in &lt;code&gt;commonMain&lt;/code&gt; — trending list, aggregated feed, README&lt;br&gt;
detail, profile with paging, settings, favorites — and there isn't a single&lt;br&gt;
&lt;code&gt;if (isAndroid)&lt;/code&gt; branch in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3% that isn't shared
&lt;/h2&gt;

&lt;p&gt;The native cost isn't spread thinly across the codebase. It's concentrated in&lt;br&gt;
~10 &lt;code&gt;expect/actual&lt;/code&gt; seams, and this is the entire list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform info&lt;/strong&gt; — app version, system language, User-Agent string&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System interaction&lt;/strong&gt; — open URL, open app settings, share sheet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics&lt;/strong&gt; — a &lt;code&gt;trackEvent&lt;/code&gt; hook (Android → Aptabase; iOS is deliberately a
no-op for now)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebView&lt;/strong&gt; — the messy one (below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything that touches a platform API is small and enumerable. Everything else&lt;br&gt;
came for free.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why expect/actual and not an interface + DI?
&lt;/h3&gt;

&lt;p&gt;For these ~10 seams, &lt;code&gt;expect/actual&lt;/code&gt; was the least ceremony: no DI wiring, and&lt;br&gt;
the compiler refuses to build until every target implements the declaration. The&lt;br&gt;
moment a seam has more than one implementation, or I'd want to fake it in tests,&lt;br&gt;
an interface in &lt;code&gt;commonMain&lt;/code&gt; with injected impls is the better tool. For a fixed&lt;br&gt;
set of platform primitives, expect/actual wins on friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ugliest boundary: WebView
&lt;/h2&gt;

&lt;p&gt;I have two WebView paths, and I'll be precise because the repo is open:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rendering GitHub READMEs&lt;/strong&gt; from an HTML string — inline &lt;code&gt;expect/actual&lt;/code&gt; over
&lt;code&gt;android.webkit.WebView&lt;/code&gt; / &lt;code&gt;WKWebView&lt;/code&gt;, JS disabled, theming done by injecting
CSS colors so light/dark stays consistent. ~30–50 lines per side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opening external links in-app&lt;/strong&gt; — this one I pulled into a standalone
library, &lt;a href="https://github.com/HarlonWang/kmp-webview" rel="noopener noreferrer"&gt;kmp-webview&lt;/a&gt;, because the
lifecycle/config surface (file chooser, media capture, title handling) got
hairy enough to deserve its own home.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;WebView is hands-down the least elegant part of the codebase. If you've wrapped&lt;br&gt;
&lt;code&gt;WKWebView&lt;/code&gt; / Android &lt;code&gt;WebView&lt;/code&gt; behind a common composable, I'd genuinely welcome&lt;br&gt;
a sanity check on the library's API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest status: iOS is WIP
&lt;/h2&gt;

&lt;p&gt;Android is live on Google Play. iOS compiles and every Compose screen renders in&lt;br&gt;
the simulator — but it isn't polished:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;iPad share crashes without a &lt;code&gt;popoverPresentationController&lt;/code&gt; anchor (there's a
literal TODO)&lt;/li&gt;
&lt;li&gt;iOS has no analytics wired&lt;/li&gt;
&lt;li&gt;the AI-chat screen is Android-only right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not vaporware, just not shipped. And honestly, &lt;strong&gt;iOS is where the KMP tax&lt;br&gt;
actually shows up&lt;/strong&gt; — not in the shared code, but in the toolchain around it:&lt;br&gt;
living with Xcode for the shell, slower clean builds when the iOS target is in&lt;br&gt;
the loop, and fewer battle-tested libraries. The Kotlin/Compose side is smooth;&lt;br&gt;
the iOS edge is where I spend my "fighting the tools" time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Kotlin 2.3.21 · Compose Multiplatform 1.11.1 · Ktor 3.5.0 · Coil 3 ·&lt;br&gt;
multiplatform-settings · Material 3. Navigation is a small stack-based setup in&lt;br&gt;
&lt;code&gt;commonMain&lt;/code&gt; (data objects/classes as route keys), no platform-specific nav.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For a content/list/detail app, shared-UI CMP pays off on Android.&lt;/strong&gt; Write 17
screens once, not twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The native tax is small and predictable.&lt;/strong&gt; It lives at ~10 &lt;code&gt;expect/actual&lt;/code&gt;
seams — anything that wraps a platform view or calls a platform API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pain is exactly where you'd guess:&lt;/strong&gt; WebView, and the iOS toolchain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;expect/actual for fixed platform primitives; interfaces for anything you'd
test or swap.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be honest about iOS.&lt;/strong&gt; Compiling and rendering in a simulator is not the same
as shipping.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app is a GitHub Trending / Hacker News / Product Hunt reader, if you want&lt;br&gt;
context on the screens. Repo (MIT): &lt;a href="https://github.com/HarlonWang/TrendingAI" rel="noopener noreferrer"&gt;https://github.com/HarlonWang/TrendingAI&lt;/a&gt; —&lt;br&gt;
and the WebView lib: &lt;a href="https://github.com/HarlonWang/kmp-webview" rel="noopener noreferrer"&gt;https://github.com/HarlonWang/kmp-webview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What's your experience taking CMP to production, especially on iOS? I'd love to&lt;br&gt;
compare notes in the comments.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>android</category>
      <category>multiplatform</category>
    </item>
  </channel>
</rss>
