Learning to Share With Kotlin Multi-Platform + Native

| Ellen Shapiro

If there's one thing that has remained consistent over the years, it's the desire of developers trying to avoid repeating themselves and businesses trying desperately to save money to figure out a way to have some kind of shared code between Android and iOS.

There have been a ton of ways to do this over the years, often using thin wrappers like PhoneGap, Appcellerator, and Titanium around web views that allow your web devs to build something that looks essentially like someone crowbared your website into an app.

More recently, this idea has evolved into React Native, which uses a much more robust bridge between JavaScript and native code to allow JavaScript code to manipulate native UI. It works considerably better than just throwing a web view into your app and calling it a day, but it can still have significant issues.

Most native mobile developers hate all of these frameworks with a flaming passion. They're difficult to debug, they're in a dynamic language that many of us don't know very well, and have really poor support for nullability handling. Languages like Swift and Kotlin handle all these things much better, eliminating entire classes of errors from our apps.

Why would we want to give up all that good stuff just to have a few things that work smoother across platforms? To which JetBrains, which created Kotlin, answered with their own question: What if you don't have to?

JetBrains has a lot of incentives to get Kotlin running anywhere they can. Their language is not tied to a specific platform the way something from Google (like Go) or Apple (like Swift) would be. And they realized that if they could come up with a better way to share code across platforms while still retaining type and null safety, they could have a huge, huge win.

Enter Kotlin/Native and Multi-Platform

With that in mind, a couple of years ago, they took a look at a compiler called LLVM, which took an intermediate bytecode and allowed it to be compiled to run on many different architectures. JetBrains realized that if they could generate LLVMIR bytecode from Kotlin, they could run Kotlin anyplace that machine code compiled from LLVMIR bytecode ran.

Not coincidentally, Swift outputs LLVMIR. LLVM was actually the Master's thesis project of Chris Lattner, who is the father of the Swift programming language. So if JetBrains could get Kotlin outputting LLVMIR, they could get it running on iOS in a way that would be very, very difficult for Apple to break.

Once JetBrains got their support for getting Kotlin code compiled down to LLVMIR (mostly) working, they announced Kotlin/Native. They also announced support for multi-platform projects, which would allow you to have JVM, Native, and Javascript code all written in Kotlin within the same project.

I was super-pumped when I read through all the theory, but the Alpha versions were rough. I couldn't even get existing code that allegedly worked out of the box to run after two days of fighting it.

Then last fall, JetBrains moved Kotlin/Native from Alpha to Beta, and the level of usability improved about a thousand fold. They added a much, much better getting started guide for multi-platform projects targeting iOS and Android, and I was able to get the starter tutorial working after a couple hours.

I took that and turned it into a little sample app I could play with to learn more about how the interop worked and how to solve some problems. Eventually, after seeing what became one of my favorite YouTube videos ever, Package Thief vs. Glitter Bomb Trap, I started working on something new.

I wanted to work with Kotlin across the board to create a box you could lock packages on in your porch, and lock and unlock the box remotely using an app on your phone. With a Multi-Platform project you can easily work with iOS, Android, a server, and a Raspberry pi. I started building out Porch Pirate Protector.

I've learned a lot from building this thing out over the last few months, and I've given a talk going into a ton of the details of how to build things with K/N and MPP as I've built it up.

Here, instead of rehashing all that, I wanted share a few questions you ask yourself before deciding to use this stuff in production.

How much patience do you have?

Working with any alpha or beta software always takes patience, but when you have multiple levels of "Wait, is this my code, the Kotlin/Native compiler, the Objective-C runtime, the operating system, or some combination of all of the above?", you are going to run into things which take some time to debug.

Now Kotlin is considerably easier for Swift developers to understand (and vice-versa) than Java and Objective-C were, and particularly more than JavaScript is, but there are still some weird sharp corners you can run into that can be really hard if you don't understand the full stack in both languages.

Particularly as Kotlin/Native code often needs things at the Gradle level which use the experimental (though now finalized and soon to be non-experimental) GRADLE_METADATA feature, you will spend an absolutely stupid amount of time fighting with things.

The fluctuating state of Coroutines and Ktor can also be massively annoying, and dealing with "Wait, why does the runtime think this object is being modified? I'm not touching anything!" bits can be a pain, and trying to set all this up in a way that doesn't create retain cycles is frankly something I'm still working on.

All these problems are considerably lower-level than most mobile developers spend their day dealing with, and this can lead to a great deal of frustration. You have to have the patience to not take your computer and chuck it directly into the nearest body of water as you work your way through them.

How much time do you have?

You can be as patient as you want, but time has a truly unfortunate habit of being linear. You only have so much time before you actually need to ship something - whether it's to beat a competitor to market, to make your client's deadline, or just to actually get something out the door.

If you're considering Kotlin/Native in production, hardcore deadlines should not be even remotely on your horizon, at least until K/N is out of Beta. Eventually as you get better at it and as the tooling and libraries improve, I foresee this being something that could save an enormous amount of time.

If you have a longer time-horizon and someone with strong Kotlin and iOS knowledge, it's a safer bet to move some portion of the app to it. But what portion? Ah, that's the hard question.

What should you actually be sharing?

This is one of the hardest questions to answer, partly because the result can be a bit of a moving target.

The most immediate answer is business logic: Anything that doesn't actually involve drawing things to the screen is much cleaner to test and centralize, because you don't have to care as much about how it's displayed.

For me, parsing, networking, and validation are three places where writing the same logic twice seems extremely silly, and certain business rules around models (for example "If a user has a first name and no last name, here's how you display that") can be the same across platforms.

But you might have some business logic on one platform that doesn't exist on the other: Each platform supports different specialized features, and you may have an idea for something on one platform that is not (presently) possible on the other platform. Should you add that to the shared code in the hopes that parity will eventually be achieved, or should you write it for Platform A and deal with porting it to multi-platform when Platform B actually supports that feature?

Frameworks like React Native and Flutter in particular encourage you to share both your UI and your business logic. But what part of your UI should remain the same, and what part of your UI (if any!) needs to be platform-specific?

Should you define all your colors and fonts semantically in your shared framework and use them from there? I found that to be very helpful, but you might not (especially since to get this playing super-nice with iOS and Android I wound up using some .kts scripts to auto-generate the colors.xml and Asset Catalog color objects).

Once you start combining things though, it gets more complicated very quickly. Do both platforms have the same characteristics in Dark Mode? Do both platforms have the same understanding of state for a pressed button? How does each platform deal with localization, and are you willing to write your own wrapper to do it yourself instead, at the risk of Doing It Wrong™?

I'm not providing answers to this question because, to be a real jerk programmer about it, it depends. You have to think through for your specific project what is worth combining and what isn't.

Some stuff that might seem annoying to combine will be way more helpful in the long run because you can ensure consistency in a way that just wasn't possible before. Some stuff that seems too easy to combine might be helpful because then it's only done once, and only has to be fixed once when it breaks.

But sometimes some thing that could initially seem the same across platforms may have nuances you don't grasp at first. At that point, it helps to break the task down even further: What do you have that's the same on both platforms, and where does it diverge?

What do you know you don't know?

Identifying potential rabbit holes that could eat your time and add up to risks which could derail your project is a really important step in making this decision.

You might be a kick-ass Android dev who knows Kotlin inside and out, but have you ever had to fight with iOS provisioning and signing? If you're trying to build a server, do you know how to set up a Docker instance that has a MySQL image? Do you know that the latest versions of MySQL play better with some ORMs than others? If you're working with a Raspberry pi, do you know how to provide it with enough power to run but not enough power to fry it?

When you identify these rabbit holes, you give yourself some opportunity to learn around them before you try to run straight into them with Kotlin. That helps narrow down the WTFs per minute you'll be dealing with, because you can learn more about the more standard operation of things before starting to bang your head on something experimental.

Excitement for the future, caution for now

I'm incredibly excited for the future of this stuff. I think K/N's lack of a shared UI framework is actually a great thing. It encourages you to keep UI as platform-specific logic, which is always, always going to be a better experience for the user.

It's still enough of a pain to set up and use that widespread adoption will likely wait until after Kotlin/Native is out of Beta, but there is definitely real use for these tools right now.

And frankly, it's real fun to work on this stuff. It's damned neat to see the same exact code running on multiple platforms, and it makes setting up a server a breeze to use the same framework and objects for JSON encoding and decoding on both the server and the client.

I would need to take all the above into account before I dove into a project using Kotlin/Native and multi-platform, but personally, I'm already up on the diving board, bouncing up and down and itching to jump in.