Tech Stack 4.0
I’ve recently tweaked the tech stack on my personal project yet again. I’m hoping that this is the last time for a while. As a result, it felt like a good time to share where I currently am.
Background
My project is a fairly generic web app, which I’d possibly also want a mobile app for in the future. Seems pretty standard.
A (personal) complicating factor is that I had very little exposure to modern anything, especially in the frontend ecosystem. There’s been a lot of investigation and learning during this project.
I wanted to use Kotlin, for two main reasons. One, I liked it—writing Kotlin code feels nice. Two, their multiplatform ambitions seem like a game changer.
The TL;DR on Kotlin Multiplatform: they’d like to make it possible to write some Kotlin code that is then (natively, I believe) usable in any platform—JVM, JS, etc.
In theory, being able to use the same code across platforms would (a) reduce developer effort to add/maintain platforms and (b) help keep things in sync.
Tech Stack 1.0
This approach was pure Kotlin.
On the backend, any JVM-based approach seems stable and feasible (one of Kotlin’s earliest features was 100% interop with Java). To the extent that I’ve used it, JetBrains' web framework (Ktor) seems solid.
On the frontend, you can use Kotlin/JS with JS libraries—notably, any npm library. JetBrains also provides wrappers around some very common JS libraries (React + Redux).
My aim was to use Kotlin libraries as much as possible, as I figured they’d (a) work best and (b) be easiest to use, potentially cross-platform. If that wasn’t possible, I’d import an npm library.
Initial Success
I actually had a lot of success with an initial prototype. I had a Kotlin multiplatform libraries and a game engine, which I was able to easily reuse both server-side and client-side. That was great.
One day, however, my code suddenly broke. I’m still not sure what changed. I think I’m trying to pass objects with behavior (i.e. non-data) through my React components, which isn’t allowed. But I don’t get why it worked at all…?
Challenges
A frequently frustrating problem is that JS problems are extremely confusing with Kotlin/JS. There are also many places for things to go wrong.
- Build configuration.
Using Kotlin means using Gradle, which adds a layer of indirection to your JS setup. This made it quite frustrating for me to set up what should have been easy—JS libraries (Tailwind), a local development environment, etc.
A significant challenge here was just trying to understand what Gradle was doing, especially with plugins. I find the Gradle documentation to be bad (it’s there but it’s hard to figure out what you need to know). There’s also a lack of examples for this tech stack, making things harder. - JS errors.
Kotlin translates your code to JS and minifies it, making stack traces harder to read. (I haven’t looked into source maps, which might fix this issue.)
Errors also come from underlying libraries. They could have problems or you might be using them incorrectly. It’s sometimes hard to figure out what these errors actually mean, since you have to unravel which library is upset and why.
Additionally, it’s frustrating for someone with little modern front-end experience: I was confused for a while trying to figure out what to do when I got an Immer error. Platform-specific errors are confusing and are quite annoying when you’re trying to write platform-agnostic code. - Translation It’s good that you can write Kotlin code and it works in your platform of choice. The difficulty is when you have problems (e.g. the errors mentioned above). Fixing these sometimes involves knowing what Kotlin is doing under the hood—which can be frustrating to figure out.
Some of my problems revolved around Redux. I was trying to use redux-kotlin—a multiplatform Kotlin redux implementation.
Pros: redux stores usable in non-JS environments. Cons: lack of features and typing issues.
Since typing in Kotlin and JS is very different, some things can’t really be done well in Kotlin. Specifically, redux-kotlin can’t provide a createReducer
because of type system reasons—which feels like a dealbreaker.
Redux-kotlin also doesn’t have React support yet. I wasn’t willing to try to plug things in and see what happened, which means writing your own versions of things (e.g. a Provider
so you can access the store). That wasn’t great either.
Tech Stack 2.0
The only real change here was that I wanted to try using the npm version of redux directly.
I didn’t try especially hard here, but I ran into hurdles as well. If I remember correctly, there were still some typing issues (you’re still writing code in Kotlin).
It was also frustrating that I didn’t know how to do extremely basic tasks—like import the npm library I had added. I just didn’t know where the library was, so I didn’t know what the import statement should look like. IntelliJ also struggled to help me out with this.
Tech Stack 3.0
Next idea: let’s abandon Kotlin and instead use TypeScript for the frontend. We’ll still use Kotlin for the backend.
Also, we’ll create Kotlin shared libraries for data models, allowing some code reuse there.
This didn’t work either. I had similar issue to before, like not knowing how to import my shared library. However, there was a new dealbreaker as well.
To write Kotlin code that’s usable in a JS code base, you need to specify (module) exports. This is easy to do, just tag classes and stuff with @JsExport
. This tells Kotlin what should be made available.
The problem: enums aren’t exportable. This is understandably not simple, as enums in JS are not simple. However, JetBrains currently seems to see this as a minor issue, rather than a major language blocker that is destroying their JS offering.
To explain: you cannot export your enums as part of a JS library. This means that you cannot write your usual library—which almost certainly involves enums—and have it available in any platform. So, if you want to write a multiplatform library, you have to replace all your exported enums with a (bad) workaround. This results in some really non-idiomatic code and makes writing Kotlin unfun.
I’m not willing to use a workaround every time I need to write an enum, so this approach is a no-go for me.
Tech Stack 4.0
This brings me to my (somewhat unfortunate) position: give up on Kotlin/JS. This means reimplementing any needed models/interfaces in TypeScript.
This gives up on the multiplatform benefits, but should fully enable the node ecosystem (which is much more proven and not in alpha).
My hope is that Kotlin export issues are resolved, allowing me to migrate to using shared models. This influences my code organization—I keep my Kotlin and TypeScript code for a foo
library under $REPO/foo
.
If you take this approach as well, you may run into issues with Yarn (for me, yarn 2 / yarn berry). All of my Kotlin libraries were set up as multiplatform libraries with a shared configuration—they automatically got a JVM and a JS build. The JS build doesn’t play nice with my new approach, due to workspacing issues.
First, the Gradle task generates multiple nested workspaces with the same name. This confuses yarn.
Second, even if the Gralde task generates a correct workspace, its name conflicts with my JS implementation. An easy workaround is to rename one of the two, but that’s annoying as well.
My approach has to been to take out my Kotlin/JS build. I split my shared build config into a JVM and a JS common build, giving me greater control. (This is perhaps the obvious/natural solution, but I might’ve just been frustrated by all the hoops at this point.) If exports are fixed in the future, this will hopefully let me migrate my code with minimal overhead.
I suspect there aren’t too many people trying to make full Kotlin multiplatform projects. (Or, they understand all this stuff much better than me—in which case, please teach me!) If that’s you, I hope this saves you some headache!