<?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>Forem: Mykhailo Dorokhin</title>
    <description>The latest articles on Forem by Mykhailo Dorokhin (@adjorno).</description>
    <link>https://forem.com/adjorno</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3718314%2F8ccbeeb9-8bcf-43af-98fb-83aff4c33186.png</url>
      <title>Forem: Mykhailo Dorokhin</title>
      <link>https://forem.com/adjorno</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/adjorno"/>
    <language>en</language>
    <item>
      <title>How I Moved a KMP Project from Scratch to Production on 5 Platforms (and What I Learned)</title>
      <dc:creator>Mykhailo Dorokhin</dc:creator>
      <pubDate>Sun, 18 Jan 2026 19:06:13 +0000</pubDate>
      <link>https://forem.com/adjorno/how-i-moved-a-kmp-project-from-scratch-to-production-on-5-platforms-and-what-i-learned-273b</link>
      <guid>https://forem.com/adjorno/how-i-moved-a-kmp-project-from-scratch-to-production-on-5-platforms-and-what-i-learned-273b</guid>
      <description>&lt;p&gt;I'm moving a small Kotlin Multiplatform project to production across 5 platforms. This is my experience so far.&lt;/p&gt;

&lt;p&gt;Overall? Surprisingly positive. Web, Android, and iOS are live. Desktop and CLI are working locally, distribution coming soon. I recorded all my coding sessions and they are on &lt;a href="https://www.youtube.com/playlist?list=PLGS6AZIpM4eHR6EWt6IZ8HeizdP8SOCxU" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The project is ongoing. Here's what actually broke along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Working
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;td&gt;Live&lt;/td&gt;
&lt;td&gt;&lt;a href="https://justusefuckingkotlin.com" rel="noopener noreferrer"&gt;justusefuckingkotlin.com&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android&lt;/td&gt;
&lt;td&gt;Beta&lt;/td&gt;
&lt;td&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.ifochka.jufk" rel="noopener noreferrer"&gt;Play Store&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;TestFlight&lt;/td&gt;
&lt;td&gt;&lt;a href="https://testflight.apple.com/join/sENnMKjM" rel="noopener noreferrer"&gt;Join Beta&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Coming soon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same Kotlin code compiles to WASM, JVM, and Native.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Kotlin 2.3.0 + Compose Multiplatform 1.9.3&lt;/li&gt;
&lt;li&gt;GitHub Actions for CI/CD&lt;/li&gt;
&lt;li&gt;Cloudflare Pages (web)&lt;/li&gt;
&lt;li&gt;fastlane (iOS + Android)&lt;/li&gt;
&lt;li&gt;ktlint + detekt&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://justusefuckingkotlin.com" rel="noopener noreferrer"&gt;justusefuckingkotlin.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/adjorno/JUFK" rel="noopener noreferrer"&gt;adjorno/JUFK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YouTube:&lt;/strong&gt; &lt;a href="https://www.youtube.com/playlist?list=PLGS6AZIpM4eHR6EWt6IZ8HeizdP8SOCxU" rel="noopener noreferrer"&gt;Build sessions playlist&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X:&lt;/strong&gt; &lt;a href="https://x.com/adjorno" rel="noopener noreferrer"&gt;@adjorno&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  9 Problems Nobody Warned Me About
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. macOS App Store rejects KMP apps
&lt;/h3&gt;

&lt;p&gt;Apple requires every binary signed: your app, JVM runtime, Kotlin native libs, every &lt;code&gt;.dylib&lt;/code&gt;. Each needs provisioning profiles. One unsigned file = instant rejection.&lt;/p&gt;

&lt;p&gt;Our workaround: skip Mac App Store. Distribute via &lt;strong&gt;DMG&lt;/strong&gt; (signed + notarized, simpler) in Github Releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. WASM vs JS: Material Icons Extended breaks Webpack
&lt;/h3&gt;

&lt;p&gt;Tried JS target for compatibility. Webpack couldn't optimize Material Icons Extended. Switched back to WASM.&lt;/p&gt;

&lt;p&gt;If you need extended icons on web, stick with &lt;code&gt;wasmJs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. detekt silently skips KMP code
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;./gradlew detekt&lt;/code&gt; passes but checks nothing. You need source-set-specific tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew &lt;span class="se"&gt;\&lt;/span&gt;
  detektMetadataCommonMain &lt;span class="se"&gt;\&lt;/span&gt;
  detektAndroidMain &lt;span class="se"&gt;\&lt;/span&gt;
  detektIosArm64Main &lt;span class="se"&gt;\&lt;/span&gt;
  detektDesktopMain &lt;span class="se"&gt;\&lt;/span&gt;
  detektWasmJsMain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;detektMetadataCommonMain&lt;/code&gt; is your shared code. Without it, CI is green while checking zero lines.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. iOS fastlane version injection fails silently with &lt;code&gt;GENERATE_INFOPLIST_FILE = true&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;GENERATE_INFOPLIST_FILE&lt;/code&gt; is set to &lt;code&gt;true&lt;/code&gt; in your KMP project, &lt;code&gt;fastlane&lt;/code&gt; actions like &lt;code&gt;increment_version_number&lt;/code&gt; have no effect because Xcode generates &lt;code&gt;Info.plist&lt;/code&gt; dynamically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Either set &lt;code&gt;GENERATE_INFOPLIST_FILE = false&lt;/code&gt; in your Xcode project settings, or inject version properties directly via &lt;code&gt;xcargs&lt;/code&gt; in your &lt;code&gt;Fastfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;build_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;xcargs: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"MARKETING_VERSION=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;sanitized_version&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"CURRENT_PROJECT_VERSION=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;build_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&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;h3&gt;
  
  
  5. Cloudflare Pages needs &lt;code&gt;branch: main&lt;/code&gt; for tag deployments
&lt;/h3&gt;

&lt;p&gt;When deploying from a git tag, Cloudflare doesn't know your branch. Without this, deployments go to preview URLs instead of production. This snippet was adapted from the official Cloudflare Workers CI/CD documentation for GitHub Actions: &lt;a href="https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/" rel="noopener noreferrer"&gt;https://developers.cloudflare.com/workers/ci-cd/external-cicd/github-actions/&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to Cloudflare&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare/wrangler-action@v3&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;apiToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_API_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CLOUDFLARE_ACCOUNT_ID }}&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pages deploy ./composeApp/build/dist/wasmJs/productionExecutable --project-name=${{ inputs.cloudflare_project_name }} --branch main&lt;/span&gt; &lt;span class="c1"&gt;# Required for tags/production deployments&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Desktop packaging fails with version &lt;code&gt;0.x.y&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;DMG, MSI, PKG all require major version ≥ 1. And &lt;code&gt;packageVersion&lt;/code&gt; must be set BEFORE &lt;code&gt;targetFormats()&lt;/code&gt; — that call triggers validation immediately.&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="nf"&gt;nativeDistributions&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;pkgVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"0."&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="s"&gt;"1.0.0"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;
    &lt;span class="n"&gt;packageVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pkgVersion&lt;/span&gt;  &lt;span class="c1"&gt;// Set first&lt;/span&gt;
    &lt;span class="nf"&gt;targetFormats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dmg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Msi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Deb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Validates here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Windows build validates ALL desktop formats
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;createDistributable&lt;/code&gt; on Windows still validates DMG/PKG and fails. Use the platform-specific task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Wrong&lt;/span&gt;
&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew createDistributable&lt;/span&gt;

&lt;span class="c1"&gt;# Right&lt;/span&gt;
&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew packageMsi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Unicode arrows don't render on Compose Web
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;→&lt;/code&gt; and &lt;code&gt;▸&lt;/code&gt; show as boxes. Canvas rendering doesn't handle Unicode fonts well.&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="c1"&gt;// Broken&lt;/span&gt;
&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"→"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Works&lt;/span&gt;
&lt;span class="nc"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Icons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AutoMirrored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Filled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArrowForward&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires &lt;code&gt;compose.materialIconsExtended&lt;/code&gt; dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. A surprising 3x CI speed boost from &lt;code&gt;setup-gradle&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I knew that caching dependencies with &lt;code&gt;gradle/actions/setup-gradle&lt;/code&gt; would make CI faster. What I didn't expect was &lt;em&gt;how much&lt;/em&gt; faster. For this KMP project, adding this single line cut the build time by nearly 3x, from over 6 minutes to just 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gradle/actions/setup-gradle@v4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It caches dependencies and build outputs. Without it, every CI run downloads everything fresh, which is especially painful with multi-platform artifacts.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;KMP tooling is ready. These are edge cases, not blockers. If you've been waiting to try it — now's the time.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>ios</category>
      <category>android</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
