BT

InfoQ Homepage Presentations Kotlin: Write Once, Run (Actually) Everywhere

Kotlin: Write Once, Run (Actually) Everywhere

Bookmarks

Transcript

I am Jake [Wharton], I work at Google. I work on the Android team. And specifically about 18 months ago, Android actually announced Kotlin as a new supported language for the platform, which is why I'm there working on it. I'm going to introduce you to Kotlin. A lot of the talk will be framed contrasting it to Java and why the language exists. But really, the premise of this talk is actually about running Kotlin beyond just where it was originally designed to run.

So there's this effort called Kotlin Multi-Platform, which is what this talk is about. It's taking this language that was built for a very specific purpose, and the realization that actually it can run on more than one platform, and there's this sentence that comes from Java which is, “write once, run everywhere.” Kotlin is kind of taking that and delivering it in a different way, where it's not just the JVM runs everywhere and your Java code then runs everywhere the JVM runs. Kotlin is instead taking this and saying, "Your Kotlin code can run everywhere, but it can do it in a way that's much more intrinsic to each platform."

Kotlin?

I'd like to start with introducing the language itself. How many people have seen Kotlin at all before? Okay, so that's good. That means this won't be for naught. Kotlin is a language that was created by a company called JetBrains. JetBrains are primarily makers of IDEs of tools. And all these tools are mostly written in Java. The foundation of this suite of tools is called IntelliJ IDEA. There's a Community Edition which is open source and it has about three and a half million lines of Java code. That's just the Community Edition. There's an ultimate edition which is paid which has a bunch of extra functionality. And then all these other icons are IDEs for other languages which are built on the IntelliJ platform, so that same Java based platform. They then build language support and refactoring tools and all this stuff for PHP, for C.

So JetBrains, they're very aware of the pain points of the Java programming language. This is a company that's intimately familiar with the language. Three and a half million lines just in the base platform and then however many million builds that customize all these other languages. So they know Java well. And, jeez, at this point, seven or eight years ago, they started looking for ways- and the years ago context is really important- this is Java seven, eight years ago as well. And so they started looking at other languages to help improve their productivity and the tools that they built. Because this is JVM-based, they're limited to JVM-based languages. They looked at Groovy. They looked at Scala.

They just found that while these other languages had a lot of the things that they wanted, they also came with a lot of downsides, whether it's performance overhead, just complexity of the language. And they made a choice that is pretty bold, which is they're going to invent this new language. It's going to be compatible with Java. But since none of the other ones that were out at the time were able to solve the use cases that they wanted in the way that they wanted, and because they were a large enough company to invest these resources, they decided to build this new language called Kotlin.

I want to go over what the language looks like to just get you familiar with. I'll try and touch back on this over and over throughout the rest of the talk as to why knowing what the language looks like is important. It looks like almost any modern language you would expect. Now, like I said, this comes from people that had worked mostly with Java, but they're aware of the other programming languages that were coming into existence over the course of about seven to five years ago, which is when most of the language work was done. So we have here some properties which are defined. The very first keyword denotes whether something is read only or read write. So Val is a value, which means it has a value set, and then it cannot change. It's a strongly typed language. So everything must have a type that's known at compile time.

Then it has the concept of optionality elevated into the type system. If you're familiar with Java, this is one of the biggest pain points of the language that you can try and solve with things like annotations and static analysis or maybe the optional type which eventually came out in Java 8. But by having optionality in the type system, you're now forced to do things like null checks before you can dereference something. Oftentimes the type can be known, can be inferred from the context whether in this case, it's a literal, that's a string. Or maybe you're just calling a method and that method has a defined return type and so you can align it from the definition.

Now, because this was a language that was built from Java and needing to work with Java, it had to have a really strong interop story, similar to how Scala can talk to Java compiled code or Groovy can use Java compiled code, this language needed to do the same. They recognize certain patterns that are frequent in Java and quite verbose. One is the so called being pattern where you have this getter and a setter for a property and then there'll be some private field that backed it. And when you view this from Kotlin, what it's going to do is automatically recognize that pattern and collapse them down to a single property. So you get this much more terse representation of something that you can read from and it will call the getter or you write to and it calls the setter. There’s string interpolation, something Java doesn't have, to allow you to more easily create, mix and match string literals with variables. And then if you just want to print the toString of an object, you can just reference it like that.

What's cool about Kotlin is they not only decided to do Java interop in the way from Java to Kotlin, they wanted the opposite experience to be something that was also pleasant. So if you use a language like Scala, like Groovy, it has really good interop with code that's written in Java. But sometimes when you're writing in Java and interacting with a library that's written in Scala or Groovy, there's things that can look a little weird and kind of have to jump through some extra hoops because of how that more powerful language works that you can't express the same thing in Java.

But Kotlin wanted to try and avoid this wherever possible. If we do the opposite of what was on the last slide, we just define now a var property, which means it's both able to be read and written too, if you consume this from the Java side, it looks like that getter and setter, this standard being format. So you can call the getter or you can call the setter. Actually, when you compile Kotlin, if you're targeting the JVM, it compiles down to byte code. And so even if when you interact with this property through just Kotlin, it's actually going to compile down to calls to a get method or a set method.

I talked about the read only or read write. So if we define something that's a val and try to reassign its value, that's going to be a compile time error, whereas if we have something that's a variable, we have the ability to change it. Because JetBrains is a tools company, not only were they developing a language, but they also were able to develop the tooling alongside of it. And so in their IDEs, IntelliJ especially, IntelliJ IDEA, they can do a lot of things to highlight parts of the language to just make your code better. Mutability is often a point of where bugs are introduced. They actually will underline references that are immutable to just call out that this is something where you need to pay extra special care because mutability is in play.

Now, in order to reuse a lot of the things that were present on the JVM, they added something that's common in maybe C# which is extension functions, the ability to hang additional functionality off of a type that you don't control. Now this is not exactly the most useful of methods but given any date, you can now call in isTuesday function on it and it will run the body and it will look at the call site as if it's a member function, but you're not actually modifying the date class. If you do Java, this basically compiles to just a static method. In Java, you write a lot of static utility methods because you don't have this functionality and they're hard to discover. But by, again, having the IDE on Kotlin side, now if you just have a date and you hit the dot, it will actually autocomplete these extension methods and then import them automatically for you.

Again, going back to the two-way interoperability, if you want to call this from Java, it will actually show up as an extension method. If this is in a file called Date, Date.kt, which is the Kotlin extension, by default, the class name will be Date.kt, which is kind of awkward. But you can actually control this and make it whatever you want. So if you want to name it Dates or Date utils, you can have that idiomatic behavior from both languages. Then it's got features of modern languages that you would expect. Things like lambdas, the ability to just pass code as data. Function references, so instead of having a lambda that contains code, if you already have a function that contains code that you want to pass, you can just refer to it using this syntax.

Higher level functions and this is both an extension function and higher level function. So we're extending the functionality of list and we're adding this filter method. This is actually built in method in the Kotlin standard library, but we could write it if we want to. And so now anywhere someone has a list, they can call our filter method, passing a lambda and then we can, in the body of the method, do the obvious thing with the for loop. What's nice about how the higher order functions work is this is how you would maybe naively call it at the call site where you call the filter method; you open the parentheses because you're calling it as a function and then you pass in the lambda as the first argument. This is a pattern that shows up a lot, where there's a lambda that has a single argument.

So here we're just always operating on the items in the list. What you can actually do is not specify an argument name and just use the implicit it. But then the pattern of a lambda being the last argument in a function call is something that's quite common. And so what Kotlin actually did is allow you to move that lambda outside of the parentheses, which looks a little weird, but then because of the parentheses block is empty, you can actually eliminate that all together and get this nicer filter syntax, where you call it with just the lambda. It looks a little weird. If you haven't seen this before, this is your first time seeing the language, but again, being a language that's statically typed and having the IDE on your side, there's nothing that can be inferred incorrectly here.

Then this still works if there are other arguments. So there's overload of filter called Filter To, which allows you to pass in the destination list that you want to filter your list into. So you can see here we have the parentheses and we're passing that list, but then that lambda still stays outside of the parentheses. There's a cool function. In the JVM, you rely on its runtime compilers, its just in time or maybe ahead of time compilers, to do certain optimizations for you. Like inlining, you have a function that's called over and over and over again, in a very hot path. You rely on the JIT to recognize that and then inline it to the call site so you don't have to be constantly packing and unpacking arguments and actually jumping to that separate piece of code.

Kotlin actually allows you to do this at compile time. If you know something is going to be called either in a hot path or the overhead of creating the arguments is a certain way where you want to eliminate that. So for this filter, you might be wondering why would you ever inline this function? And that's because if you've done filtering in a language without higher level functions like this, such as Java before Java 8, you would have to write. You have to create a new local property that's like an array list. You have to write the for loop over the current things. You would call the if statement. Your predicate would be inside the if conditional whether you wanted to keep an item or not. And then you would add it to the destination list.

Now what this is doing is saying you can have the ergonomics of the higher order function, where it's much more declarative and all you see at the filter site is the lambda. But the way this works is that lambda has to live somewhere. And so there's usually an object created for that, and that object if you're targeting the JVM, has a class associated with it. By inlining this function, you actually eliminate the need to have a class generated for what the lambda implements and then at runtime, that object implemented. In the byte code, you get what you otherwise would have written if you had done it manually. And so you lose the overhead of both the jumping to the call site, but also the overhead associated with the lambda. Something that's important for context if you're running an Android where things like performance, especially on the main thread is the very key.

Then since this pattern, this pattern of just having classes that carry data is somewhat common, it's essentially ubiquitous, I guess, there's actually a shorthand for...I mean, this is a terrible user because it's always named Jake. But if you wanted to take in a name, this is what you would write, because the pattern of creating an object that's like a record type that just carries data is common, Kotlin allows you to collapse the property and the constructor argument into one definition, and then you can actually just drop the curly braces because they are no longer needed.

If you use this though, you might be a little surprised. I want to create myself again and I want to print that out. Again, you're running on the JVM and I promise I'm going to get to multiplatform stuff eventually. It'll do the default Java thing where it just prints the memory address, which is useless because you need to override toString. But instead of having to override toString or also the other stuff that Java forces you to do for value-based types, which is like equals and hash code if you want to use them in a map, you can actually just use a modifier on the class called data, which communicates in the compiler that, listen, this is just a dumb carrier of other references. And I want you to generate that boilerplate automatically for equals hash code toString and now we get a toString that's actually saying.

Almost done with the language here. Another cool feature of these data classes is when you have multiple properties. Very frequently, you'll see these returned from a function. Then you need to access the component bits of that type. You need to pull out the name, you need to pull out the age and then use them somewhere else. There's this feature called destructuring, which is in a couple other languages. You may have seen it in Python, where you can actually just pull out the component types using parentheses. You don't need a separate reference for this. I mean, this is kind of stupid. I'm just creating an object and then destructuring it. Where you usually see this is when it's returned from a function. Function returns a point and pull out X and Y or user and you pull out name and age.

Two more language features before we move on. This again is in the context of Android but it applies to everything. We may have a property where it's expensive to initialize. And so in this case, I'm creating a property that actually talks to the database and creates a compiled statement, which is like I'm going to execute this SQL query over and over again. So I want you to compile it to a more efficient representation. But the problem is this happens eagerly. As soon as I create this class, the very first time I create this class, it goes and does IO in the constructor instead of, say, the first time I call the Delete method.

There are patterns to avoid this. You can have a lazy pattern where maybe it's nullable and then you check if it's null and initialize it the first time. Kotlin has this concept called delegated properties where you basically say I have a certain behavior that I want to apply to a property and every time I call get on the property, it will call in to this delegate object that has certain behavior. One of the ones that's built in is a lazy delegate. You give the lazy delegate a lambda. And what this does is when you call get, it does the annoying lazy pattern where it checks if it's null. If it's not null, it takes a lock, calls the lambda, gets the return value, sets that to its field, and then returns that to you. And then the next time you call get, it checks its field that's already non-null and it just returns it to you over and over again.

So that lazy pattern, the pattern of lazy initialization is something that you can repeat over and over and over again. But what proper delegates allow you to do is encapsulate that behavior in a class, and then delegate your property behavior to that class. That lazy is a function that returns this property delegate class. And so that function then takes this lambda. There's a bunch of these property delegates. Some of them are built in and you can write ones yourself. So an example would be, maybe you have a side effect you want to do every time a property value changes. You want to log for debugging or for analytics purposes, whatever. You can have a lambda called back every time this property has changed. The consumer, so the person consuming this name property, they're just reading and writing a string property. They don't see the delegate. But then you have this behavior attached to it automatically.

Because nullability is a concept in the type system, sometimes you know a property will never be null, but the value isn't immediately accessible. It has to be initialized just after an object is created. So there's a delegate that basically allows a property to have a non-null type, but if you call get on it before someone has actually set a value, it'll throw an exception. This is nice because then it doesn't force the consumer to always do that null check. It encapsulates it inside the property itself. Then you can write your own. So, like I said, I work on Android, Android has this view system and so you can write delegate that little, go out and bind a view given a certain ID to a certain type. And then you never know this, you just interact with the property, the name view, and it will lazily go out and find the view the first time you call it.

The last language feature, something that's really new, just became stable actually a week ago, is this concept called Coroutines, which a lot of times they're described as lightweight threads. So this piece of code, it's actually old piece of code, it's a really good example they like to show, JetBrains likes to show, where they launch 100,000 Coroutines that all sleep for one second and then print a dot. And so if you try and do this with threads, launch 100,000 threads, that's not going to work. You're going to get out of memory, especially if you do this on the JVM. But with Coroutines, this really just creates 100,000 objects. Then there's a single thread with a looper that basically is just waiting one second until all these delays are up and then it just runs through the 100,000 objects and calls the print method. So this is actually one thread, just 100,000 objects that are really lightweight compared to something like threads, which are very heavyweight.

Targeted Platforms

That was just kind of a quick whirlwind tour of a bunch of features of the language. And I'm going to try and reference that over and over again of why Kotlin is something that started for the JVM, and then moved outside of that space. So when I think about something like multiplatform and running everywhere, the modern version of this is really three platforms, which is native, which I'm representing with iOS, but this could be native running on a microcontroller or your desktop or whatever. Then you have JVM Java byte code, which I'm representing with Android, but it could just be running on your backend server. And then the web which I'm representing just with a Chrome icon, but obviously all web browsers, or even potentially a JavaScript engine that's not a browser, Node.js, or maybe just an embedded Ben and JavaScript engine and another application.

But really, it's the targeted platforms that are important. Java byte code, JavaScript, and native. That gives you everywhere. And the important thing is you can get everywhere with just native; native code can run everywhere. You can do native code on Android, you could do native code on desktop, you could do native code in the browser with WebAssembly. But these aren't inherently intrinsic to each of these platforms. If you write a native application for Android, it's very different than how a normal application is built with the managed byte code and the SDK they provide. And for the browser, WebAssembly runs in certain contexts, but can't do stuff like interacting with the DOM, the actual JavaScript that traditionally we associate with the browser.

Kotlin has a notion of platform independent and platform dependent Kotlin. What it does is when we use Kotlin, traditionally we had been talking about Kotlin targeting the JVM, whether this was for backend servers, desktop applications, or Android. But over the last couple years, JetBrains has come to realize, and if you think back to all the language features that I've walked through, there's nothing specific about the JVM in the language itself. It's just features of a modern language, features that we would expect of any language and there's nothing that really prevents that from targeting backend, like a compilation backend different from Java byte code.

So the JetBrains started with JavaScript about two, two and a half years ago at this point where you could compile platform independent Kotlin, which I'll talk more about what that means later on. You can compile that directly to JavaScript. Then over the last year, year and a half or so, they've targeted Native as well. So you can now compile this platform independent Kotlin to Native. But then there's also platform dependent Kotlin, so if I'm writing Kotlin for Android, I need to be able to reference Android types. If I'm running Kotlin for JavaScript, maybe I need to reference types from the DOM. Or if I'm running code for Native, I need to reference types from libcurl or any data library that I'm interacting with.

When you use Kotlin in a multiplatform context, the way that it's easy to get started with is to separate this out from the rest of your application where you have, say, a library that you know is by the business logic or just some encapsulated piece of code that you could then write in Kotlin compiled each of these backends and then consume it as a library. If this is Android or Java, consume it as just a jar. If it’s JavaScript, you just consume it through NPM or whatever. You don't even need to know that it was written in Kotlin. If its native, you just consume essentially the header, and then the compiled binary. And the rest of your app gets to be written in the thing you're already writing it in, C, C++, Go, Rust, Swift, JavaScript TypeScript, or Java Scala, Groovy, or even Kotlin, if you're targeting Java backend.

But this is a very easy way to introduce it into your app but you don't have to do this. If you want to have Kotlin everywhere, you could have just your app be this whole multiplatform abstraction where the majority of it is Kotlin and there's very specific parts that are platform-specific. I want to go through an example of what this actually would look like in order to motivate it a little bit more. I'm going to use a concrete example, which is a game. We're going to build a tiny little game, which is Tic-Tac-Toe. And we're going to have it run on four platforms: Android through the Java backend, iOS through Native backend, the web through the JavaScript backend, and then the server can be through any backend. It could be running Java and using the Java byte code, could be Node.js pulling in JavaScript, or C, C++, Go, Rust, whatever, through the native backend.

But I'm an Android developer, so the way that I usually teach Kotlin is by starting through Java and then working my way to the other platforms. So if I've written this game, say, I just have written the model here for representing the game and I've written it in Java. So I have, whether something is an extra 0, I have the board which is just a two-dimensional array of the cells and whether they're marked or not. The player has a name and whether they've assigned the X to the O. You don't need to really pay attention to what's going on here. A state machine, these are the states of the state machine. It's either one of the two players' moves. One of the two players has won or it was a draw, nobody won. Then you have the game which encapsulates all this. It has a state machine and has the two players and the board.

I've been hammering on the Kotlin interrupt story between Kotlin and Java. And it's something JetBrains has certainly been talking about. So we have these objects and these objects are connected in a tiny little dependency graph. Maybe you just go and grab the player class and say, "Well, this looks like that. This is just a dumb carrier object of two properties. It's the name of the person and the name of the player and the whether they're an X or an O." I have to write this equals hash code and toString and I just talked about how this data classes allow you to create record like types. This is a good candidate for turning into a Kotlin data class. Now I get to show my boss that I replaced 150 lines of Java boilerplate with just one line of code. Now I'm more efficient as a programmer.

But this is a little weird because what we've done is actually created a cyclic dependency between two languages. We have our mark class, which is written in Java, which was the extra O enum. We have our player class, which then has to hold on to a mark reference for each player. And then our game board has to hold onto two-player instances. So we have Java referencing Kotlin and then Kotlin referencing back into Java. This is one like compilation unit. All this code is going to be compiled in one module. This is our model.

But this actually works even though it seems like it shouldn't. This cyclic thing shouldn't work because either the Kotlin compiler runs first which compiles the Java byte code, then the Java compiler runs which pulls in that Java byte code as a library and compiles a Java or the opposite has to happen. Java compiler runs and spits out Java byte code and then the Kotlin compiler runs and eats the Java byte code and spits out more Java byte code for the Kotlin. So it doesn't seem like this should work but it does work.

Again, it's not even just about having the interoperability between the two languages. It's about making the tool chain work in a way that's conducive to you to migrate into this language. Because otherwise, we don't need this language, right? There's enough languages. I mean, every now and then, a new language is invented and it comes with a very specific purpose. But another language on the JVM, or at least that started on the JVM, doesn't seem like something we need. And so it's really important to have this really good interoperability story both in the language but also in the tool chain.

The way this actually works is that the Kotlin compiler is both a Kotlin compiler and a Java compiler, except it doesn't actually compile the Java code the whole way. It goes 50% of the way just so that it can read enough to understand what will be compiled by the Java compiler. And so, you feed both things, are four Java classes and are one Kotlin class into the Kotlin compiler. It compiles only the Kotlin class using what it knows from the Java classes that will be compiled later. Then the Java compiler runs after the Kotlin compiler and it takes in the class file, the Java byte code class file of just the one Kotlin class and then our other four Java classes get compiled again except this time they get compiled the whole way. And so they spit out the four class files for Java code and then the two-byte code outputs are combined. That's how we get this cyclic dependency to work. And then the center library or any other libraries that you're using, any other Java libraries, are just available on both class paths, because they are made available to any downstream consumer. And if you want, like I said, this can all just be encapsulated in a module and nobody downstream of you has to know you've written in Kotlin.

Building Model

I've just been talking about this hypothetical model of this game and only for Android, I guess, for the Java backend. But if we take a step back and look at what all is required for building something even as simple as just a Tic-Tac-Toe game, where we actually want to host it and deploy it. We have our mobile clients where they’re pretty much going to be the same thing. They're going to have view models that represent what's being displayed on screen. Maybe there are presenters which deal with talking to the network backend. And then with the web, the web as a different client probably renders a little bit differently, at least a more sophisticated app would, but there's still backend infrastructure that you could share, which is how you talk to the server. Then you have the server API which interacts with the rest of these, where they all share the business logic which is maybe like the state machine of the game and then the models which was what we were just talking about.

So we've really only done one thing here. We've just converted the most basic part of our applications to Kotlin. Can we do the rest and how does that work? If we look at just the mobile clients, the view models, these are also just DOM data carriers so we can just write these in Kotlin. Because they're platform independent, which again, unfortunately, I will only just distinguish between platform independent and platform dependent a little more deeply at the end, but there's no references to a platform here. We're not referencing iOS, a UI view controller or an Android view or web, anything in the DOM. These are just DOM objects, they're trivially compiled to every platform.

Presenters are also very similar except a presenter has to talk to, say, Persistence. So maybe, our games are being saved locally. We're going to take that in as a reference and then we query like the game store. So this is the new game screen, where we show how many games you've won. Maybe we query for the totals, and then we just return that DOM object. And then when you're actually in a game, that takes in the ID of the game you're playing. The game stores that, it can query the game information using what looks like a type code observable here, because the game state's going to update over time and your UI has to update them. When the game state changes, an observable is just a way to push updates to a model but nothing is platform specific here. Then interactions from the UI come from here, or if you liked Redux, you've done the web stuff, you can have states coming in and states going out and you're just a state machine but this is all just pure Kotlin code again.

It becomes really easy to see that each of these layers don't have anything specific. This is a super DOM example of Tic-Tac-Toe. You could probably just reuse these models. More complex product, you'd probably split these out. But the client backend is where things get a little more interesting. I've just been talking about DOM records that carry data and pure Kotlin logic and the presenters. The game store that I referenced is Persistence, and Persistence is something very specific to each platform.

Now I can get into something where there's the difference between platform independent and platform dependent Kotlin. We start with an interface. I didn't talk about the suspend keyword, that's another Coroutines thing. It basically means the function is asynchronous and so it can only be called in certain contexts. But you can think of it as a function that has a callback under the hood, but you don't program like it has a callback; you program in this nice imperative flow. That's because this is going to hit a database or the file system in order to look up these totals for displaying the overview.

And then we have the current state of a particular game. Then the ability to actually record a move which we have to talk, we have to call out to the server and to ask it to play this move. Also asynchronous operation. So this is just an interface, but we need this logic of how this is actually implemented. The interface is just, again, boring Kotlin, but the implementations of that have to be platform specific on Android. We want maybe a database on iOS CoreData, on the web, maybe like local storage.

This is where we start getting into how we differentiate between platform specific and platform agnostic. Kotlin, it doesn't look that different. For the Android one, the SQL like database type is Android ACK, the type in the Android ACK only available to Android platforms. What would have to happen is now we're writing code that's platform specific and these would have to be in- it's not quite separate compilation units, but you sort of compile the same project multiple times and then mix and match which source files are contributing to it. So the interface, you could have in a library but then you start getting just tons of modules, the only purpose is multi-platform.

What you really want is just the interface and the implementation and one representation of a module. The interface and this platform specific implementation would be compiled by Kotlin C and Java C in one module and then we get to iOS. It's still the same module but it's a separate compilation. Here we reference CoreData, and I'm not showing implementations because they don't really matter here but you're talking to the actual native platform and you're doing that from Kotlin. We're going to look at how you can do that from different languages in a second.

Because Kotlin was built to start interacting with the JVM as its primary platform that it runs on, the interop there is very strong, and the way Java works is there's no real header files. You just kind of infer the headers from the jar itself. The jar which has the Java byte code contains a representation of what APIs you can call. Native, that's a little bit different and so for something like calling into CoreData, you would need the headers that you're actually able to link against.

The Kotlin native compiler allows you to specify which headers you want to be made available as Kotlin API. We'll look at this a little bit more later, but basically you give it a header and it creates this almost like stubs in pure Kotlin that you can then interact with. If you only know Kotlin and want to write something targeting native like iOS, you don't actually have to interact with something like Swift or Objective C. You can allow Kotlin to synthesize stubs in the Kotlin language that you only interact with. And then JavaScript to be used like local storage.

What's interesting here is that Kotlin’s standard library is something that also has platform agnostic and platform specific versions. So there are APIs that you can call in that common code like the interface or the code that's just like the view models. You have a list, you have primitives like string and integers. But if you're compiling to a platform specific standard library, you're compiling against the platform specific standard library. You have types that are available in only JavaScript, like the DOM storage, DOM storage APIs. They're, again, like stubs. They don't actually exist as implementations. When you compile this code to JavaScript, which we'll see in a couple more slides, that actually just calls into the JavaScript APIs directly.

This is one mechanism that allows you to have both the platform agnostic client backend, which is the concept of the storage and then platform specific implementations that are specific to Android, iOS, and the web. Then business logic is just boring. It's a bunch of just state machine code which you don't need to look at and that can be in Kotlin and then everything becomes Kotlin multi-platform. And then all that's left, if we do this hypothetical thing and we go like full Kotlin or whatever, all that's left is the view part, the rendering part, which is platform specific. An Android in Kotlin or Java, you can write Android's version of views. In iOS, this is Swift. So Kotlin types are made available in Swift and Objective C. This UI model which came from the presenter that was written in Kotlin, you can use in Swift. JavaScript, because it's not typed, it doesn't really matter. You can just write the JavaScript as if the types are there. I believe it will generate TypeScript definitions from Kotlin. In a second, I'll show a slide. It actually does the opposite.

Then in Java or whatever, this is JAX-RS whatever it's called for writing out a Java backend. This is a very pretty picture where you have all of your Kotlin code. You're using Kotlin because maybe you're just the small shop or an individual developer and you have the ability to target all three of these platforms where there's a lot of ways people can get into these platforms. That no matter how people get in, it's all the same code and so you don't have to worry about the fact that this person is using a Chromebook, and Chromebook has installable Chrome apps, they're just websites that you can install. Or Android has this thing called Instant Apps, which is maybe they visit a website and just in time, installs, and launches an app, or iOS is getting support or Mac OS is getting support for native apps next year. So your iOS app can now run on the desktop. And because it's all the same code, it doesn't matter how the user gets into your system. Everything is just the same because it's all written in the same code base.

How the Implementations Work

I want to talk very, very quickly about how each of these implementations work. I talked about Java a lot, that Java is the thing that I know, the JVM and the Java C compilation is the thing I know the best. I already showed how that worked. For JavaScript, the way that APIs from JavaScript are generally exposed is through stubs that you can write yourself, or that you can have generate automatically. I work on something that's a Chrome extension written in Kotlin. I've defined Chrome's APIs using interfaces, stubs with this external annotation and then an implementation of that which is a property that is the route, how you get to that object called Chrome. I've named it how you would name it in Kotlin, but then I can tell it how it's actually named in JavaScript.

Now if there were TypeScript definitions for the Chrome APIs, which there might be, I haven't actually looked, there's a tool which can take any TypeScript definition and generate these stubs for you automatically. If you're pulling independencies that have TypeScript definitions, you don't have to write any of this yourself. What's nice is when this actually gets compiled, so this is a line of code from the Chrome extension where I'm creating an object and I'm passing in reference to Chrome storage, and I'm using the Kotlin C property reference. When this actually gets compiled to Kotlin, it looks like what you have written if you wrote the JavaScript directly which is the chrome reference. And you see, the other code kind of got mangled a little bit because of how the Kotlin compiler represents stuff in JavaScript.

But it's not just pure JavaScript. It's JavaScript that you would have written had you written it directly. It's not exactly idiomatic, but it's nice enough that if you wanted to, you could interact with it from other JavaScript or other TypeScript code. Then Native, which is the most interesting one, this emerging platform is the newest. I showed earlier about how we had the CoreData thing and we pass in the headers. The way this actually works is you give it a package name and it works similar to the TypeScript to Kotlin tool where it's going to take in the headers, read them, and then turn them into Kotlin specific stubs in this package. Here you would have an import. If my game_store.h had a CoreDataGameStore type, I would just import that as if it was something in Kotlin. And I never really have to know that it was written in another language.

The way that this compiler works is quite similar to how the Java compiler works, where the header files come in into the native compiler which is called Conan, and then it spits out LLVM IR. So this is how it essentially was free- not free because there's work being done- but the ability to target every platform that LLVM targets came for free. This is what gets you running on iOS, Mac OS, Windows, Linux, WebAssembly. Then LLVM takes in the headers as well and it spits out the platform specific machine code that can run on all those platforms. The hard work here is just translating Kotlin into LLVM IR and then everything else, all the optimizations and stuff basically come for free.

ACK Search

I'm skipping a section because I'm out of time. I work on a project called ACK Search. It's a little side project just so that I can mess around with this kind of stuff. It's an Android app and a Chrome extension. This pattern that I have implemented or that I have talked about in the Tic-Tac-Toe, you can kind of see here where the orange is the Kotlin platform agnostic stuff. And then there are front-ends for Android and for the web. The way this manifests is that now I essentially just write something once and it runs on Android, and then when you're in Chrome, it's a very different user interface which is just the omnibox in Chrome. And so very, very different front-ends but same exact backends. I don't do iOS, it's an Android app, so I don't do iOS. But the Kotlin Conference App that JetBrains hosts every year, they've written their app in this entire model, where the Android and iOS app share essentially all their code.

What I really wanted to convey here is that the language didn't start out as something that was intended to run everywhere, but because it's just a language that has been built with features that any modern language could have and that it becomes then, I don't want to say easy, but it becomes almost obvious that targeting more than Java was something that was going to happen, and by doing only JavaScript and LLVM and everything it provides, you essentially get every platform. Java by code, JavaScript, and Native means you're going to run everywhere.

But also you run in a way that's intrinsic to the platform. You're not running native code in an Android app and having to do jumping through a bunch of hoops to interface, or even native code running on the web, where you basically only have WebAssembly as your target, which is useful for certain things but if you're doing visual stuff, the actual UI that the user interacts with, it's not the thing that you want to have, at least not yet. So by targeting JavaScript, you get the ability to get the things that are intrinsic to that platform.

That's all I have. I have run a tiny bit over time, but I will be happy to stick around and answer any questions you guys have. Thank you.

 

See more presentations with transcripts

 

Recorded at:

Mar 07, 2019

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

  • It's a shame he didn't mention Codename One which supports Kotlin

    by Shai Almog /

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Codename One is an Open Source Java/Kotlin WORA solution that supports: iOS, Android, Web & Desktop. Unlike the approach taken by Kotlin Native we tool an approach of full WORA similar to Swing's approach of lightweight component but with enhancements (lightweight/heavyweight mixing builtin).
    Since it's built on top of our open source JVM solutions you get the benefits of full access to Java from Kotlin in case you need to integrate Java code. It's also "battle tested" as it's been around much longer.

  • Where can I get the source codes?

    by Bai Hantsy /

    Your message is awaiting moderation. Thank you for participating in the discussion.

    It is very helpful to understand Kotlin for crossing platform development. Hope there is somewhere to get the source codes to play on my local system.

  • Kotlin on ARMv6 (Raspberry Pi Zero)

    by L. von Leeuwen /

    Your message is awaiting moderation. Thank you for participating in the discussion.

    Will Kotlin run on the ARMv6 architecture of the Raspberry pi Zero? Now that the JVM dropped this platform as of Java 9+, the code base is of the JVM is not maintained for ARMv6 any more. None of the recent Java (9+) versions run on it any more. Java is dead in the water for ARMv6 / the internet of things, does Kotlin fill in this gap or is it also constrained from running on AMRv6 because of its JVM dependency?

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT

Is your profile up-to-date? Please take a moment to review and update.

Note: If updating/changing your email, a validation request will be sent

Company name:
Company role:
Company size:
Country/Zone:
State/Province/Region:
You will be sent an email to validate the new email address. This pop-up will close itself in a few moments.