If you want a less technical version of this, check out the one on my personal blog here.

Kotlin MultiPlatform has been taking off, and for a while now I’ve felt that it is the most promising multi-platform code-sharing path forward. The problem it attempts to solve is very real, and at Skip Scooters I saw first-hand how expensive it was to write the same business logic three times. Startups want to move quickly, and hiring developers is extremely difficult across all companies — wasting developer time, then, should be a cardinal sin. But up until now, I haven’t been happy with any solution.

React Native, while easy to get started, has severe performance implications, which led even the most ardent supporters to abandon it after thousands of engineering hours were invested.

Flutter’s very approach seems doomed to fail. Embedding a full V8 engine inside every application causes unnecessary bloat — and still can’t compete with native performance.

I’ll freely admit I never seriously considered Cordova, Xamarin, or any others.

For the first time, however, we have a multiplatform solution that seems to not make any compromises. The UI can deliver native-level performance precisely because the UI code is written natively. Kotlin multiplatform handles all the business logic that needs to be thoroughly tested and validated, and it exports libraries compatible with various platforms. It even supports Desktop!

I was pretty optimistic when I saw that Square’s CashApp was written in Kotlin Multiplatform, especially because it’s an app I’ve always admired. From my time on Stripe Terminal, we spent a great deal of energy looking into Square’s apps, and they always impressed me. Then Netflix announced that they, too, switched to KMM for their apps.

If Square and Netflix can use KMM without too many gotchas, then KMM is ready for prime-time enterprise-level loads. I decided to check out Kotlin/JS + React first because it had the lowest barrier to entry. I already knew Kotlin, Javascript, and React after all.

I decided to make a simple app — PaintMix — to test out how KMM behaves, the learning curve, and any known gotchas. Check it out!

Building

The first time you build a brand new project, there’s a ton of scary-looking errors that turn out to be completely innocuous and expected.

An example error IntelliJ gives when building a brand new project

When you run the project in continuous development mode, the previous build messages just stay. Even a successful build never ‘erases’ the previous errors, so you’re never quite sure if the error message you’re currently seeing is stale or not.

Worse, there’s no place to delete previous messages while continuous development is running. The only way I found to remove them was to stop IntelliJ’s tasks entirely, delete them, and then re-start continuous development mode… which defeated the entire purpose of continuous development.

Just like Android Studio, I’d randomly have to “File > Invalidate Caches & Restart” and ./gradlew clean , and I’d never really know when I had to do it (see the functional components section below for full details). I definitely foresee a lot of hesitance from JS developers about this. JS is meant to be a lightweight, ultra-expressive language, and having strange compiler errors that force you to restart the IDE — while the norm for Android developers — is not something most JS developers are accustomed to.

While building via Android Studio worked every time for me, I’d periodically run into mysterious build failures on the command line. These were almost always flakey and would go away with a repeat. As you can see below, the first ./gradlew build fails, and the second succeeds. All I did in between was run git status . It doesn’t inspire confidence that such a small project (just six files) would already run into these sorts of issues.

Debugging

Debugging within IntelliJ doesn’t work. The recommended solution as of today is to just use chrome to find the file, and use the debugger there.

This was tricky at first. For whatever reason, Chrome wasn’t uploading the source mappings, as you can see there, and clicking the “i” or help didn’t do much, so all I had access to was the generated JS files which were not readable, even in debug builds.

An example of the generated JS code

I’m not exactly sure how, but at some point I think I did a ./gradlew cleanand restarted my browser, and magically, I found the Kotlin files. Setting breakpoints in the browser in the Kotlin code works perfectly.

Setting a breakpoint in the .kt file

This is good! It’s almost exactly what I want, except in Chrome for whatever reason instead of in IntelliJ, but it didn’t bother me too much.

React Classes vs Functional Components

When you create a new project via IntelliJ, it defaults to classes instead of functional components. I made the mistake of just rolling with it instead of doing the refactor as early as possible.

I tried to muddle along as much as I could, but eventually, I found myself overriding too many lifecycle methods, and I kept getting ominous warnings like componentWillReceiveProps will soon be deprecated .

One of the tricky parts I discovered using these React classes was that we’re not supposed to call super.componentWillReceiveProps (or any other lifecycle method) — if you do, it’ll generate a very vague null pointer exception. It took me a while to realize you’re not supposed to call super, and coming from Java where it’s an error if you don’t call super, it felt super weird.

Anyway, I decided to do the refactor and switch to functional components. Many blog posts recommended using the kotlin-react-function library over the vanilla JetBrains approach, but for whatever reason Gradle had huge problems fetching the package from maven, even though it clearly exists.

As an Android developer, I’m quite familiar with Gradle. I tried debugging this for the better part of a day, and could not figure it out at all. In the end I gave up and decided to just use the vanilla functional component style.

This is when I remembered that sometimes when you make Gradle changes, IntelliJ will randomly cache the wrong configuration. I was getting super weird errors, and upon remembering, I invalidated caches and restarted the IDE.

Official Docs > Google / Stack Overflow

Like any new technology in the early stages, googling and searching Stack Overflow is not only difficult due to the relatively few search results, but many times the results are wrong because the early APIs change in breaking ways. This happened several times, but especially so in the functional react components section because soon after the blog posts that recommended kotlin-react-function were published, IntelliJ actually addressed the feedback and came out with functionalComponent<T> as a way to remove all the boilerplate!

See the updated official documentation about using React hooks. I was about a day into this project, and I learned that the docs, and especially the labs, really are the best resources around. Most blog posts are just too old and outdated.

Compilers

Along the way in my classes to functional components refactor, I ran into KT-40711. The new IR compiler seems awesome, and I hope at some point it’ll be ready, but for the purposes of this project I just switched back to LEGACY, and the error disappeared.

Deploying

Deploying was a breeze. All I had to do was a single ./gradlew build, just like I’d do on Android, and everything was ready to be uploaded!

Hooking up to firebase hosting was simple, too. I just had to set the “public” path to “build/distributions”.

NPM Modules — Tailwind

Using any NPM module is, theoretically, easy. Simply start by running ./gradlew kotlinNpmInstall. This will create a build/js folder, inside of which there will be all the standard stuff one would expect — node_modules/ , package.json , etc.

In my case, I wanted to use Tailwind, so I ran npx tailwind init inside the build/js folder. This succeeded, so I moved the config file outside to the root of the project. Then I had to install PostCSS and create a webpack config file for proper tree-shaking.

For the most part, I followed this guide, but I could never get the final step of require -ing my .css file inside the main Kotlin client actually working. It kept giving me “module not found” errors, so after half a day of debugging I tried just adding <link rel="stylesheet" href="app.css"> to the index.html file, and it seemed to work.

While that compiles, I actually couldn’t access any of the tailwind classes. I ended up giving up and simply using the CDN version of Tailwind, despite its limitations, inside the HTML file, and that finally worked.

NPM Modules — Firebase

This was probably the most disappointing section for me to write. Integrating NPM modules, theoretically, is very easy based on the instructions above. Just run a simple gradle command and add implementation(npm("firebase", "8.3.2")) to your dependencies. But it’s not exactly that simple.

Since JS libraries are untyped, the user is responsible for writing Kotlin wrappers around the libraries. This is an example of what I wrote for firebase.

Even still, whenever I called firebase in any of my Kotlin files, I’d get Uncaught ReferenceError: firebase is not defined . I spent roughly a full day trying to figure this out.

I found this pretty amazing repo that implements type-safe firebase wrappers. I tried copying these files down even to the filenames and the directory structures, but I still had no luck.

I did find a pretty sweet pre-built library, but it seems that they’re early-access for now. With that, I resigned myself to set up a backend and just using JSON instead of any Firebase SDK.

Ultimately, even if it had worked, I think the real takeaway is that integrating NPM modules to KotlinJS is not as trivial as it may appear. Creating the typesafe wrapper can take quite a bit of work.

API Requests

Making API requests was another area that was harder than it should be. Based on the documentation, this is what I tried.

For whatever reason, it simply was not happy with casting to any sort of data class. I tried using external interface instead, but that still didn’t work. I also tried mucking with the backend API into various formats for about an hour, but nothing satisfied it. I also copied the raw server response and just did a JSON.parse(), and it was totally fine!

At this point, I was at a loss for how to proceed, but luckily, the documentation shows what not to dounsafeCast . This worked perfectly.

Definitely not ideal at all, but it’s good enough for a side-project.

The Positives

When things worked, they just worked. JetBrains has done a great job providing type-safe bindings for React, HTML, and even CSS! No more typos in CSS values and wondering why things don’t render.

The type-safety allowed me to refactor components with relative ease. I had very few bugs when converting from classes to functional components (excepting the compiler issue, of course).

No more need for PropTypes! Kotlin data classes are a joy to work with, and putting all the real logic there made the code extremely testable. Since these data classes contain no React code, you can achieve relative code confidence with unit tests alone (who wants to spin up Selenium, yuck).

TypeScript

The real issue with Kotlin/JS is that it’s not really competing against Javascript in most cases. It’s competing against Typescript. I’ve been using Typescript on the backend and frontend for most of my personal projects, and it accomplishes ~80% of the type safety of Kotlin with much less friction.

I can’t really say anyway that Kotlin/JS outperforms Typescript, when there are no compiler issues, debugging can be done from right within the IDE, and the gradual typing system allows a pretty easy migration story.

Conclusion

I loved how few logic errors I had due to Kotlin, and the type-safe wrappers JetBrains provided by default made basic views incredibly easy. My velocity was definitely slower though. Part of this is learning a new framework, but some of this is just due to the various gotchas inherent in Kotlin/JS. Integrating with NPM libraries, while possible, isn’t as easy. You have to write your own type-safe wrapper. Even making API requests seems harder than it has to be, and the debugging story isn’t all the way there yet.

I think if your company has dedicated developer productivity resources to help iron out these issues, Kotlin/JS definitely makes sense in an enterprise setting. In large organizations, trading velocity for reliability and type-safety is a feature, not a bug. And if your company is all in on Kotlin Multi-Platform, then it makes all the sense in the world.

For a personal project though, I wouldn’t recommend it.

I dabble in software and writing.