<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Soft Art Development]]></title><description><![CDATA[Soft Art Development]]></description><link>https://blog.softartdev.org</link><generator>RSS for Node</generator><lastBuildDate>Fri, 24 Apr 2026 22:52:30 GMT</lastBuildDate><atom:link href="https://blog.softartdev.org/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The “Stale Capture” Trap in Jetpack Compose Side Effects (and why it can surprise Kotlin developers)]]></title><description><![CDATA[Sometimes your UI shows the correct value, but a log (or callback) inside LaunchedEffect uses an old one.This is usually not a Compose “bug”. It is a closure capture issue: a long-running lambda can keep old references.
This article explains the prob...]]></description><link>https://blog.softartdev.org/the-stale-capture-trap-in-jetpack-compose-side-effects-and-why-it-can-surprise-kotlin-developers</link><guid isPermaLink="true">https://blog.softartdev.org/the-stale-capture-trap-in-jetpack-compose-side-effects-and-why-it-can-surprise-kotlin-developers</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[Jetpack Compose]]></category><dc:creator><![CDATA[SoftArtDev]]></dc:creator><pubDate>Mon, 26 Jan 2026 02:29:03 GMT</pubDate><content:encoded><![CDATA[<p>Sometimes your UI shows the correct value, but a log (or callback) inside <code>LaunchedEffect</code> uses an old one.<br />This is usually not a Compose “bug”. It is a <strong>closure capture</strong> issue: a long-running lambda can keep <strong>old references</strong>.</p>
<p>This article explains the problem with a simple example and shows two safe fixes.</p>
<hr />
<h2 id="heading-what-stale-capture-means">What “stale capture” means</h2>
<p>In Kotlin, a lambda can capture variables from the outer scope.<br />But it does not capture “magic live variables”. It stores <strong>references</strong> to objects.</p>
<p>So if you capture an object, and later a new object is created, the old lambda will still point to the old object.</p>
<p>This is normal Kotlin behavior.</p>
<hr />
<h2 id="heading-a-simple-compose-example">A simple Compose example</h2>
<p>Imagine a screen where:</p>
<ul>
<li>the ViewModel sometimes provides <code>text</code> (for example, after loading from a database)</li>
<li>the user edits a text field</li>
<li>after a short delay, we run validation and call <code>onValidate(text)</code></li>
</ul>
<h3 id="heading-code-buggy">Code (buggy)</h3>
<pre><code class="lang-kotlin"><span class="hljs-meta">@Composable</span>
<span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">DemoScreen</span><span class="hljs-params">(
    text: <span class="hljs-type">String</span>,        <span class="hljs-comment">// changes over time (ViewModel update)</span>
    onValidate: (<span class="hljs-type">String</span>) -&gt; <span class="hljs-type">Unit</span> <span class="hljs-comment">// can also change on recomposition</span>
)</span></span> {
    <span class="hljs-comment">// ⚠️ This creates a NEW state object when text changes.</span>
    <span class="hljs-keyword">val</span> textState = remember(text) {
        mutableStateOf(text)
    }

    <span class="hljs-comment">// We start ONE coroutine for the whole lifetime of this call site.</span>
    LaunchedEffect(<span class="hljs-built_in">Unit</span>) {
        <span class="hljs-comment">// Some long-running work (timer, polling, etc.)</span>
        delay(<span class="hljs-number">2_000</span>)

        <span class="hljs-comment">// ⚠️ This lambda can use an old reference to textState and/or old onValidate.</span>
        onValidate(textState.value)
    }

    TextField(
        value = textState.value,
        onValueChange = { textState.value = it }
    )
}
</code></pre>
<h3 id="heading-what-can-happen">What can happen</h3>
<p>1) First composition: <code>text = ""</code><br />   Compose creates <code>textState₁</code> (object A) and starts the <code>LaunchedEffect</code>.</p>
<p>2) Later: <code>text = "Hello"</code><br />   Because of <code>remember(text)</code>, Compose creates <strong>new</strong> <code>textState₂</code> (object B).<br />   The UI now reads object B.</p>
<p>3) After 2 seconds the coroutine finishes <code>delay(...)</code><br />   The coroutine may still hold object A, so it calls <code>onValidate("")</code> even though the UI shows <code>"Hello"</code>.</p>
<p>So UI and side effects can disagree, even if the UI is correct.</p>
<hr />
<h2 id="heading-why-this-happens-in-launchedeffect">Why this happens in <code>LaunchedEffect</code></h2>
<p><code>LaunchedEffect(key1, key2, ...)</code> starts a coroutine.<br />That coroutine keeps running <strong>until the keys change</strong>.</p>
<p>If your effect uses a value but that value is not in the keys, the coroutine may continue running with old references.</p>
<p>That is why Compose has two main patterns:</p>
<ul>
<li>restart the effect when inputs change (use keys)</li>
<li>keep the effect running, but read the <strong>latest</strong> values (<code>rememberUpdatedState</code>)</li>
</ul>
<hr />
<h2 id="heading-fix-1-restart-the-effect-add-the-right-keys">Fix 1: Restart the effect (add the right keys)</h2>
<p>If it is OK to restart the coroutine when input changes, add keys:</p>
<pre><code class="lang-kotlin">LaunchedEffect(text, onValidate) {
    delay(<span class="hljs-number">2_000</span>)
    onValidate(textState.value)
}
</code></pre>
<p>Now when <code>text</code> changes, the old coroutine is cancelled and a new one starts.</p>
<p><strong>Use this when restarting is cheap and safe.</strong></p>
<hr />
<h2 id="heading-fix-2-keep-the-effect-stable-but-read-the-latest-value">Fix 2: Keep the effect stable, but read the latest value</h2>
<p>If you do <em>not</em> want to restart the coroutine, use <code>rememberUpdatedState</code>.</p>
<p>See the official docs for <a target="_blank" href="https://developer.android.com/develop/ui/compose/side-effects#rememberupdatedstate"><code>rememberUpdatedState</code></a>.</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> latestText <span class="hljs-keyword">by</span> rememberUpdatedState(textState.value)
<span class="hljs-keyword">val</span> latestOnValidate <span class="hljs-keyword">by</span> rememberUpdatedState(onValidate)

LaunchedEffect(<span class="hljs-built_in">Unit</span>) {
    delay(<span class="hljs-number">2_000</span>)
    latestOnValidate(latestText)
}
</code></pre>
<p>Idea: <code>rememberUpdatedState</code> returns a stable holder, and Compose updates its value on recomposition.<br />So your long-running coroutine keeps the same reference, but it reads the latest value.</p>
<p><strong>Use this when you want one collector, one listener, one timer, etc.</strong></p>
<hr />
<h2 id="heading-how-to-see-the-problem-object-identity">How to “see” the problem (object identity)</h2>
<p>On JVM you can print identity to prove that the state object changed:</p>
<pre><code class="lang-kotlin">SideEffect {
    println(<span class="hljs-string">"textState identity = <span class="hljs-subst">${System.identityHashCode(textState)}</span>"</span>)
}
</code></pre>
<p>When <code>remember(text)</code> recreates the state, the identity will change.</p>
<hr />
<h2 id="heading-under-the-hood-optional">Under the hood (optional)</h2>
<p>Compose state is implemented in the runtime. If you want to see a real implementation file, check<br /><a target="_blank" href="https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt#L340"><code>SnapshotState.kt</code> in Compose runtime</a>.</p>
<p>You do not need to read it to fix the bug, but it helps to understand that “state” is a real object.</p>
<hr />
<h2 id="heading-practical-checklist">Practical checklist</h2>
<p>When you write a long-running side effect (<code>LaunchedEffect</code>, <code>DisposableEffect</code>, timers, listeners, etc.):</p>
<p>1) List what the effect reads (state objects, values, callbacks).
2) For each item, choose:</p>
<ul>
<li><strong>Restart the effect</strong> when it changes → put it in the effect keys</li>
<li><strong>Do not restart</strong>, but need the latest value → use <code>rememberUpdatedState</code></li>
</ul>
<p>This simple rule prevents most stale-capture bugs.</p>
<hr />
<h2 id="heading-references">References</h2>
<ul>
<li>Android Developers — side effects and <a target="_blank" href="https://developer.android.com/develop/ui/compose/side-effects#rememberupdatedstate"><code>rememberUpdatedState</code></a>  </li>
<li>Compose runtime source — <a target="_blank" href="https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt#L340"><code>SnapshotState.kt</code></a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Kotlin Coroutines on wasmJs: why there is no `runBlocking` (and why “hacks” are risky)]]></title><description><![CDATA[Cheat sheet

runBlocking is a thread-blocking bridge.
It exists only for targets that are built from the concurrent source set.
js/wasm* are built from jsAndWasm* source sets, so they don’t get runBlocking.
On JS (and wasmJs hosted by JS) you cannot ...]]></description><link>https://blog.softartdev.org/kotlin-coroutines-on-wasmjs-why-there-is-no-runblocking-and-why-hacks-are-risky</link><guid isPermaLink="true">https://blog.softartdev.org/kotlin-coroutines-on-wasmjs-why-there-is-no-runblocking-and-why-hacks-are-risky</guid><category><![CDATA[Kotlin]]></category><category><![CDATA[kotlin coroutines]]></category><category><![CDATA[wasm]]></category><category><![CDATA[js]]></category><category><![CDATA[Kotlin Multiplatform]]></category><dc:creator><![CDATA[SoftArtDev]]></dc:creator><pubDate>Sat, 03 Jan 2026 15:02:23 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-cheat-sheet">Cheat sheet</h2>
<ul>
<li><code>runBlocking</code> is a <strong>thread-blocking</strong> bridge.</li>
<li>It exists only for targets that are built from the <strong><code>concurrent</code></strong> source set.</li>
<li><code>js/wasm*</code> are built from <strong><code>jsAndWasm*</code></strong> source sets, so they <strong>don’t get</strong> <code>runBlocking</code>.</li>
<li>On JS (and wasmJs hosted by JS) you cannot block the single event loop and still let async callbacks run.</li>
<li>Background reading: <a target="_blank" href="https://kt.academy/article/cc-other-languages">Kt. Academy — Coroutines in other languages</a></li>
</ul>
<hr />
<h2 id="heading-1-the-build-layout-makes-it-explicit">1) The build layout makes it explicit</h2>
<p>In <code>kotlinx.coroutines</code>, the source set graph is documented directly in
<a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/build.gradle.kts"><code>kotlinx-coroutines-core/build.gradle.kts</code></a>:</p>
<pre><code class="lang-text">/* ==========================================================================
  Configure source sets structure for kotlinx-coroutines-core:

     TARGETS                            SOURCE SETS
     ------------------------------------------------------------
     wasmJs \------&gt; jsAndWasmJsShared ----+
     js     /                              |
                                           V
     wasmWasi --------------------&gt; jsAndWasmShared ----------+
                                                              |
                                                              V
     jvm ----------------------------&gt; concurrent -------&gt; common
                                        ^
     ios     \                          |
     macos   | ---&gt; nativeDarwin ---&gt; native ---+
     tvos    |                         ^
     watchos /                         |
                                       |
     linux  \  ---&gt; nativeOther -------+
     mingw  /
 ========================================================================== */
</code></pre>
<p><strong>Key point:</strong> Targets that support <code>runBlocking</code> share the <strong><code>concurrent</code></strong> source set (<code>jvm</code>, <code>native*</code>).
Targets like <code>js</code>, <code>wasmJs</code>, and <code>wasmWasi</code> share <strong><code>jsAndWasm*</code></strong>, where blocking builders are intentionally absent.</p>
<hr />
<h2 id="heading-2-where-runblocking-actually-lives">2) Where <code>runBlocking</code> actually lives</h2>
<p>In <a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt"><code>concurrent/src/Builders.concurrent.kt</code></a>
it is declared as an <code>expect</code> function:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> <span class="hljs-keyword">expect</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">runBlocking</span><span class="hljs-params">(
    context: <span class="hljs-type">CoroutineContext</span> = EmptyCoroutineContext,
    block: <span class="hljs-type">suspend</span> <span class="hljs-type">CoroutineScope</span>.() -&gt; <span class="hljs-type">T</span>
)</span></span>: T
</code></pre>
<p>Then each “blocking-capable” platform provides its own <code>actual</code> implementation.</p>
<h3 id="heading-native-runblocking-the-essential-mechanics">Native <code>runBlocking</code>: the essential mechanics</h3>
<p>See <a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/native/src/Builders.kt"><code>native/src/Builders.kt</code></a> (simplified):</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> eventLoop: EventLoop? = ...
<span class="hljs-keyword">val</span> newContext: CoroutineContext = ...
<span class="hljs-keyword">val</span> coroutine = BlockingCoroutine&lt;T&gt;(newContext, eventLoop)

coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
<span class="hljs-keyword">return</span> coroutine.joinBlocking()
</code></pre>
<p><strong>What matters:</strong></p>
<ol>
<li>It selects/creates an <strong>EventLoop</strong> for the blocked thread.</li>
<li>It starts the coroutine.</li>
<li>It <strong>blocks</strong> the thread in <code>joinBlocking()</code> until completion, while pumping the event loop manually.</li>
</ol>
<hr />
<h2 id="heading-3-minimum-about-eventloop-only-what-we-need-here">3) Minimum about EventLoop (only what we need here)</h2>
<p>Blocking builders depend on the ability to <strong>process queued work while the thread is blocked</strong>.</p>
<p>In <a target="_blank" href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/EventLoop.common.kt"><code>common/src/EventLoop.common.kt</code></a>:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">open</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">processNextEvent</span><span class="hljs-params">()</span></span>: <span class="hljs-built_in">Long</span> {
    <span class="hljs-keyword">if</span> (!processUnconfinedEvent()) <span class="hljs-keyword">return</span> <span class="hljs-built_in">Long</span>.MAX_VALUE
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>
}
</code></pre>
<p>So <code>runBlocking</code> is not just “wait”; it is “wait <strong>and</strong> keep progressing coroutine work”.</p>
<hr />
<h2 id="heading-4-why-blocking-is-still-possible-on-android-even-with-one-ui-thread">4) Why “blocking” is still possible on Android (even with one UI thread)</h2>
<p>Even if you think “my Android app is single-threaded”, it still runs on a real <strong>OS thread</strong>.
That thread can be <strong>parked / blocked</strong>, and other threads (timers, I/O, Binder, etc.) can still run.</p>
<p>Also, Android’s main thread is built around a <strong>message loop</strong> (an event loop). The core loop is literally an
infinite loop in <code>Looper.loop()</code>.</p>
<p>Sources:</p>
<ul>
<li><a target="_blank" href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Looper.java"><code>Looper.java</code> on Android Code Search</a></li>
<li><a target="_blank" href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/os/Handler.java"><code>Handler.java</code> on Android Code Search</a></li>
</ul>
<p>Key fragment from <code>Looper.loop()</code>:</p>
<pre><code class="lang-java"><span class="hljs-keyword">for</span> (;;) {
    <span class="hljs-keyword">if</span> (!loopOnce(me, ident, thresholdOverride)) {
        <span class="hljs-keyword">return</span>;
    }
}
</code></pre>
<p>On Android, “one thread” does <strong>not</strong> mean “no event loop”. It means: “one thread running an event loop”.</p>
<p><strong>Important:</strong> Calling <code>runBlocking</code> on the <strong>main/UI thread</strong> is still a bad idea.
It freezes the Looper, stops message processing, and can lead to ANR (Application Not Responding).</p>
<hr />
<h2 id="heading-5-why-this-cannot-exist-on-wasmjs">5) Why this cannot exist on wasmJs</h2>
<p>On wasmJs (and Kotlin/JS), coroutine resumptions often come from <strong>JS callbacks</strong>
(Promise microtasks, timers, I/O). These callbacks run only when control returns to the <strong>JS event loop</strong>.</p>
<p>If you block the main thread, you stop the loop, so the coroutine <strong>cannot resume</strong>.</p>
<p>Minimal JS deadlock example:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fakeRunBlocking</span>(<span class="hljs-params">promise</span>) </span>{
  <span class="hljs-keyword">let</span> done = <span class="hljs-literal">false</span>;
  promise.then(<span class="hljs-function">() =&gt;</span> done = <span class="hljs-literal">true</span>);

  <span class="hljs-keyword">while</span> (!done) {} <span class="hljs-comment">// blocks the event loop -&gt; then() never runs</span>
}
</code></pre>
<p>This is why JS frameworks “wait” by <strong>returning a Promise</strong>, not by blocking a thread.</p>
<hr />
<h2 id="heading-6-about-runblocking-for-all-targets-libraries">6) About “runBlocking for all targets” libraries</h2>
<p>There are small community libraries that try to provide <code>runBlocking</code> for JS/WASM.
Example: <a target="_blank" href="https://mvnrepository.com/artifact/com.javiersc.kotlinx/coroutines-run-blocking-all"><code>com.javiersc.kotlinx:coroutines-run-blocking-all</code></a>.</p>
<p>One JS/WASM implementation is found in
<a target="_blank" href="https://github.com/JavierSegoviaCordoba/run-blocking-kmp/blob/main/coroutines-run-blocking-all/js/main/kotlin/com/javiersc/kotlinx/coroutines/run/blocking/RunBlocking.kt"><code>RunBlocking.kt</code> in run-blocking-kmp</a> (simplified):</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">public</span> <span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">runBlocking</span><span class="hljs-params">(
    context: <span class="hljs-type">CoroutineContext</span>,
    block: <span class="hljs-type">suspend</span> <span class="hljs-type">CoroutineScope</span>.() -&gt; <span class="hljs-type">T</span>,
)</span></span>: T = GlobalScope.promise(context) { block() } <span class="hljs-keyword">as</span> T
</code></pre>
<h3 id="heading-why-this-is-dangerous">Why this is dangerous</h3>
<ol>
<li><strong>It lies about the return type:</strong> <code>Promise&lt;T&gt;</code> is cast to <code>T</code>. This breaks type safety.</li>
<li><strong>It returns immediately:</strong> It does not actually wait.</li>
<li><strong>It uses <code>GlobalScope</code>:</strong> This breaks <em>structured concurrency</em> (harder cancellation, easier leaks).</li>
</ol>
<hr />
<h2 id="heading-7-your-runblockingall-better-idea-but-still-not-a-solution">7) Your <code>runBlockingAll</code>: better idea, but still not a solution</h2>
<p>A simplified version of a common attempt to "fix" this:</p>
<pre><code class="lang-kotlin"><span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-type">&lt;T&gt;</span> <span class="hljs-title">runBlockingAll</span><span class="hljs-params">(
    context: <span class="hljs-type">CoroutineContext</span>,
    block: <span class="hljs-type">suspend</span> <span class="hljs-type">CoroutineScope</span>.() -&gt; <span class="hljs-type">T</span>
)</span></span>: T {
    <span class="hljs-keyword">var</span> <span class="hljs-keyword">out</span>: Result&lt;T&gt;? = <span class="hljs-literal">null</span>
    <span class="hljs-keyword">val</span> continuation: Continuation&lt;T&gt; = Continuation(context) { <span class="hljs-keyword">out</span> = it }
    block.startCoroutine(receiver = GlobalScope, completion = continuation)
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">out</span>!!.getOrThrow()
}
</code></pre>
<h3 id="heading-why-it-is-a-bit-better">Why it is a bit better</h3>
<ul>
<li>It does <strong>not</strong> return a Promise disguised as <code>T</code>.</li>
<li>If <code>block</code> completes <strong>immediately</strong> (no suspension), it really returns <code>T</code>.</li>
</ul>
<h3 id="heading-why-it-is-not-a-solution">Why it is not a solution</h3>
<ul>
<li>If the coroutine <strong>suspends</strong>, <code>out</code> is still <code>null</code>, so it crashes on <code>out!!</code>.</li>
<li>It still uses <code>GlobalScope</code>.</li>
<li>You still can’t “block until resume” on JS/WASM without freezing the event loop.</li>
</ul>
<p><strong>The verdict:</strong></p>
<blockquote>
<p>It can “unwrap” only <em>non-suspending</em> coroutines.<br />It cannot turn async work into sync work on JS/WASM.</p>
</blockquote>
<hr />
<h2 id="heading-8-conclusion-dont-overuse-runblocking-even-where-it-exists">8) Conclusion: don’t overuse <code>runBlocking</code> (even where it exists)</h2>
<p>Even on platforms where <code>runBlocking</code> exists (JVM/Android/Native), it should be used <strong>rarely</strong>.</p>
<ul>
<li>It <strong>blocks a thread</strong>.</li>
<li>On Android, using it on the <strong>main/UI thread</strong> can freeze the app (no Looper messages → ANR risk).</li>
<li>It makes code harder to maintain and easier to deadlock.</li>
</ul>
<p>In app code, prefer launching coroutines from a proper scope:</p>
<ul>
<li><strong>Android UI layer:</strong> use lifecycle scopes like <code>viewModelScope</code>.</li>
<li><strong>Lower layers / non-Android:</strong> create your own scope and control its lifetime. For example:</li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> scope = CoroutineScope(
    SupervisorJob() + Dispatchers.Default + coroutineExceptionHandler
)
</code></pre>
<p>The best long-term strategy is to <strong>avoid <code>runBlocking</code> in your business logic</strong>.
If you later port your app/library to more platforms (especially web / wasmJs), you will have much less work,
because you won’t need to migrate blocking calls to <code>suspend</code> and async flows.</p>
]]></content:encoded></item><item><title><![CDATA[Under the Hood: How Compose Multiplatform Opens a URL]]></title><description><![CDATA[As mobile developers moving into the Multiplatform space, we often take high-level APIs for granted. A perfect example is opening a hyperlink. In Jetpack Compose, we simply grab the LocalUriHandler and call openUri(). But what happens after that call...]]></description><link>https://blog.softartdev.org/under-the-hood-how-compose-multiplatform-opens-a-url</link><guid isPermaLink="true">https://blog.softartdev.org/under-the-hood-how-compose-multiplatform-opens-a-url</guid><category><![CDATA[compose multiplatform]]></category><category><![CDATA[Kotlin Multiplatform]]></category><dc:creator><![CDATA[SoftArtDev]]></dc:creator><pubDate>Tue, 23 Dec 2025 09:47:30 GMT</pubDate><content:encoded><![CDATA[<p>As mobile developers moving into the Multiplatform space, we often take high-level APIs for granted. A perfect example is opening a hyperlink. In Jetpack Compose, we simply grab the <code>LocalUriHandler</code> and call <code>openUri()</code>. But what happens after that call? How does a single line of Kotlin code trigger the default browser on Android, iOS, Windows, macOS, and Linux?</p>
<p>Today, we are diving deep into the call stack—from the Compose API down to the C++ JNI calls in the JDK.</p>
<p>For example, <code>UriHandler</code> is used like this (Kotlin Multiplatform RSS Reader sample):</p>
<ul>
<li><a target="_blank" href="https://github.com/Kotlin/kmp-production-sample/blob/master/shared/src/commonMain/kotlin/com/github/jetbrains/rssreader/ui/Screen.kt#L81">https://github.com/Kotlin/kmp-production-sample/blob/master/shared/src/commonMain/kotlin/com/github/jetbrains/rssreader/ui/Screen.kt#L81</a></li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-keyword">val</span> uriHandler = LocalUriHandler.current
<span class="hljs-comment">// ... some code omitted</span>
uriHandler.openUri(url)
</code></pre>
<p>This allows opening a link in an external browser on Android, iOS, Desktop (Java/AWT) and Web (wasmJs).</p>
<h2 id="heading-compose-api">Compose API</h2>
<p><code>UriHandler</code> interface (AndroidX source code):</p>
<ul>
<li><p>AOSP: <a target="_blank" href="https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/UriHandler.kt">https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/UriHandler.kt</a></p>
</li>
<li><p>JetBrains fork: <a target="_blank" href="https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/UriHandler.kt">https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/UriHandler.kt</a></p>
</li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">UriHandler</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">openUri</span><span class="hljs-params">(uri: <span class="hljs-type">String</span>)</span></span>
}
</code></pre>
<p>Android implementation (<code>AndroidUriHandler</code>):</p>
<ul>
<li><p>AOSP: <a target="_blank" href="https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUriHandler.android.kt">https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUriHandler.android.kt</a></p>
</li>
<li><p>JetBrains fork: <a target="_blank" href="https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUriHandler.android.kt">https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUriHandler.android.kt</a></p>
</li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AndroidUriHandler</span></span>(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> context: Context) : UriHandler {
    <span class="hljs-keyword">override</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">openUri</span><span class="hljs-params">(uri: <span class="hljs-type">String</span>)</span></span> {
        <span class="hljs-keyword">try</span> {
            context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uri)))
        } <span class="hljs-keyword">catch</span> (e: ActivityNotFoundException) {
            <span class="hljs-keyword">throw</span> IllegalArgumentException(<span class="hljs-string">"Can't open <span class="hljs-variable">$uri</span>."</span>, e)
        }
    }
}
</code></pre>
<h2 id="heading-desktop-ios-web-skiko-based">Desktop / iOS / Web (Skiko-based)</h2>
<p>Next, Compose delegates to a Skiko-based implementation (<code>PlatformUriHandler</code>):</p>
<ul>
<li><a target="_blank" href="https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformUriHandler.skiko.kt">https://github.com/JetBrains/compose-multiplatform-core/blob/jb-main/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformUriHandler.skiko.kt</a></li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-keyword">internal</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PlatformUriHandler</span> : <span class="hljs-type">UriHandler</span>, <span class="hljs-type">URIManager</span></span>()
</code></pre>
<p><code>URIManager</code> and <code>URIHandler_openUri</code> are defined in Skiko:</p>
<ul>
<li><a target="_blank" href="https://github.com/JetBrains/skiko/blob/master/skiko/src/commonMain/kotlin/org/jetbrains/skiko/Platform.kt">https://github.com/JetBrains/skiko/blob/master/skiko/src/commonMain/kotlin/org/jetbrains/skiko/Platform.kt</a></li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-keyword">open</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">URIManager</span> </span>{
    <span class="hljs-keyword">open</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">openUri</span><span class="hljs-params">(uri: <span class="hljs-type">String</span>)</span></span> = URIHandler_openUri(uri)
}
<span class="hljs-keyword">internal</span> <span class="hljs-keyword">expect</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">URIHandler_openUri</span><span class="hljs-params">(uri: <span class="hljs-type">String</span>)</span></span>
</code></pre>
<h3 id="heading-skiko-platform-actuals">Skiko platform actuals</h3>
<p><strong>Desktop (AWT / Java):</strong></p>
<ul>
<li><a target="_blank" href="https://github.com/JetBrains/skiko/blob/master/skiko/src/awtMain/kotlin/org/jetbrains/skiko/Actuals.awt.kt">https://github.com/JetBrains/skiko/blob/master/skiko/src/awtMain/kotlin/org/jetbrains/skiko/Actuals.awt.kt</a></li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-comment">// excerpt</span>
<span class="hljs-keyword">val</span> desktop = Desktop.getDesktop()
<span class="hljs-keyword">if</span> (desktop.isSupported(Desktop.Action.BROWSE)) {
    desktop.browse(URI(uri))
    <span class="hljs-keyword">return</span>
}
<span class="hljs-comment">// Linux fallback: Runtime.getRuntime().exec(arrayOf("xdg-open", URI(uri).toString()))</span>
</code></pre>
<p><strong>macOS (Kotlin/Native):</strong></p>
<ul>
<li><a target="_blank" href="https://github.com/JetBrains/skiko/blob/master/skiko/src/macosMain/kotlin/org/jetbrains/skiko/Actuals.macos.kt">https://github.com/JetBrains/skiko/blob/master/skiko/src/macosMain/kotlin/org/jetbrains/skiko/Actuals.macos.kt</a></li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-keyword">internal</span> <span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">URIHandler_openUri</span><span class="hljs-params">(uri: <span class="hljs-type">String</span>)</span></span> {
    NSWorkspace.sharedWorkspace.openURL(NSURL.URLWithString(uri)!!)
}
</code></pre>
<p><strong>Web (wasmJs):</strong></p>
<ul>
<li><a target="_blank" href="https://github.com/JetBrains/skiko/blob/master/skiko/src/wasmJsMain/kotlin/org/jetbrains/skiko/Actuals.wasmJs.kt">https://github.com/JetBrains/skiko/blob/master/skiko/src/wasmJsMain/kotlin/org/jetbrains/skiko/Actuals.wasmJs.kt</a></li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-keyword">internal</span> <span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">URIHandler_openUri</span><span class="hljs-params">(uri: <span class="hljs-type">String</span>)</span></span> {
    window.<span class="hljs-keyword">open</span>(uri, target = <span class="hljs-string">"_blank"</span>)
}
</code></pre>
<p><strong>iOS (UIKit):</strong></p>
<ul>
<li><a target="_blank" href="https://github.com/JetBrains/skiko/blob/master/skiko/src/uikitMain/kotlin/org/jetbrains/skiko/Actuals.uikit.kt">https://github.com/JetBrains/skiko/blob/master/skiko/src/uikitMain/kotlin/org/jetbrains/skiko/Actuals.uikit.kt</a></li>
</ul>
<pre><code class="lang-kotlin"><span class="hljs-keyword">internal</span> <span class="hljs-keyword">actual</span> <span class="hljs-function"><span class="hljs-keyword">fun</span> <span class="hljs-title">URIHandler_openUri</span><span class="hljs-params">(uri: <span class="hljs-type">String</span>)</span></span> {
    UIApplication.sharedApplication.openURL(
        url = URLWithString(uri)!!,
        options = emptyMap&lt;Any?, Any&gt;(),
        completionHandler = <span class="hljs-literal">null</span>
    )
}
</code></pre>
<h1 id="heading-what-javaawtdesktopbrowse-actually-does-openjdk">What <code>java.awt.Desktop.browse()</code> actually does (OpenJDK)</h1>
<p>Next: what <code>java.awt.Desktop.browse()</code> (AWT, not Compose) actually does, and how it opens the browser on different OSes.</p>
<h2 id="heading-high-level-flow">High-level flow</h2>
<p>In <code>java.awt.Desktop</code>, <code>browse()</code> does a few checks and delegates to the platform peer:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">browse</span><span class="hljs-params">(URI uri)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    checkAWTPermission();
    checkExec();
    checkActionSupport(Action.BROWSE);
    Objects.requireNonNull(uri);
    peer.browse(uri);
}
</code></pre>
<p>The peer comes from <code>SunToolkit.createDesktopPeer(this)</code> inside the private constructor:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-title">Desktop</span><span class="hljs-params">()</span> </span>{
    Toolkit defaultToolkit = Toolkit.getDefaultToolkit();
    <span class="hljs-comment">// same cast as in isDesktopSupported()</span>
    <span class="hljs-keyword">if</span> (defaultToolkit <span class="hljs-keyword">instanceof</span> SunToolkit) {
        peer = ((SunToolkit) defaultToolkit).createDesktopPeer(<span class="hljs-keyword">this</span>);
    }
}
</code></pre>
<p>Sources:</p>
<ul>
<li><p>Tag: <a target="_blank" href="https://github.com/openjdk/jdk17u/tree/jdk-17.0.2-ga">https://github.com/openjdk/jdk17u/tree/jdk-17.0.2-ga</a></p>
</li>
<li><p>Tarball: <a target="_blank" href="https://openjdk-sources.osci.io/openjdk17/openjdk-17.0.2-ga.tar.xz">https://openjdk-sources.osci.io/openjdk17/openjdk-17.0.2-ga.tar.xz</a></p>
</li>
<li><p>Release announcement: <a target="_blank" href="https://mail.openjdk.org/pipermail/jdk-updates-dev/2022-January/010793.html">https://mail.openjdk.org/pipermail/jdk-updates-dev/2022-January/010793.html</a></p>
</li>
</ul>
<p>Files:</p>
<ul>
<li>Desktop API: <a target="_blank" href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/share/classes/java/awt/Desktop.java">https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/share/classes/java/awt/Desktop.java</a></li>
</ul>
<h2 id="heading-windows">Windows</h2>
<p>On Windows, the peer is <a target="_blank" href="http://sun.awt.windows"><code>sun.awt.windows</code></a><code>.WDesktopPeer</code>. It calls a native <code>ShellExecute(...)</code> wrapper:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">browse</span><span class="hljs-params">(URI uri)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    <span class="hljs-keyword">this</span>.ShellExecute(uri, ACTION_OPEN_VERB);
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">ShellExecute</span><span class="hljs-params">(URI uri, String verb)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    String errmsg = ShellExecute(uri.toString(), verb);

    <span class="hljs-keyword">if</span> (errmsg != <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IOException(<span class="hljs-string">"Failed to "</span> + verb + <span class="hljs-string">" "</span> + uri
                + <span class="hljs-string">". Error message: "</span> + errmsg);
    }
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">native</span> String <span class="hljs-title">ShellExecute</span><span class="hljs-params">(String fileOrUri, String verb)</span></span>;
</code></pre>
<p>Native side uses Windows ShellExecute (after COM init):</p>
<pre><code class="lang-cpp"><span class="hljs-function">JNIEXPORT jstring JNICALL <span class="hljs-title">Java_sun_awt_windows_WDesktopPeer_ShellExecute</span>
  <span class="hljs-params">(JNIEnv *env, jclass cls, jstring fileOrUri_j, jstring verb_j)</span>
</span>{
    ...
    HRESULT hr = ::CoInitializeEx(<span class="hljs-literal">NULL</span>, COINIT_APARTMENTTHREADED |
                                        COINIT_DISABLE_OLE1DDE);
    HINSTANCE retval;
    DWORD error;
    <span class="hljs-keyword">if</span> (SUCCEEDED(hr)) {
        retval = ::ShellExecute(<span class="hljs-literal">NULL</span>, verb_c, fileOrUri_c, <span class="hljs-literal">NULL</span>, <span class="hljs-literal">NULL</span>,
                                SW_SHOWNORMAL);
        error = ::GetLastError();
        ::CoUninitialize();
    }
    ...
}
</code></pre>
<p>Files:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/windows/classes/sun/awt/windows/WDesktopPeer.java">https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/windows/classes/sun/awt/windows/WDesktopPeer.java</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/windows/native/libawt/windows/awt_Desktop.cpp">https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/windows/native/libawt/windows/awt_Desktop.cpp</a></p>
</li>
</ul>
<h2 id="heading-macos">macOS</h2>
<p>On macOS, the peer is <code>sun.lwawt.macosx.CDesktopPeer</code>. <code>browse()</code> delegates to <code>lsOpen(uri)</code> which calls native <code>_lsOpenURI(...)</code>:</p>
<pre><code class="lang-java"><span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">browse</span><span class="hljs-params">(URI uri)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    <span class="hljs-keyword">this</span>.lsOpen(uri);
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">lsOpen</span><span class="hljs-params">(URI uri)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    <span class="hljs-keyword">int</span> status = _lsOpenURI(uri.toString());

    <span class="hljs-keyword">if</span> (status != <span class="hljs-number">0</span> <span class="hljs-comment">/* noErr */</span>) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IOException(<span class="hljs-string">"Failed to mail or browse "</span> + uri + <span class="hljs-string">". Error code: "</span> + status);
    }
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">native</span> <span class="hljs-keyword">int</span> <span class="hljs-title">_lsOpenURI</span><span class="hljs-params">(String uri)</span></span>;
</code></pre>
<p>Native side uses LaunchServices (<code>LSOpenURLsWithRole</code>):</p>
<pre><code class="lang-plaintext">JNIEXPORT jint JNICALL Java_sun_lwawt_macosx_CDesktopPeer__1lsOpenURI
(JNIEnv *env, jclass clz, jstring uri)
{
    OSStatus status = noErr;
JNI_COCOA_ENTER(env);

    // So we use LaunchServices directly.
    NSURL *url = [NSURL URLWithString:JavaStringToNSString(env, uri)];

    LSLaunchFlags flags = kLSLaunchDefaults;
    LSApplicationParameters params = {0, flags, NULL, NULL, NULL, NULL, NULL};
    status = LSOpenURLsWithRole((CFArrayRef)[NSArray arrayWithObject:url],
                                kLSRolesAll, NULL, &amp;params, NULL, 0);

JNI_COCOA_EXIT(env);
    return status;
}
</code></pre>
<p>Files:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/macosx/classes/sun/lwawt/macosx/CDesktopPeer.java">https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/macosx/classes/sun/lwawt/macosx/CDesktopPeer.java</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/macosx/native/libawt_lwawt/awt/CDesktopPeer.m">https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/macosx/native/libawt_lwawt/awt/CDesktopPeer.m</a></p>
</li>
</ul>
<h2 id="heading-linux-x11-unix">Linux / X11 (Unix)</h2>
<p>On Linux/X11, the peer is <code>sun.awt.X11.XDesktopPeer</code>. Java delegates to a native <code>gnome_url_show(...)</code>:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">browse</span><span class="hljs-params">(URI uri)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    launch(uri);
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">launch</span><span class="hljs-params">(URI uri)</span> <span class="hljs-keyword">throws</span> IOException </span>{
    <span class="hljs-keyword">byte</span>[] uriByteArray = ( uri.toString() + <span class="hljs-string">'\0'</span> ).getBytes();
    <span class="hljs-keyword">boolean</span> result = <span class="hljs-keyword">false</span>;
    XToolkit.awtLock();
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">if</span> (!nativeLibraryLoaded) {
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IOException(<span class="hljs-string">"Failed to load native libraries."</span>);
        }
        result = gnome_url_show(uriByteArray);
    } <span class="hljs-keyword">finally</span> {
        XToolkit.awtUnlock();
    }
    <span class="hljs-keyword">if</span> (!result) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IOException(<span class="hljs-string">"Failed to show URI:"</span> + uri);
    }
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">native</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">gnome_url_show</span><span class="hljs-params">(<span class="hljs-keyword">byte</span>[] url)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">native</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">init</span><span class="hljs-params">(<span class="hljs-keyword">int</span> gtkVersion, <span class="hljs-keyword">boolean</span> verbose)</span></span>;
</code></pre>
<p>Native side tries GTK first (<code>gtk_show_uri</code>), then GNOME (<code>gnome_url_show</code>):</p>
<pre><code class="lang-c"><span class="hljs-function">JNIEXPORT jboolean JNICALL <span class="hljs-title">Java_sun_awt_X11_XDesktopPeer_init</span>
  <span class="hljs-params">(JNIEnv *env, jclass cls, jint version, jboolean verbose)</span>
</span>{
    <span class="hljs-keyword">if</span> (gtk_has_been_loaded || gnome_has_been_loaded) {
        <span class="hljs-keyword">return</span> JNI_TRUE;
    }

    <span class="hljs-keyword">if</span> (gtk_load(env, version, verbose) &amp;&amp; gtk-&gt;show_uri_load(env)) {
        gtk_has_been_loaded = TRUE;
        <span class="hljs-keyword">return</span> JNI_TRUE;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (gnome_load()) {
        gnome_has_been_loaded = TRUE;
        <span class="hljs-keyword">return</span> JNI_TRUE;
    }

...

<span class="hljs-function">JNIEXPORT jboolean JNICALL <span class="hljs-title">Java_sun_awt_X11_XDesktopPeer_gnome_1url_1show</span>
  <span class="hljs-params">(JNIEnv *env, jobject obj, jbyteArray url_j)</span>
</span>{
    ...
    <span class="hljs-keyword">if</span> (gtk_has_been_loaded) {
        gtk-&gt;gdk_threads_enter();
        success = gtk-&gt;gtk_show_uri(<span class="hljs-literal">NULL</span>, url_c, GDK_CURRENT_TIME, <span class="hljs-literal">NULL</span>);
        gtk-&gt;gdk_threads_leave();
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (gnome_has_been_loaded) {
        success = (*gnome_url_show)(url_c, <span class="hljs-literal">NULL</span>);
    }
    ...
}
</code></pre>
<p>Files:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/unix/classes/sun/awt/X11/XDesktopPeer.java">https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/unix/classes/sun/awt/X11/XDesktopPeer.java</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/unix/native/libawt_xawt/xawt/awt_Desktop.c">https://github.com/openjdk/jdk17u/blob/jdk-17.0.2-ga/src/java.desktop/unix/native/libawt_xawt/xawt/awt_Desktop.c</a></p>
</li>
</ul>
<h2 id="heading-other-platforms">Other platforms</h2>
<p>In OpenJDK, desktop support is primarily implemented for Windows, macOS, and Unix/X11. On headless/minimal builds (or when native libraries are missing), <code>Desktop</code> may report that <code>BROWSE</code> is unsupported and throw <code>UnsupportedOperationException</code> / <code>IOException</code> instead of opening a browser.</p>
<h2 id="heading-summary">Summary</h2>
<p>When you write <code>uriHandler.openUri("</code><a target="_blank" href="https://hashnode.com/">https://hashnode.com/</a><code>")</code> in your common Compose code, you are triggering a massive chain of abstractions:</p>
<ol>
<li><p><strong>Compose:</strong> <code>LocalUriHandler</code> delegates to platform implementation.</p>
</li>
<li><p><strong>Skiko:</strong> Delegates to <code>java.awt.Desktop</code> (on JVM).</p>
</li>
<li><p><strong>AWT:</strong> Delegates to OS-specific Peers (<code>WDesktopPeer</code>, <code>CDesktopPeer</code>, <code>XDesktopPeer</code>).</p>
</li>
<li><p><strong>JNI:</strong> Crosses the boundary into C/C++/Objective-C.</p>
</li>
<li><p><strong>OS API:</strong> Finally calls <code>ShellExecute</code>, <code>LSOpenURLsWithRole</code>, or <code>gtk_show_uri</code>.</p>
</li>
</ol>
<p>It is a long journey for a simple click, but it highlights the power of Kotlin Multiplatform: writing once, and letting the framework handle the system-level complexity.</p>
]]></content:encoded></item></channel></rss>