<?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: Raul Arroyo</title>
    <description>The latest articles on DEV Community by Raul Arroyo (@rarroyo00).</description>
    <link>https://dev.to/rarroyo00</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%2F1406089%2F51d04800-78a2-4f3d-9521-2c012aa4b42e.jpeg</url>
      <title>DEV Community: Raul Arroyo</title>
      <link>https://dev.to/rarroyo00</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rarroyo00"/>
    <language>en</language>
    <item>
      <title>The UI Details That Make It Feel Native</title>
      <dc:creator>Raul Arroyo</dc:creator>
      <pubDate>Sat, 06 Jun 2026 05:01:00 +0000</pubDate>
      <link>https://dev.to/rarroyo00/coming-back-to-kotlin-building-a-real-app-with-kmp-part-4-making-compose-feel-native-2g5l</link>
      <guid>https://dev.to/rarroyo00/coming-back-to-kotlin-building-a-real-app-with-kmp-part-4-making-compose-feel-native-2g5l</guid>
      <description>&lt;h2&gt;
  
  
  Part 4 - The UI Layer, Native Chrome, and the Details That Make It Feel Like an App
&lt;/h2&gt;




&lt;p&gt;In Part 3 we converted raw WordPress HTML into native content blocks.&lt;/p&gt;

&lt;p&gt;That solved one big problem: the app no longer had to display a website inside a &lt;code&gt;WebView&lt;/code&gt;. Posts became structured content that Compose could render natively.&lt;/p&gt;

&lt;p&gt;But once the content was native, another problem became obvious.&lt;/p&gt;

&lt;p&gt;The app worked, but it still needed a visual identity.&lt;/p&gt;

&lt;p&gt;Not just colors. Not just a logo. The whole reading experience needed to feel designed: typography, spacing, hero images, top bars, progress indicators, shapes, transitions, and platform behavior.&lt;/p&gt;

&lt;p&gt;This part is about styling the app. Not in the sense of "making it pretty", but in the sense of making the interface support the content and feel intentional.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With a Technically Correct UI
&lt;/h2&gt;

&lt;p&gt;The first version of the detail screen was functional.&lt;/p&gt;

&lt;p&gt;It had an image, a title, the author, and the parsed content blocks from Part 3. Text rendered as text, images rendered as images, embeds had their own components.&lt;/p&gt;

&lt;p&gt;Technically, that was a big step forward.&lt;/p&gt;

&lt;p&gt;Visually, it was not enough.&lt;/p&gt;

&lt;p&gt;The screen still felt too close to a default Compose app. Clean, readable, but generic. And for a content-heavy app, generic is a problem. The UI has to create a reading mood before the user reaches the first paragraph.&lt;/p&gt;

&lt;p&gt;The goal became:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make articles feel editorial&lt;/li&gt;
&lt;li&gt;keep content readable&lt;/li&gt;
&lt;li&gt;avoid a default Material look&lt;/li&gt;
&lt;li&gt;support dark mode&lt;/li&gt;
&lt;li&gt;keep Android and iOS feeling native&lt;/li&gt;
&lt;li&gt;avoid overbuilding a custom design system too early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters. It is easy to disappear into design-system work before the app has enough real screens to justify it.&lt;/p&gt;

&lt;p&gt;So the approach was pragmatic: use Material3 as a base, then override the pieces that affected the product feel the most.&lt;/p&gt;




&lt;h2&gt;
  
  
  Typography as Branding
&lt;/h2&gt;

&lt;p&gt;The biggest visual change came from typography.&lt;/p&gt;

&lt;p&gt;Default Material typography is good, but it is intentionally neutral. For this app, neutral made the UI feel like a template.&lt;/p&gt;

&lt;p&gt;The content needed stronger hierarchy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;big editorial headlines&lt;/li&gt;
&lt;li&gt;readable body text&lt;/li&gt;
&lt;li&gt;compact labels&lt;/li&gt;
&lt;li&gt;a different feel between covers, sections, and article body&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app defines custom font families once:&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;data class&lt;/span&gt; &lt;span class="nc"&gt;AppFonts&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;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FontFamily&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FontFamily&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;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;FontFamily&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 the Material typography scale is adapted instead of ignored:&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="nc"&gt;MaterialTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;typography&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;appTypography&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fonts&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;colorScheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appColors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;shapes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appShapes&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="nc"&gt;AppContent&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;This was an important decision.&lt;/p&gt;

&lt;p&gt;I did not want every component to manually choose fonts. That becomes messy quickly. But I also did not want the default Material scale to define the personality of the app.&lt;/p&gt;

&lt;p&gt;So the theme sets the baseline, and individual branded moments can still be more explicit:&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="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typography&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coverTitle&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made the hero feel like part of the product, not just a header.&lt;/p&gt;

&lt;p&gt;The lesson: typography is not decoration. In a content app, typography is part of the product behavior.&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.amazonaws.com%2Fuploads%2Farticles%2Fx4rbf8bysbua0d7eixkd.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.amazonaws.com%2Fuploads%2Farticles%2Fx4rbf8bysbua0d7eixkd.png" alt="App fonts" width="404" height="647"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Platform Problem
&lt;/h2&gt;

&lt;p&gt;A real KMP app has to answer an uncomfortable question:&lt;/p&gt;

&lt;p&gt;Should the UI be identical on Android and iOS?&lt;/p&gt;

&lt;p&gt;For the article content, shared Compose UI made sense. The content model is shared, the rendering logic is shared, and the reading experience should be consistent.&lt;/p&gt;

&lt;p&gt;But navigation chrome is different.&lt;/p&gt;

&lt;p&gt;iOS users have different expectations around back buttons, safe areas, toolbar spacing, glass effects, and touch behavior. Android has its own expectations too.&lt;/p&gt;

&lt;p&gt;Trying to force the exact same chrome everywhere made the app feel less native.&lt;/p&gt;

&lt;p&gt;So the decision was hybrid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared Compose for the content&lt;/li&gt;
&lt;li&gt;shared Kotlin state and navigation&lt;/li&gt;
&lt;li&gt;Compose chrome where it feels right&lt;/li&gt;
&lt;li&gt;native iOS chrome where platform feel matters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Android vs iOS
&lt;/h3&gt;

&lt;p&gt;This is the comparison that best explains the decision.&lt;/p&gt;

&lt;p&gt;The article content can stay the same, but the surrounding chrome should respect the platform. On Android, the Compose version can feel natural. On iOS, native toolbar behavior, safe areas, and button styling make a visible difference.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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.amazonaws.com%2Fuploads%2Farticles%2Fgr81cmsom7bacq631dde.png" alt="iOS Home" width="800" height="1739"&gt;&lt;/td&gt;
&lt;td&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.amazonaws.com%2Fuploads%2Farticles%2F9nms6pv901kdw5pn52rn.png" alt="Android Home" width="800" height="1781"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The split ended up looking roughly like this:&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.amazonaws.com%2Fuploads%2Farticles%2Fp2ihhn84tcl0zvmdqtj5.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.amazonaws.com%2Fuploads%2Farticles%2Fp2ihhn84tcl0zvmdqtj5.png" alt="Diagram" width="576" height="243"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bridge stays small. Compose sends simple chrome state to iOS:&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="nc"&gt;ArticleChromeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;collapsed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readProgress&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SwiftUI renders the native buttons and calls back into Kotlin:&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="kt"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;systemName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"heart"&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;The important part is what is not duplicated.&lt;/p&gt;

&lt;p&gt;There is no second article screen in Swift. No second saved-state implementation. No separate navigation model.&lt;/p&gt;

&lt;p&gt;Native chrome is presentation. Kotlin still owns behavior.&lt;/p&gt;

&lt;p&gt;That decision sets up the rest of the styling work: the content can share the same visual language, while each platform keeps the pieces that users expect to feel native.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hero Image
&lt;/h2&gt;

&lt;p&gt;The article hero became the first big styling decision.&lt;/p&gt;

&lt;p&gt;A normal image at the top of the screen made the layout feel like a feed item expanded into a detail page. That was not the feeling I wanted. The article needed to open more like a cover.&lt;/p&gt;

&lt;p&gt;So the detail screen starts with a tall, full-width hero image, with the category and title placed over it.&lt;/p&gt;

&lt;p&gt;The problem: real CMS images are unpredictable.&lt;/p&gt;

&lt;p&gt;Some images are bright. Some are dark. Some are concert photos with harsh lights. Some have faces near the title area. Some have text inside the image. Some have no clear focal point.&lt;/p&gt;

&lt;p&gt;If the title is rendered directly over that, readability breaks immediately.&lt;/p&gt;

&lt;p&gt;The fix was to treat the image as hostile until proven otherwise:&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="nc"&gt;Box&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;RemoteCoverImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;ReadabilityGradient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nc"&gt;CoverTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&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;There is nothing clever here. It is just a gradient.&lt;/p&gt;

&lt;p&gt;But this kind of boring styling decision is what makes dynamic content survive real-world data. The image still feels immersive, but the title remains readable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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.amazonaws.com%2Fuploads%2Farticles%2Flhg7iwa03m30dqlb858g.png" alt="Hero iOS" width="399" height="414"&gt;&lt;/td&gt;
&lt;td&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.amazonaws.com%2Fuploads%2Farticles%2Fml3ocmfn399oex79mzlx.png" alt="Hero Android" width="395" height="372"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Escaping the Default Material Look
&lt;/h2&gt;

&lt;p&gt;Material3 is a great foundation, but it has a visual opinion.&lt;/p&gt;

&lt;p&gt;That opinion is useful: accessible colors, typography slots, component behavior, interaction states, dark mode support. But if you accept all of it unchanged, your app can start to look like every other Material app.&lt;/p&gt;

&lt;p&gt;One of the first things that felt wrong was shape.&lt;/p&gt;

&lt;p&gt;The default rounded corners made the interface feel softer than intended. The app needed sharper edges, especially around content surfaces and editorial blocks.&lt;/p&gt;

&lt;p&gt;So the theme defines flatter shapes:&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;val&lt;/span&gt; &lt;span class="py"&gt;appShapes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Shapes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;small&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sharpCorners&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;medium&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sharpCorners&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;large&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sharpCorners&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gotcha: passing shapes into &lt;code&gt;MaterialTheme&lt;/code&gt; is not a magic wand.&lt;/p&gt;

&lt;p&gt;Some Material components have their own defaults. Some custom components already had explicit shapes. Some buttons and cards still needed direct styling.&lt;/p&gt;

&lt;p&gt;So the decision became:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use Material3 for structure and behavior&lt;/li&gt;
&lt;li&gt;use the app theme as the baseline&lt;/li&gt;
&lt;li&gt;override specific components when the product identity needs it&lt;/li&gt;
&lt;li&gt;avoid creating a giant design system before the UI proves what it needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That balance kept the app moving.&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.amazonaws.com%2Fuploads%2Farticles%2Fdeegfrz4g6koe7xgpjyn.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.amazonaws.com%2Fuploads%2Farticles%2Fdeegfrz4g6koe7xgpjyn.png" alt="Material Look" width="800" height="1781"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Collapsing Top Bar
&lt;/h2&gt;

&lt;p&gt;The top of the article should feel immersive.&lt;/p&gt;

&lt;p&gt;The user lands on the hero image and title first, not a heavy toolbar. But after scrolling, the screen needs to become practical: back button, title, save, share, and progress.&lt;/p&gt;

&lt;p&gt;The top bar starts transparent and becomes solid once the reader scrolls past enough of the hero:&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;val&lt;/span&gt; &lt;span class="py"&gt;collapsed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;scrollOffset&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;heroHeight&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;collapseThreshold&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the background and title fade in:&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;val&lt;/span&gt; &lt;span class="py"&gt;topBarAlpha&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;animateFloatAsState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;targetValue&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;collapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;1f&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0f&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was less about animation and more about rhythm.&lt;/p&gt;

&lt;p&gt;At the beginning, the screen behaves like a cover.&lt;/p&gt;

&lt;p&gt;After scrolling, it behaves like a reader.&lt;/p&gt;

&lt;p&gt;That transition made the screen feel much more intentional without adding much complexity.&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.amazonaws.com%2Fuploads%2Farticles%2Ftnc416vnetkkm9la9m7n.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Ftnc416vnetkkm9la9m7n.gif" alt="Collapsing" width="422" height="254"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading Progress
&lt;/h2&gt;

&lt;p&gt;The reading progress indicator came from a simple observation: posts are not always short, and the body can contain very different block types.&lt;/p&gt;

&lt;p&gt;Text, images, galleries, embeds, and related content do not create a uniform scroll experience. A small progress indicator gives users a sense of place without interrupting the article.&lt;/p&gt;

&lt;p&gt;The implementation is intentionally approximate:&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;val&lt;/span&gt; &lt;span class="py"&gt;readProgress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;visibleContent&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;totalContent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is not pixel-perfect.&lt;/p&gt;

&lt;p&gt;But pixel-perfect progress would mean measuring dynamic content that may change size after images or embeds load. That is more complexity for very little user benefit.&lt;/p&gt;

&lt;p&gt;The decision was to optimize for feel, not mathematical precision.&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.amazonaws.com%2Fuploads%2Farticles%2Fwuuods8rjtjp1kr785cx.gif" 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.amazonaws.com%2Fuploads%2Farticles%2Fwuuods8rjtjp1kr785cx.gif" alt="Reading progress" width="599" height="221"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Insets: The Detail You Notice When It Fails
&lt;/h2&gt;

&lt;p&gt;Once native chrome enters the picture, layout gets more delicate.&lt;/p&gt;

&lt;p&gt;If SwiftUI renders a navigation bar, Compose needs to know how much space that bar occupies. Otherwise content can end up behind it, especially when an article has no hero image.&lt;/p&gt;

&lt;p&gt;The fix was to measure the native chrome and send the inset back to Compose:&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;val&lt;/span&gt; &lt;span class="py"&gt;topPadding&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;usesPlatformChrome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;measuredChromeHeight&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;composeToolbarHeight&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not the exciting part of styling.&lt;/p&gt;

&lt;p&gt;But it is one of the most important parts of making the app feel polished. Users may not know what a safe area is, but they absolutely notice when content starts in the wrong place.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The UI started as technically correct, but generic.&lt;/p&gt;

&lt;p&gt;After this pass, it had more identity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;full-bleed article heroes&lt;/li&gt;
&lt;li&gt;protected title readability over unpredictable images&lt;/li&gt;
&lt;li&gt;custom typography for editorial hierarchy&lt;/li&gt;
&lt;li&gt;sharper Material shapes&lt;/li&gt;
&lt;li&gt;a collapsing top bar that changes with reading context&lt;/li&gt;
&lt;li&gt;lightweight reading progress&lt;/li&gt;
&lt;li&gt;shared content UI&lt;/li&gt;
&lt;li&gt;platform-aware chrome where it matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson was that styling a real app is not one big theme file.&lt;/p&gt;

&lt;p&gt;It is a series of decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What should be branded?&lt;/li&gt;
&lt;li&gt;What should stay standard?&lt;/li&gt;
&lt;li&gt;What should be shared?&lt;/li&gt;
&lt;li&gt;What should feel native?&lt;/li&gt;
&lt;li&gt;What complexity is worth it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where KMP became interesting for this part of the app. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It was not about proving that everything could be shared. It was about deciding what should be shared.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;In Part 5 I will cover the modularization work: splitting the project into client, data, domain, database, and UI modules, and what changed once the app stopped being one big Compose module.&lt;/p&gt;




&lt;p&gt;Stack: Kotlin 2.3.10 · Compose Multiplatform 1.10.2 · Ktor 3.4.1 · SQLDelight 2.3.1 · Koin 4.1.1&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>android</category>
      <category>multiplatform</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Coming Back to Kotlin: Building a Real App with KMP Part 3 — From Raw HTML to Native Content dame algo parecido</title>
      <dc:creator>Raul Arroyo</dc:creator>
      <pubDate>Wed, 25 Mar 2026 00:44:51 +0000</pubDate>
      <link>https://dev.to/rarroyo00/coming-back-to-kotlin-building-a-real-app-with-kmp-part-3-from-raw-html-to-native-content-16nl</link>
      <guid>https://dev.to/rarroyo00/coming-back-to-kotlin-building-a-real-app-with-kmp-part-3-from-raw-html-to-native-content-16nl</guid>
      <description>&lt;h2&gt;
  
  
  Part 3 — Parsing WordPress HTML and Enriching Embeds
&lt;/h2&gt;




&lt;p&gt;WordPress posts arrive as raw HTML strings. A single post might contain paragraphs, headings, YouTube iframes, Spotify embeds, Instagram blockquotes, image galleries, internal post references, and ordered lists — all mixed together, none of it consistent.&lt;/p&gt;

&lt;p&gt;You can't just dump that into a &lt;code&gt;WebView&lt;/code&gt; and call it a day. Well, you could. But the result looks like a website inside an app, it doesn't respect your theme, dark mode breaks, fonts are wrong, and interactions feel foreign. For a magazine app where the reading experience is the whole point, that's not acceptable.&lt;/p&gt;

&lt;p&gt;The approach: a custom HTML parser that converts raw WordPress HTML into a typed &lt;code&gt;List&amp;lt;ContentPart&amp;gt;&lt;/code&gt; that Compose can render natively.&lt;/p&gt;




&lt;h2&gt;
  
  
  ContentPart — The Typed Model
&lt;/h2&gt;

&lt;p&gt;The first thing to build is the sealed interface that represents every possible block of content:&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="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Text&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AnnotatedString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Heading&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AnnotatedString&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;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Image&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;alt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Gallery&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;imageUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&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;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Video&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;thumbnailUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;videoUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Spotify&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;embedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&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;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;thumbnailUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&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;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;InstagramPost&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;InternalPost&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;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;UnorderedList&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;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnnotatedString&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;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;OrderedList&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;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnnotatedString&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;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;CallToAction&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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Divider&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Html&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Html&lt;/code&gt; is the escape hatch — anything the parser doesn't recognize yet goes there. It renders as-is with a fallback composable. Over time, as new block types show up in the content, they get their own &lt;code&gt;ContentPart&lt;/code&gt; subtype and a proper UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Parser — Strategy Pattern
&lt;/h2&gt;

&lt;p&gt;The parser uses a &lt;code&gt;BlockHandler&lt;/code&gt; interface:&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;interface&lt;/span&gt; &lt;span class="nc"&gt;BlockHandler&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;canHandle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;
    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each handler knows how to process one type of block. The main &lt;code&gt;HtmlParser&lt;/code&gt; finds block-level tags with a regex, extracts the full block content (with a depth counter for nested tags), and routes to the right handler:&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;val&lt;/span&gt; &lt;span class="py"&gt;blockRegex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"&amp;lt;(p|img|iframe|figure|hr|ul|ol|h[1-6])(?:\\s[^&amp;gt;]*)?&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;RegexOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IGNORE_CASE&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handlers are registered as a list and checked in order:&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="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;defaultHandlers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BlockHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;DividerBlockHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;ImageBlockHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;TextBlockHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;EmbedBlockHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;CallToActionBlockHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;ListBlockHandler&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A KMP-specific gotcha
&lt;/h3&gt;

&lt;p&gt;Kotlin's &lt;code&gt;Regex&lt;/code&gt; on KMP doesn't support &lt;code&gt;DOT_MATCHES_ALL&lt;/code&gt; as a &lt;code&gt;RegexOption&lt;/code&gt;. On JVM you can use &lt;code&gt;(?s)&lt;/code&gt; in the pattern or &lt;code&gt;Pattern.DOTALL&lt;/code&gt;. In KMP the portable fix is replacing &lt;code&gt;.&lt;/code&gt; with &lt;code&gt;[\s\S]&lt;/code&gt; in any regex that needs to match across newlines. Worth knowing before you spend an hour wondering why your regex works in unit tests on JVM but silently fails on iOS.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tricky Cases
&lt;/h2&gt;

&lt;p&gt;Most block types are straightforward — a &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; becomes &lt;code&gt;ContentPart.Text&lt;/code&gt;, an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; becomes &lt;code&gt;ContentPart.Image&lt;/code&gt;, an &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt; becomes &lt;code&gt;ContentPart.Divider&lt;/code&gt;. The interesting ones are the embeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  YouTube — lazy loading
&lt;/h3&gt;

&lt;p&gt;WordPress with the custom theme uses lazy loading on iframes. The actual URL isn't in &lt;code&gt;src&lt;/code&gt; — it's in &lt;code&gt;data-lazy-src&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"about:blank"&lt;/span&gt; 
        &lt;span class="na"&gt;data-lazy-src=&lt;/span&gt;&lt;span class="s"&gt;"https://www.youtube.com/embed/{videoId}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any image validation logic needs to filter out &lt;code&gt;about:blank&lt;/code&gt; URLs or you'll end up with broken thumbnails. The &lt;code&gt;EmbedBlockHandler&lt;/code&gt; checks &lt;code&gt;data-lazy-src&lt;/code&gt; first, then falls back to &lt;code&gt;src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For YouTube we don't embed the player — we show a thumbnail with a play button that opens the YouTube app via a confirmation dialog. Clean, fast, no WebView required.&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.amazonaws.com%2Fuploads%2Farticles%2F5gsxoxmb244cm1zibspk.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.amazonaws.com%2Fuploads%2Farticles%2F5gsxoxmb244cm1zibspk.png" alt="Youtube" width="399" height="225"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fep4bne4z9zbxhqebztt6.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.amazonaws.com%2Fuploads%2Farticles%2Fep4bne4z9zbxhqebztt6.png" alt="Youtube open dialog" width="402" height="227"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Spotify — oEmbed enrichment
&lt;/h3&gt;

&lt;p&gt;Spotify embeds arrive as an iframe with just the embed URL. Not very exciting to look at as a card. But Spotify has a public oEmbed endpoint that returns the track/album/playlist title and thumbnail — no authentication required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://open.spotify.com/oembed?url={spotifyUrl}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SpotifyContentPart&lt;/code&gt; composable fetches this data with a &lt;code&gt;LaunchedEffect&lt;/code&gt; and shows a skeleton while it loads. The result is a card with the actual album art and track title — something worth tapping.&lt;/p&gt;

&lt;p&gt;The decision of &lt;em&gt;where&lt;/em&gt; to make this request was interesting. It could live in the ViewModel, pre-fetched for all Spotify parts before rendering. But that means waiting for N requests before showing anything. Instead, each &lt;code&gt;SpotifyContentPart&lt;/code&gt; fetches its own data independently using &lt;code&gt;koinInject&amp;lt;SpotifyService&amp;gt;()&lt;/code&gt; directly in the composable. The cards load progressively as you scroll.&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.amazonaws.com%2Fuploads%2Farticles%2Fxia872yf8usq5o5o4glb.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.amazonaws.com%2Fuploads%2Farticles%2Fxia872yf8usq5o5o4glb.png" alt="Spotify" width="388" height="117"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fhj56q3bjs52kfm5hx82h.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.amazonaws.com%2Fuploads%2Farticles%2Fhj56q3bjs52kfm5hx82h.png" alt="Spotify open dialog" width="403" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Instagram — blockquote detection
&lt;/h3&gt;

&lt;p&gt;Instagram blocks thumbnail URLs from third-party apps — or so I thought. &lt;br&gt;
Instagram actually has a semi-public thumbnail URL pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.instagram.com/p/{shortcode}/media/?size=l
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We extract the shortcode from the embed URL and attempt to load it with &lt;code&gt;SubcomposeAsyncImage&lt;/code&gt;. If Instagram blocks the request (which it does inconsistently), the &lt;code&gt;error&lt;/code&gt; slot shows a fallback placeholder with the Instagram gradient branding. If it loads — and it does load sometimes — you get the actual post image with the aspect ratio adjusted dynamically using &lt;code&gt;onSuccess&lt;/code&gt; to read the painter's &lt;code&gt;intrinsicSize&lt;/code&gt;.&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="n"&gt;onSuccess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;painter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intrinsicSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;height&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;painter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intrinsicSize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&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;width&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;imageAspectRatio&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;height&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;The container height starts at &lt;code&gt;220.dp&lt;/code&gt; as a fallback and adjusts to the real aspect ratio once the image loads. Not perfect, but better than a static placeholder.&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.amazonaws.com%2Fuploads%2Farticles%2Fnepvw6conbrh7xwtir5b.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.amazonaws.com%2Fuploads%2Farticles%2Fnepvw6conbrh7xwtir5b.png" alt="Instagram" width="389" height="592"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Felpvo62d5k4alqd7rwq9.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.amazonaws.com%2Fuploads%2Farticles%2Felpvo62d5k4alqd7rwq9.png" alt="Instagram open dialog" width="392" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Internal post references
&lt;/h3&gt;

&lt;p&gt;The magazine embeds links to related articles within post content using a custom WordPress block class: &lt;code&gt;wp-block-embed-my-wp-site&lt;/code&gt;. The &lt;code&gt;EmbedBlockHandler&lt;/code&gt; detects this and extracts the slug from the href:&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wp-block-embed-my-wp-site"&lt;/span&gt;&lt;span class="p"&gt;))&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;slug&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractSiteSlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;emptyList&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;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractEmbedTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InternalPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&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;At render time, &lt;code&gt;InternalPostContentPart&lt;/code&gt; resolves the slug to a post ID via the repository — first checking the local DB, then the API if not cached — and navigates to that post on tap using the &lt;code&gt;AppNavigator&lt;/code&gt; singleton from Part 2.&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.amazonaws.com%2Fuploads%2Farticles%2F5nqj6jj2woo68kiftk23.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.amazonaws.com%2Fuploads%2Farticles%2F5nqj6jj2woo68kiftk23.png" alt="Internal post" width="383" height="103"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Image Grouping
&lt;/h2&gt;

&lt;p&gt;WordPress content frequently includes multiple consecutive images — a photo series from a concert, an event gallery. Rendering them one-by-one as full-width images takes forever to scroll through.&lt;/p&gt;

&lt;p&gt;After parsing, the ViewModel runs a post-processing step that groups three or more consecutive &lt;code&gt;ContentPart.Image&lt;/code&gt; items into a &lt;code&gt;ContentPart.Gallery&lt;/code&gt;:&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;groupConsecutiveImages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="n"&gt;minCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mutableListOf&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&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;part&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;part&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;j&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;j&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;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;i&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;count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;minCount&lt;/span&gt;&lt;span class="p"&gt;)&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;images&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filterIsInstance&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mapNotNull&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="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Gallery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageUrls&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;i&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="n"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;GalleryContentPart&lt;/code&gt; composable renders these with different layouts depending on count — 3 images get a "hero + two below" layout, 4 get a "large left + three stacked right" layout, and 5+ get a mosaic with a &lt;code&gt;+N&lt;/code&gt; counter on the last tile. Tapping any image or the counter opens a full-screen gallery viewer with pinch-to-zoom and swipe navigation.&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.amazonaws.com%2Fuploads%2Farticles%2Fvliz2g2g3icv7pmww9f4.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.amazonaws.com%2Fuploads%2Farticles%2Fvliz2g2g3icv7pmww9f4.png" alt="Gallery" width="385" height="315"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fqm7x8v5qcf1214l4ordc.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.amazonaws.com%2Fuploads%2Farticles%2Fqm7x8v5qcf1214l4ordc.png" alt="Gallery plus" width="386" height="461"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;After parsing, a typical post produces something like:&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="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&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;"La cantante belga Angèle ha regresado..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InstagramPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://www.instagram.com/p/DVTyrbVCPXe/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&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;"El video musical, creado con (LA)HORDE..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Video&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thumbnailUrl&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="n"&gt;videoUrl&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each type gets its own composable renderer. The &lt;code&gt;LazyColumn&lt;/code&gt; in &lt;code&gt;PostDetailContent&lt;/code&gt; iterates over the list and dispatches to the right component. No WebViews, no HTML rendering engines, no platform-specific workarounds — just Compose all the way down.&lt;/p&gt;




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

&lt;p&gt;In Part 4 we cover the UI layer — the &lt;code&gt;PostHero&lt;/code&gt; component with the collapsible top bar, custom theming with Anton and Space Grotesk fonts, platform-specific transitions, and the Material3 shapes gotcha that will catch you off guard if you expect the theme to propagate automatically.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack: Kotlin 2.3.10 · Compose Multiplatform 1.10.2 · Ktor 3.4.1 · SQLDelight 2.3.1 · Koin 4.1.1&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>mobile</category>
      <category>tutorial</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>Building the Architecture in KMP: Data Flow, MVI, and Hard Decisions</title>
      <dc:creator>Raul Arroyo</dc:creator>
      <pubDate>Fri, 20 Mar 2026 01:07:12 +0000</pubDate>
      <link>https://dev.to/rarroyo00/building-the-architecture-in-kmp-data-flow-mvi-and-hard-decisions-4bbm</link>
      <guid>https://dev.to/rarroyo00/building-the-architecture-in-kmp-data-flow-mvi-and-hard-decisions-4bbm</guid>
      <description>&lt;h2&gt;
  
  
  Part 2 — Architecture, MVI, and the Offline-First Decision
&lt;/h2&gt;




&lt;p&gt;In Part 1 I wrote about the stack and the first walls — library compatibility, Navigation3 bugs, Coil 3 differences. Now we get into the decisions that shaped how the whole app is structured.&lt;/p&gt;

&lt;p&gt;Fair warning: this post has opinions. Architecture is one of those topics where everyone has a preferred approach, and mine was shaped by years of Android development followed by three years of Flutter. Your mileage may vary.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Overall Structure
&lt;/h2&gt;

&lt;p&gt;I'm following a simplified Clean Architecture. Not strict by the book, but principled enough that I can navigate the codebase without thinking too hard about where things live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data/
  remote/
    model/       ← DTOs — what the API returns
    service/     ← Ktor clients (WordPress, Spotify)
    mapper/      ← DTO → Entity, Entity → Domain
  repository/    ← PostRepository — single source of truth
domain/
  model/         ← PostDetail, PostSummary, ContentPart
ui/
  screens/       ← ViewModels + Composables
  components/    ← Shared UI components
  navigation/    ← Navigation3 setup
  theme/         ← Colors, typography, shapes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key constraint I set for myself: &lt;strong&gt;the UI layer never talks to the network directly&lt;/strong&gt;. Everything goes through the repository, and the repository always reads from the database. The network is just a way to keep the database up to date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Domain Models Instead of One
&lt;/h2&gt;

&lt;p&gt;Early on I had a single &lt;code&gt;Post&lt;/code&gt; model that tried to serve both the list view and the detail view. That doesn't scale well. The list needs almost nothing — title, excerpt, image, category. The detail needs author info, content, tags, related posts. Cramming those into one model means either always fetching too much or constantly dealing with nullable fields that "don't apply here."&lt;/p&gt;

&lt;p&gt;I split them:&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;// For lists — lightweight, fast to load from DB&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;PostSummary&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;excerpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;categoryName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;isSaved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// For the detail screen — everything we need&lt;/span&gt;
&lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;PostDetail&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&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;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;categoryName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;categoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&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;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Author&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;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&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;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;tagIds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isSaved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing worth noting: &lt;code&gt;PostDetail&lt;/code&gt; has both &lt;code&gt;tags&lt;/code&gt; (the display names, like "Vive Latino") and &lt;code&gt;tagIds&lt;/code&gt; (the WordPress IDs, like &lt;code&gt;[38763, 1032]&lt;/code&gt;). The names come from the API's embedded &lt;code&gt;_embedded.wp:term&lt;/code&gt; data. The IDs come from the top-level &lt;code&gt;tags&lt;/code&gt; array. We need both — names for display, IDs for querying related posts.&lt;/p&gt;

&lt;p&gt;Also notice that &lt;code&gt;PostDetail&lt;/code&gt; has &lt;code&gt;content&lt;/code&gt; as a raw string, but we never show it directly. It gets parsed into &lt;code&gt;List&amp;lt;ContentPart&amp;gt;&lt;/code&gt; in the ViewModel before reaching the UI. More on the parser in Part 3.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Data Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WordPress API (Ktor)
    ↓
WordpressPostDto
    ↓  toEntity()
PostEntity (SQLDelight DB)
    ↓  toPostSummary() / toPostDetail()
Domain Model
    ↓
ViewModel → UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every piece of data touches the database. The API is the source of &lt;em&gt;new&lt;/em&gt; data. The database is the source of &lt;em&gt;truth&lt;/em&gt; for the UI. This gives you offline support for free — if the API fails, you still render cached content.&lt;/p&gt;

&lt;p&gt;The mapper layer is split into two files: &lt;code&gt;WordpressPostDtoMapper.kt&lt;/code&gt; for DTO → Entity (data layer concern), and &lt;code&gt;PostEntityMapper.kt&lt;/code&gt; for Entity → Domain (connecting the data layer to the domain). Each file has one job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline-First Sync
&lt;/h2&gt;

&lt;p&gt;The WordPress REST API has a &lt;code&gt;modified_after&lt;/code&gt; parameter that returns only posts modified since a given timestamp. Combined with SQLDelight's ability to query &lt;code&gt;MAX(lastModified)&lt;/code&gt;, incremental sync becomes straightforward:&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="k"&gt;suspend&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;syncPosts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Dispatchers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)&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;latestModified&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLatestModifiedDate&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;executeAsOneOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nc"&gt;MAX&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;latestDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLatestDate&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;executeAsOneOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nc"&gt;MAX&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;latestModified&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;span class="n"&gt;latestDate&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;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Empty DB — initial download&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;posts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;perPage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;savePosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Incremental — only what changed since last sync&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;newPosts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latestDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;modifiedAfter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;latestModified&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;newPosts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nf"&gt;savePosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newPosts&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;First launch: 50 posts. Every subsequent launch: only what changed. Fast, bandwidth-efficient, and the UI always has something to show even offline.&lt;/p&gt;

&lt;p&gt;One gotcha: WordPress's &lt;code&gt;modified_after&lt;/code&gt; parameter is not widely documented and the naming is slightly inconsistent across WordPress versions. Test it against your actual endpoint before trusting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  MVI for Screen State
&lt;/h2&gt;

&lt;p&gt;I've always used MVVM. It's what the Android documentation pushes, it's what most tutorials use, and honestly it works fine for most apps. But MVI has been getting a lot of attention lately — and for good reason. The unidirectional data flow, the explicit state modeling, the clear separation between what the user &lt;em&gt;does&lt;/em&gt; and what the UI &lt;em&gt;shows&lt;/em&gt; — it all clicks when you see it in action.&lt;/p&gt;

&lt;p&gt;This project felt like the right place to try it properly. Not because MVVM would have failed here, but because I wanted to understand MVI from the inside, not just from blog posts.&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.amazonaws.com%2Fuploads%2Farticles%2Fct1xpd4p7efaigue5r61.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.amazonaws.com%2Fuploads%2Farticles%2Fct1xpd4p7efaigue5r61.png" alt="MVI Diagram" width="727" height="830"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern maps cleanly to KMP ViewModels.&lt;/p&gt;

&lt;p&gt;Each screen has three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Intent&lt;/strong&gt; — what the user can do:&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="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;LoadPost&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;postId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;ToggleSaved&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;postId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&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;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;OpenGallery&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;imageUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;initialIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;State&lt;/strong&gt; — what the UI renders:&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="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PostDetailState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="kd"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Loading&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetailState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Success&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;postDetail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetail&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;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;imageUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
        &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;isSaved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&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;relatedPosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostSummary&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyList&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="nc"&gt;PostDetailState&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Error&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetailState&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Effect&lt;/strong&gt; — one-time events that don't belong in state:&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="k"&gt;sealed&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PostDetailEffect&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;ShowError&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetailEffect&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ViewModel exposes a &lt;code&gt;StateFlow&amp;lt;PostDetailState&amp;gt;&lt;/code&gt; for state and a &lt;code&gt;Channel&amp;lt;PostDetailEffect&amp;gt;&lt;/code&gt; for effects. Why &lt;code&gt;Channel&lt;/code&gt; and not &lt;code&gt;SharedFlow&lt;/code&gt; for effects? Because &lt;code&gt;SharedFlow&lt;/code&gt; can drop events if the collector isn't active at the moment of emission. &lt;code&gt;Channel&lt;/code&gt; buffers them. For a snackbar that should appear exactly once, that matters.&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;PostDetailViewModel&lt;/span&gt;&lt;span class="p"&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;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostRepository&lt;/span&gt;&lt;span class="p"&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;htmlParser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HtmlParser&lt;/span&gt;&lt;span class="p"&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;navigator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;AppNavigator&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ViewModel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&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;_state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MutableStateFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostDetailState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;PostDetailState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Loading&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;state&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;PostDetailState&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asStateFlow&lt;/span&gt;&lt;span class="p"&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;_effects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostDetailEffect&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BUFFERED&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;effects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receiveAsFlow&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;onIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LoadPost&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;loadPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ToggleSaved&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;toggleSaved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;PostDetailIntent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OpenGallery&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigateToGallery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imageUrls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initialIndex&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;One Kotlin-specific gotcha worth mentioning: &lt;code&gt;state&lt;/code&gt; comes from &lt;code&gt;collectAsState()&lt;/code&gt; in Compose, which returns a delegated property. Smart casts don't work on delegated properties. This means you can't write &lt;code&gt;if (state is PostDetailState.Success) { state.postDetail }&lt;/code&gt; — the compiler won't smart-cast &lt;code&gt;state&lt;/code&gt; inside the branch. The fix is a local variable:&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;val&lt;/span&gt; &lt;span class="py"&gt;currentState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;  &lt;span class="c1"&gt;// local variable — smart cast works here&lt;/span&gt;
&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;PostDetailState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Success&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;postDetail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A small thing that can cost 20 minutes of confusion if you've never hit it before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigation as a Singleton
&lt;/h2&gt;

&lt;p&gt;One non-obvious decision: &lt;code&gt;AppNavigator&lt;/code&gt; is a &lt;strong&gt;singleton in Koin&lt;/strong&gt;. Not a scoped dependency, not passed through the composable tree — a single instance that any ViewModel or Composable can inject.&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;AppNavigator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;backStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NavKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;

    &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;MutableList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NavKey&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;backStack&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;backStack&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;navigateToPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;backStack&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PostDetail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;))&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;navigateToGallery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;initialIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&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;json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageUrls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;backStack&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AppRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ImageGallery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initialIndex&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;This means a component like &lt;code&gt;InternalPostContentPart&lt;/code&gt; — which renders a "Read also" card inside a post's content — can navigate to another post without needing to bubble a callback up through five layers of composables. It just injects the navigator directly:&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="nd"&gt;@Composable&lt;/span&gt;
&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;InternalPostContentPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ContentPart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InternalPost&lt;/span&gt;&lt;span class="p"&gt;)&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;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;koinInject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PostRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;navigator&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;koinInject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AppNavigator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onClick&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;postId&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;let&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigateToPost&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="p"&gt;{&lt;/span&gt; &lt;span class="o"&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;Whether this is "clean" is debatable. But it's pragmatic, and in a KMP project where threading the callback through shared composables gets complicated, the singleton approach saves real complexity.&lt;/p&gt;




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

&lt;p&gt;In Part 3 we get into the most technically interesting part of the project: the custom HTML parser that turns raw WordPress content into typed &lt;code&gt;ContentPart&lt;/code&gt; objects that Compose can render. WordPress posts contain a mix of paragraphs, headings, YouTube embeds, Spotify iframes, Instagram blockquotes, image galleries, and internal post references — none of it consistent, all of it needing different UI treatment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack: Kotlin 2.3.10 · Compose Multiplatform 1.10.2 · Ktor 3.4.1 · SQLDelight 2.3.1 · Koin 4.1.1&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>career</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>I Came Back to Kotlin for KMP — Here’s What Broke First</title>
      <dc:creator>Raul Arroyo</dc:creator>
      <pubDate>Thu, 19 Mar 2026 05:53:31 +0000</pubDate>
      <link>https://dev.to/rarroyo00/i-came-back-to-kotlin-for-kmp-heres-what-broke-first-hfn</link>
      <guid>https://dev.to/rarroyo00/i-came-back-to-kotlin-for-kmp-heres-what-broke-first-hfn</guid>
      <description>&lt;h1&gt;
  
  
  Part 1 — Why KMP, Why Now, and the First Walls
&lt;/h1&gt;

&lt;p&gt;I've been building mobile apps since 2011. Java with Eclipse, back when ADT was the only game in town and a simple ListView took half a day to get right. Then Kotlin came along and everything got better. Then Flutter came along and I spent the last three years living in Dart land — and honestly, it was great. But something kept pulling me back.&lt;/p&gt;

&lt;p&gt;Late last week I decided to come back to Kotlin. Not for a side project or a tutorial — for something real. I collaborate with a music magazine that had just updated their WordPress backend and refreshed their theme. A working REST API, fresh content, a product I actually care about. It was the perfect excuse to dive into Kotlin Multiplatform.&lt;/p&gt;

&lt;p&gt;I'd been watching KMP from a distance for years. It always seemed &lt;em&gt;almost&lt;/em&gt; ready. Then in late 2023 it went stable, the tooling caught up, and the library ecosystem started filling in. The timing finally felt right.&lt;/p&gt;

&lt;p&gt;This is the first post in a series documenting what I'm building and everything I'm learning along the way — the good decisions, the bad ones, and the things that just flat out broke.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pitch
&lt;/h2&gt;

&lt;p&gt;The project is a reader app backed by a WordPress REST API — posts, authors, categories, tags, embeds. Nothing exotic on the backend side. The interesting part is entirely in the client.&lt;/p&gt;

&lt;p&gt;The bet I'm making: &lt;strong&gt;Compose Multiplatform for the entire UI&lt;/strong&gt;, not just shared business logic. One codebase for every screen on Android and iOS. That's a significant commitment compared to the typical "shared ViewModel, native UI" approach — but coming from Flutter, where you do exactly this, it feels natural. And for someone working alone, eliminating the context switch between two UI stacks is worth a lot.&lt;/p&gt;

&lt;p&gt;Here's the full stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[versions]&lt;/span&gt;
&lt;span class="py"&gt;kotlin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2.3.10"&lt;/span&gt;
&lt;span class="py"&gt;compose-multiplatform&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.10.2"&lt;/span&gt;
&lt;span class="py"&gt;ktor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.4.1"&lt;/span&gt;
&lt;span class="py"&gt;sqldelight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"2.3.1"&lt;/span&gt;
&lt;span class="py"&gt;koin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"4.1.1"&lt;/span&gt;
&lt;span class="py"&gt;coil&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.4.0"&lt;/span&gt;
&lt;span class="py"&gt;navigation3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.0.0-alpha06"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these deserves its own explanation, because nothing here was obvious.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Room?
&lt;/h2&gt;

&lt;p&gt;The first real decision was local storage. Coming from Android, Room is the obvious choice — I've used it for years. But Room's KMP support is still marked experimental, and on iOS in particular it can be fragile.&lt;/p&gt;

&lt;p&gt;I went with &lt;strong&gt;SQLDelight&lt;/strong&gt; instead. It's been KMP-native from the start. You write plain SQL, it generates fully type-safe Kotlin query functions and a data class for each table. Any mismatch between your schema and your code is a compile error — not a runtime crash at 2am.&lt;/p&gt;

&lt;p&gt;The tradeoff is that you lose the annotation-based convenience of Room. But the result is surprisingly clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;postEntity&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;excerpt&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;imageUrl&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;categoryName&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;categoryId&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;isSaved&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kotlin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lastModified&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;selectPostById&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;postEntity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;selectPostBySlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;postEntity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQLDelight generates a &lt;code&gt;PostEntity&lt;/code&gt; data class and a &lt;code&gt;selectPostById(id: Long): Query&amp;lt;PostEntity&amp;gt;&lt;/code&gt; function from this. You call it like normal Kotlin. Adding a column means updating the SQL — and the compiler tells you everywhere that needs to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Navigation3 — Alpha Is Alpha
&lt;/h2&gt;

&lt;p&gt;For navigation I went with &lt;strong&gt;Navigation3&lt;/strong&gt;, Jetbrains' new navigation library for Compose Multiplatform. At &lt;code&gt;1.0.0-alpha06&lt;/code&gt; it's still rough around the edges.&lt;/p&gt;

&lt;p&gt;The most painful bug I hit early: double-tapping the back button crashes the app. The backstack empties completely and Navigation3 throws trying to display nothing. The fix ended up being a simple guard:&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;val&lt;/span&gt; &lt;span class="py"&gt;isNavigating&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;remember&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;mutableStateOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&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;safeBack&lt;/span&gt;&lt;span class="p"&gt;()&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;isNavigating&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;backStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;isNavigating&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
        &lt;span class="n"&gt;backStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeLastOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;isNavigating&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&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;Not elegant. But it works, and until the library stabilizes it's the pragmatic solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coil, Koin, and the Details
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coil 3&lt;/strong&gt; introduced &lt;code&gt;LocalPlatformContext&lt;/code&gt; for KMP — replacing Android's &lt;code&gt;LocalContext&lt;/code&gt;. If you copy-paste Coil 2 code into a KMP project, images just don't load on iOS with no clear error. The fix is one line, but finding it costs time:&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;// This is how you do it in KMP — not LocalContext.current&lt;/span&gt;
&lt;span class="nc"&gt;ImageRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LocalPlatformContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crossfade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Koin 4&lt;/strong&gt; initializes differently per platform. On Android you call &lt;code&gt;startKoin&lt;/code&gt; in &lt;code&gt;Application.onCreate()&lt;/code&gt;. On iOS you expose an &lt;code&gt;initKoin()&lt;/code&gt; function and call it from Swift before the first Compose frame. The gotcha that bit me: &lt;code&gt;KoinApplicationAlreadyStartedException&lt;/code&gt;. If Koin gets initialized twice — easy to do when juggling Android Application classes and iOS entry points — the app crashes immediately on launch with a confusing error message.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;libs.versions.toml&lt;/code&gt; Tax
&lt;/h2&gt;

&lt;p&gt;This one isn't unique to KMP but it cost me real time. The &lt;code&gt;libs.versions.toml&lt;/code&gt; file has a specific structure — &lt;code&gt;[versions]&lt;/code&gt;, &lt;code&gt;[libraries]&lt;/code&gt;, &lt;code&gt;[plugins]&lt;/code&gt; — and Gradle's configuration cache is unforgiving about mistakes. I had library declarations mixed into the &lt;code&gt;[versions]&lt;/code&gt; block and the error message pointed to something completely unrelated. The fix was formatting. The lesson was to read the TOML spec carefully upfront.&lt;/p&gt;




&lt;h2&gt;
  
  
  Coming from Flutter
&lt;/h2&gt;

&lt;p&gt;One thing I didn't expect: the mental model shift from Flutter to Compose Multiplatform is smaller than I thought. Both are declarative UI frameworks with a component tree and reactive state. The Dart-to-Kotlin switch is almost a relief — I'm back in a language I know deeply, with IDE support that actually works.&lt;/p&gt;

&lt;p&gt;What's different is the platform integration story. Flutter abstracts away the platform almost completely. KMP lets you go as native as you want, which is both powerful and occasionally annoying. Platform channels become &lt;code&gt;expect/actual&lt;/code&gt; declarations. The splash screen, share functionality, and back gesture all need platform-specific handling that Flutter would paper over for you.&lt;/p&gt;

&lt;p&gt;Whether that's a feature or a bug depends on what you're building. For an app that needs to feel native on both platforms, it's a feature.&lt;/p&gt;




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

&lt;p&gt;In Part 2 we'll get into the architecture — why we split posts into two domain models (&lt;code&gt;PostSummary&lt;/code&gt; and &lt;code&gt;PostDetail&lt;/code&gt;), how the data flows from the WordPress API through SQLDelight to the UI, and how MVI fits into a KMP ViewModel.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Stack: Kotlin 2.3.10 · Compose Multiplatform 1.10.2 · Ktor 3.4.1 · SQLDelight 2.3.1 · Koin 4.1.1 · Coil 3.4.0 · Navigation3 1.0.0-alpha06&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Thoughts?
&lt;/h2&gt;

&lt;p&gt;If you're exploring Kotlin Multiplatform right now, I'm curious — what’s been your biggest friction point so far?&lt;/p&gt;

&lt;p&gt;Have you tried going full Compose Multiplatform for UI, or are you sticking to shared logic only?&lt;/p&gt;

&lt;p&gt;Drop a comment, I'd love to compare notes (and maybe avoid a few future headaches).&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>career</category>
      <category>kotlin</category>
    </item>
    <item>
      <title>Don't Lose the Spark: Keeping Your Coding Passion Alive in a Leadership Role</title>
      <dc:creator>Raul Arroyo</dc:creator>
      <pubDate>Fri, 05 Apr 2024 06:15:20 +0000</pubDate>
      <link>https://dev.to/rarroyo00/dont-lose-the-spark-keeping-your-coding-passion-alive-in-a-leadership-role-1kab</link>
      <guid>https://dev.to/rarroyo00/dont-lose-the-spark-keeping-your-coding-passion-alive-in-a-leadership-role-1kab</guid>
      <description>&lt;p&gt;Remember the thrill of seeing your first line of code come to life? That feeling of creation and problem-solving is what hooked me on software development for over 10 years. During all this time, I have had the opportunity to work on a huge ammount of projects and bussinness, for both small start-ups and big companies in various industries and using different technologies. Along the path, I have had the pleasure of meeting amazing people, learning a lot, and tackling obstacles that have contributed to my personal and professional path. &lt;/p&gt;




&lt;h2&gt;
  
  
  Enthusiasm and discovery.
&lt;/h2&gt;

&lt;p&gt;My first projects involved building videogames from scratch, interact with motion sensors, create lots of eyecandy pages. Each line of code I wrote unlocked new possibilities, and the excitement of seeing it all come together was incredible. Every project was an escapade to learn new things as well as discover unexplored technology. It was exhilarating for me to be part of competent teams working alongside other developers towards making products for people's pleasure or use. &lt;/p&gt;

&lt;p&gt;I remember how exciting it felt when what I built started being useful and users were happy about it. It is so satisfying realizing that my code actually matters to individuals' lives. This makes your work feel like second nature since you are actively participating in something bigger. &lt;/p&gt;

&lt;p&gt;However, as time went by routine set in. Projects became more monotonous while tasks became repetitive. The learning curve flattened out and the initial enthusiasm wilted away. The sense of fulfilment I previously experienced upon finishing a project waned eventually giving way to emptiness and desolation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The aspiration of the eternal developer.
&lt;/h2&gt;

&lt;p&gt;From the very start, my vision was clear: to embody the essence of an eternal developer. To exist as a consistent entity, engrossed in a continual stream of code, accomplishing tasks in succession, without pause. &lt;/p&gt;

&lt;p&gt;I envisioned myself laboring ceaselessly, mastering the complexities of code and resolving issues with precise accuracy. My objective was to evolve into a development powerhouse, adept at transforming any concept into a tangible manifestation. Nevertheless, the harsh reality soon became apparent. &lt;/p&gt;




&lt;h2&gt;
  
  
  A new challenge arises: managing groups of developers.
&lt;/h2&gt;

&lt;p&gt;As my skills grew, so did my desire to contribute in a different way. Leading development teams became the next frontier for me, but it wasn't without its hurdles.&lt;/p&gt;

&lt;p&gt;This transition, though exciting, can be a minefield for those who are unprepared, like me. Many talented programmers struggle initially in this new role. The main reason is that technical skills, while essential, are not enough to lead a team. A completely new set of habilities is required, a shift in focus that many are not ready to make. &lt;/p&gt;

&lt;p&gt;What makes leading development teams so challenging?&lt;/p&gt;

&lt;p&gt;Motivating a team with diverse personalities was initially tough. I learned to tailor my communication style and celebrate individual strengths&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Managing different personalities&lt;/strong&gt; is crucial for effective leadership. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understanding each developer's unique work style&lt;/strong&gt;, motivations, and preferences is essential for bringing out the best in each team member. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear and effective communication&lt;/strong&gt; is vital for the success of any team. A leader must be able to clearly communicate goals, inspire the team, and address conflicts efficiently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delegating responsibilities is a key aspect of leadership&lt;/strong&gt;. A leader cannot handle everything on their own and must learn to delegate tasks effectively, while trusting their team to complete them. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Motivating and inspiring the team&lt;/strong&gt;, especially during challenging times, is a crucial skill for a leader. They must be able to keep their team motivated and engaged to achieve success.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Becoming the leader of development teams is a major accomplishment. Yet, this promotion comes with a harsh truth: &lt;strong&gt;you start to drift away from hands-on coding&lt;/strong&gt;. The passion you once had for coding begins to fade, causing feelings of disappointment and a lack of drive.&lt;/p&gt;




&lt;h2&gt;
  
  
  How do you keep your love for software development alive
&lt;/h2&gt;

&lt;p&gt;The pull of hands-on coding can be strong, even when you're leading a development team. Here are some strategies I used to keep myself connected to the code and the joy of creation:&lt;/p&gt;

&lt;h4&gt;
  
  
  Discover fresh methods to engage with coding
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Engage in open source initiatives&lt;/strong&gt;: Allocate time to contribute to projects that pique your interest. This will keep you aware of the newest technologies and techniques, and provide the chance to collaborate with other skilled developers. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guide fellow developers&lt;/strong&gt;: Share your expertise with novice programmers. This serves as a fantastic method to maintain your connection to coding and assist others in advancing their careers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay on top with current trends&lt;/strong&gt;: Stay informed by reading software development blogs, articles, and books. Attend industry conferences and events. Keeping up with the latest technologies will keep you motivated and on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Carve Out "Scratch Pad" Time&lt;/strong&gt;:  Scheduling dedicated time for personal coding projects, even if it was just a few hours a week, proved to be crucial.  These projects allowed me to experiment with new libraries, frameworks, and coding paradigms, keeping my coding skills sharp and reigniting the spark of creative problem-solving.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Understand the importance of your role
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remember your role as a leader&lt;/strong&gt;: it plays a crucial part in the team's success. Your choices and leadership style have a significant influence on the quality of work produced, the team's efficiency, and the satisfaction of customers. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celebrate the team's achievements&lt;/strong&gt;: When the team accomplishes its goals, take the time to acknowledge and celebrate the effort put in by each individual. This not only boosts morale but also keeps the team driven and engaged. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep your focus on the overarching goal&lt;/strong&gt;: Don't lose sight of the primary objective in software development: creating products that are not only functional but also improve the lives of those who use them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Achieving a balance:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One must strike a harmonious balance&lt;/strong&gt; between leadership duties and coding enthusiasm. Seek chances to remain engaged in software development, even if it entails a reduced level of involvement. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not hesitate to assign tasks to others&lt;/strong&gt;. You are not obligated to handle everything independently. Have confidence in your team and allot responsibilities to individuals, trust that they have the necessary skills and expertise. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prioritize your mental and physical well-being&lt;/strong&gt;. Stress and fatigue often afflict team leaders. Ensure you take periodic breaks, engage in physical activity and get enough rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't let your love for software development fade as you transition into a leadership role. There are plenty of ways to stay involved with coding, even if you're not actively writing lines of code every single day. As a leader, your actions have a big influence on the team's achievements. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Aim to be the kind of leader you've always admired&lt;br&gt;
All the while balancing your leadership duties and your coding passion.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Becoming a software developer is not a straightforward process. It involves exciting findings, the joy of building something, and the expected hurdles of progress. As you progress in your career, keep in mind that the same initial excitement that got you into coding is still present. &lt;/p&gt;

&lt;p&gt;Being a leader in software development allows you to use your technical skills to support and motivate others. It's all about creating a cooperative space where imagination flourishes, and where your direction helps a team accomplish extraordinary feats.&lt;/p&gt;

&lt;p&gt;Keep the passion for coding alive! Being a leader is very fulfilling. Balancing between management and creativity is a valuable journey. Embrace new challenges, fuel your coding spirit with the mentioned strategies, and most importantly, lead with the same excitement for innovation that initially attracted you to coding. &lt;/p&gt;

&lt;p&gt;The world of software development is always changing, and your leadership will influence the future. As you motivate others, don't forget to inspire yourself as well. The journey never stops!&lt;/p&gt;

</description>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
      <category>offtopic</category>
    </item>
  </channel>
</rss>
