When you start a new project on the JVM, should you pick Java or Kotlin?
You might have a preference based on the ecosystem — the tooling around it, who we can ask for help, and the libraries we can use — which we discussed in part 1 of this article series. Or you might have a preference based on the business modeling capabilities of Java or Kotlin, which we will show in part 3.
But what if you want to target a platform other than the server? In this part, we will explore whether Java or Kotlin can be used to target end-user platforms like Android (39% of market) and iOS (16%) — in addition to Windows Desktop (30%) and macOS/OS X (8%) (based on the global market share of operating systems). One additional platform we want to target is the web, because it gives us not only enormous reach but also a great application delivery mechanism. In total, that gives us the following platform targets:
- Android
- iOS
- Web
- Linux
- macOs
- Windows
For each of these platforms we need:
- Multiplatform runtime and UI frameworks
- Mature multiplatform libraries and frameworks
Let’s break down each point.
Multiplatform Runtime and UI Frameworks
Java
The 1996 slogan of Java was “write once, run anywhere”, and that is true as long as the platform you target is supported by the JVM. Mobile operating systems like iOS/Android and the modern web have changed this promise a bit. Currently, the OpenJDK is only provided as pre-built binaries for:
- Linux / AArch64, Linux / x64
- macOS / AArch64, macOS / x64
- Windows / x64
Solaris was deprecated in 2020, Java ME has seen no release since 2018, Oracle themselves certify no other platform, and IBM AIX is only supported as a build platform.
There are ways to run Java on mobile or in a web browser, but they involve a bit — shall we say — creativity.
Java On Mobile
Perhaps the easiest way is to run Java on Android. While Android does not technically use a JVM (it uses the Android Runtime [ART] instead), its runtime can execute Java code.
Given you have a recent version, you can use many Java 17 core libraries. Many advanced language features are out-of-scope, however. For example, text blocks (Java 15) and sealed classes (Java 17) can be used but records (Java 16) seem to be unsupported. This makes programming Java on Android rather tricky. Things that work in the JVM might not work in the ART. This of course also affects libraries. If the library you want to use requires records, you cannot use it on Android.
To run modern Java code on iOS we can use Gluon. It leverages JavaFX and GraalVM (more on that below) to run Java bytecode on iOS and Android. The specific tool they use is called Substrate. Under the hood, it uses ahead-of-time (AOT) compilation to native binaries rather than a traditional JVM. This means you can build a program that leverages the Java language, Java libraries, and the JavaFX UI framework on mobile.
JavaFX, an official OpenJDK project first released in 2008, feels a bit dated though. Not because of how it looks (it can be styled with regular CSS) but because of its developer experience. Where more recently designed libraries (e.g. SwiftUI, Compose or React) let you compose applications hierarchically in code, JavaFX relies on flat composition in code or hierarchical in xml. Additionally its property binding concept is a bit verbose. Nevertheless, this framework allows us to build Linux/Mac/Windows desktop user interfaces and, with Gluon, also mobile UIs.
Gluon supports JavaFX up to version 26 (March 2026) and JDK up to version 26 (also March 2026). The Gluon developers even seem to spearhead the OpenJDK mobile project, whose goal is to port the JDK to popular mobile platforms such as iOS and Android. As of September 2025, they have a working pipeline that runs a HelloWorld application on iOS with a regular JVM.
As of right now, however, mobile is not an officially supported platform and you have to rely on third-party providers. This can be acceptable for many applications, but it does mean that long-term support and ecosystem stability depend on the stability of these vendors.
Java on Web
The Java applet API has been deprecated since JDK 9 (2017) and is removed in JDK 26 (March 2026). If we really want to run Java in the web, we can use third-party providers like CheerpJ or WebSwing, that both promise to run Swing or JavaFX applications in the browser. The former works by executing .jar files in a reimplementation of Java SE in JavaScript, with a Just-In-Time (JIT) compiler running in WASM (a portable binary-code format that your browser supports natively). The latter works by rendering server-side and streaming the UI. This does not meet our “run Java in the browser” goal, but it is an interesting tool nonetheless. CheerpJ supports JDK up to version 11 (17 in preview). WebSwing supports up to JDK 21.
If we do not need a UI (Swing or JavaFX), we can also use TeaVM, a compiler that converts bytecode to JavaScript+WASM. Because it works on bytecode, it can also convert Kotlin and Scala. TeaVM only supports a subset of the Java Standard Library though.
The closest we can get to official Java on Web support is Graal, an Oracle project. It is outside of the Java Community Process (JCP) that is responsible for evolving Java. In short, it is a project by Oracle but not an official Java project, so it’s best to think of Graal as a second-party project.
You might be familiar with GraalVM, which can compile Java ahead-of-time into a native executable for a specific operating system and architecture (one file for macOS / AArch64, one for macOS / x64, etc.). But GraalVM also has an experimental web image target that can turn Java code into WASM, which means we can run Java in the browser. There are also other runtimes, such as Wasmtime, but browsers seem to have the broadest WASM feature support.
The experimental GraalVM target needs two WASM runtime features: Garbage Collection (WasmGC) and Exception Handling with exnref. The latter, because it allows almost a one-to-one mapping of Java exception handling. The former, because it means no garbage collector has to be included with the WASM binary, which reduces the file size. A hello-world Java program compiled to WASM is then about 1 MB in size. Why it is so large will become clear once we dig into using Java libraries in WASM. Nevertheless, the Graal project wants to reduce this further in the future. For reference:
- a bytecode
HelloWorld.classis about 500 bytes (requires a JVM and represents the absolute minimum) - a bytecode
RandomNumber.classis about 900 bytes - a C random number generator converted to WASM is about 500 bytes (see WASM Wheel)
- a Java random number generator converted to WASM via TeaVM is about 5 kb (see WASM Wheel)
In short, converting Java to WASM and running that in the browser is possible, but you either rely on third-party or experimental second-party tools. Additionally, a WASM runtime is not a drop-in replacement for the JVM. It has (very) different characteristics which we will explore below, when we talk about multiplatform libraries.
Web on Java
GraalVM can not only convert Java code to binary, it can also run other languages inside the JVM. Instead of Java running in the web, we can let the web run in Java. With GraalJS we can execute JavaScript (ECMAScript 2024) inside our Java program. The following code sample shows how to execute a simple JavaScript program from within Java.
import org.graalvm.polyglot.*;
import org.graalvm.polyglot.proxy.*;
String JS_CODE = """
(function myFun(param){ console.log(`Hello ${param}`); })
""";
void main(){
try (Context context = Context.create()) {
Value value = context.eval("js", JS_CODE);
value.execute("World");
}
}
Note however that GraalJs does not support full multi-threading. If that is a required feature, the KarateJs runtime might be more interesting.
Polyglot Java
Graal can also run other languages inside the JVM besides JavaScript. With GraalWasm we can execute WASM 2.0 (March 2025) binaries, and with GraalPy we can execute Python 3.12 (Oct 2023).
Graal has made the JVM polyglot. This does not help with our multiplatform goal, but it is still an impressive engineering feature of the Graal team that needs to be called out. In a way, their polyglot approach is the opposite to the one Kotlin has taken.
Kotlin Multiplatform
Where Graal allows running multiple languages inside the Java platform, Kotlin is one language that can be compiled to multiple platforms.
Kotlin can officially be compiled to the following (see also stability of Kotlin components page):
- the JVM (Kotlin/JVM) and Android ever since version 1.0 (Feb 2016)
- to JavaScript since version 1.3 (Kotlin/JS, Oct 2018), right now only ES5 with ES2015 as a roadmap goal
- to iOS/macOS since version 1.9 (Kotlin/Native, Jul 2023)
- to WASM (beta) since version 2.2.20 (Kotlin/WASM, Jan 2026)
These projects together form Kotlin Multiplatform (KMP). KMP is unique because it is designed for interop with the target platform. For example, Swift code can call Kotlin code and Kotlin code can call Swift code. This makes it possible to gradually migrate from the platform language to Kotlin, or to gradually migrate from Kotlin to the platform language, or to share code between platforms. The latter is something Norway’s postal service had great success with.
KMP is even more interesting when we want to build a UI shared between multiple platforms.
Compose Multiplatform
Where KMP is about sharing logic, Compose Multiplatform (CMP) is about sharing the user interface.
The foundation was laid by Google with their declarative and Kotlin-based UI-DSL Jetpack Compose, which they launched in Jul 2021. Jetpack Compose and CMP share common APIs and Core, giving them perfect interoperability (the version numbers of Jetpack Compose and CMP are synchronized). As a result, the UI imports always start with androidx.compose.*, even on platforms other than Android. The rendering library is called Skia. It is the same library Google Chrome, ChromeOS, Android, and Flutter use. Taken together this means that we can develop shared UIs:
- on Android and Desktop since CMP version 1.0.0 (Dec 2021)
- also on iOS/macOS since CMP version 1.8.0 (May 2025)
- soon also on Web/WASM, with a beta since CMP version 1.9.0 (Sep 2025)
Like KMP, CMP is designed with interop in mind. We can embed platform-native views in CMP or CMP in platform-native views.

Verdict
For Java, “Write once, run anywhere” means that your code can run on Linux, macOS or Windows. Web browsers or mobile platforms such as Android or iOS are not official targets. If that is of interest, we need to rely on third-party providers (with UI) or an experimental second-party compilation target (without UI).
Kotlin extends the original “write once, run anywhere” idea beyond the desktop/server JVM. With Kotlin Multiplatform and Compose Multiplatform, the same language and largely the same codebase can target JVM, Android, iOS, desktop, JavaScript, and WebAssembly, while still inter-operating closely with each platform’s native APIs and UI frameworks. In practice, this makes Kotlin a more realistic option for truly cross-platform development today, especially when you want to cover mobile and web alongside traditional JVM targets.
Mature Multiplatform Libraries and Frameworks
Java
Java has around 808k packages, including Spring (Boot), Jackson, Hibernate and JavaFx. For comparison, NuGet (.NET) has 760k, and npm (JavaScript) has 5 000k packages. Package counts are of course a rough metric. Java package tend to be larger in scope, while npm has many micro packages.
These Java packages can only be used on the platforms with a JVM, however, and those are officially limited to Linux/Mac/Windows.
Java Multiplatform
Not all Java libraries are multiplatform compatible. As discussed previously, there are roughly three ways to run Java on other platforms:
- Use the Android ART
- Compile your whole application to a native image
- Compile your whole application to a WASM image
All three affect the libraries we can use.
Java Multiplatform Android
ART supports only a subset of modern Java, which limits our libraries to that subset.
Java Multiplatform Mobile
GraalVM native image (e.g. used by Gluon to provide Java on iOS/Android) comes with some complications. Native image needs to know all code at build time. This is called the “closed-world assumption”. Some libraries however generate code or change what is executed at runtime, and that makes them incompatible with the closed-world assumption. Some libraries might also need access to Java bytecode which is not available in a native image. Or some libraries might use features that behave differently in the native image than in the JVM.
This reduces the number of available libraries significantly. The GraalVM compatibility page lists around 270 maintainer or community tested artifacts. That does not mean only these work. It simply means that if we try libraries beyond these, we are in unknown territory and have to test ourselves.
Java Multiplatform WASM
The WASM image has all the same restrictions as the native image, but with additional restrictions imposed by the WASM runtime. Some platform actions that are available in the JVM and native are not available in a WASM runtime. That is because one of WebAssembly’s security goals is to protect users from buggy or malicious modules, whereas in Java the premise is that executed code is to be trusted. As such, the WASM language has no syscall instructions or built-in I/O facilities — no description of how to reach the outside world. That means we cannot do any file operations or even get the current time. WASM runtime embedders (like the GraalVM WASM target or CheerpJ) can however pass in new functions that add capabilities, but these are then tied to the embedder and we lose portability.
An embedder is something that executes code in a WASM runtime and passes in additional functions that can be used in the runtime. The GraalVM WASM target for example embeds (wraps) our compiled Java to WASM code in a small JavaScript program. This program then launches the code in a WASM runtime and passes in a bunch of JavaScript functions that our compiled Java program can call. This means the compiler has to know which functions the embedder passes to the runtime. They are tied together. We can see that in the following example where the compiler has to know that the namespace interop houses all the interop functions.
const wasmImports = {
interop: {
'Date.now' : (...args) => Date.now(...args),
// etc.
}
};
await WebAssembly.instantiateStreaming(
fetch("myJavaProgram.wasm"),
wasmImports
);
This is a problem that the WebAssembly System Interface (WASI) will solve. It is a set of API specifications for WASM and can be thought of as the WebAssembly standard library. It will provide a set of minimal APIs that are always there, regardless of the runtime. Right now (March 2026) there are 7 proposals being implemented (phase 3): clocks, random, filesystem, I/O, sockets, command-line interface (CLI) and Http. These APIs follow a capability-based security model, in line with the WASM security goals, where modules are granted only specific, pre-approved capabilities (e.g., read-only access to a directory). Once they reach step 5 in the phase process, they are considered standardised. That might happen in 2026.
So in short, which libraries we can use depends on the WASM compiler and on the WASM runtime and currently also on the WASM embedder. The compiler has to support the Java standard library call that the Java library is making, and if the standard library is calling the outside world, the runtime has to support that call as well (see WASI), or the embedder needs to pass it in.
Java Multiplatform JavaScript
CheerpJ provides its own JDK port in JavaScript but currently only supports the JDK up to version 11 (17 in preview), again limiting the libraries we can use. They have shown the promise of their approach however by running the 400MB IntelliJ jar files and an unmodified MineCraft .jar, both in the browser.
Kotlin Multiplatform
Kotlin, when compiled to multiplatform targets like iOS, can no longer use JVM libraries. On these other platforms, Kotlin requires KMP libraries. Not all Kotlin libraries are multiplatform libraries. Developers have to explicitly opt-in by using only other multiplatform libraries and putting all their production code into src/commonMain/kotlin instead of src/main/kotlin. Around 3000 such libraries exist, including Ktor (client/server applications), kotlinx.serialization (e.g. Json serialization), Exposed (database access), Compose (multiplatform user interfaces), and TestBalloon (multiplatform testing). For comparison, other multiplatform ecosystems have 2300 (React Native) and 35 000 (Flutter) packages.
Verdict
There is no first-party way to run Java on Android, iOS or in a web browser. For that, we have to rely on other providers.
Right now, it seems Java libraries on Android/iOS get the best support when used with (third-party provider) Gluon’s native compilation. Not all libraries work when compiled to native, however. Right now there are only 270 native tested artifacts and no list exists for which libraries work on mobile.
Java libraries in the web are a more nuanced topic. TeaVM supports only a subset of the Java Standard Library, which limits our libraries. CheerpJ currently only supports the JDK up to version 11, again limiting our library choices, but also providing impressive IntelliJ and Minecraft in a browser demos. Finally, converting Java to WASM, while possible, is currently experimental second-party tooling (GraalVM).
In Kotlin, the situation is simpler. Kotlin has about 3000 multiplatform libraries that work across its targets. Compared to other multiplatform ecosystems, that puts it in a good place but with ample room to grow, especially for more specialized domains and long‑tail use cases.
Multiplatform Summary
For each of our platforms we get the following ratings:
| Platform | Java | Kotlin | Comment |
|---|---|---|---|
| mobile/Android | ●◐○ | ●●● | Java runs via ART with language/stdlib gaps or depends on third‑party native tooling (Gluon) where extent of library support is not clear; Kotlin is a first‑class Android language. |
| mobile/iOS | ●◐○ | ●●◐ | Java depends on third‑party native tooling (Gluon) where extent of library support is not clear; Kotlin/Native + Compose iOS are officially supported but younger than their Android counterpart |
| web/Js | ●◐○ | ●◐○ | Java can target JS via tools like TeaVM/CheerpJ; Kotlin/JS is official and stable, but targets old JavaScript standard and has no CMP. |
| web/WASM | ◐○○ | ●◐○ | Java targets WASM via experimental second-party GraalVM; Kotlin/Wasm with CMP is an official (beta) compiler target; both WASM compilations would benefit from a finalized, widespread WASI standard. |
| desktop/Linux | ●●○ | ●●● | Both run excellently on the JVM; using JavaFX for UIs is more cumbersome compared to Compose |
| desktop/macOS | ●●○ | ●●● | Same as Linux |
| desktop/Windows | ●●○ | ●●● | Same as Linux |
Java’s “run anywhere” is desktop‑only. Additional platforms require experimental or third-party tooling. Kotlin clearly differentiates itself because it has a coherent, officially backed cross‑platform story for logic and UI. It extends the Java world to Android, iOS, desktop, and WebAssembly with a single language and shared UI frameworks, making it a stronger candidate when you need cross‑platform apps beyond the traditional JVM server/desktop space.
In the next part of this series we will take a look at the business modeling capabilities of both languages.