Kotlin logo

I assume that readers are already familiar with the Kotlin language. Advertised from the site:

Statically typed programming language for the JVM, Android and the browser 100% interoperable with Java™

— Headline from the Kotlin website

Kotlin first penetrated the Android ecosystem and has seen a huge adoption there. There’s also a growing trend on the JVM, via Spring Boot. Since its latest 1.1 release, Kotlin also offers a production-grade Kotlin to JavaScript transpiler. This post is dedicated to the latter.

IMHO, the biggest issue regarding the process of transpiling Kotlin to JavaScript is that documentation is only aimed at server-side build tools just as Maven and Gradle - or worse, the IntelliJ IDEA IDE. Those work very well for backend-driven projects or prototypes, but is not an incentive to front-end developers whose bread and butter are npm, Grunt/Gulp, yarn, etc.

Let’s fix that by creating a simple Google Maps using Kotlin.

Managing the build file

This section assumes:

  • The Grunt command-line has already been installed, via npm install -g grunt-cli
  • The kotlin package has been installed and the kotlin compiler is on the path. I personally use Homebrew - brew install kotlin

The build file looks like the following:

Gruntfile.js
module.exports = function (grunt) {

    grunt.initConfig({
        clean: 'dist/**',
        exec: {
            cmd: 'kotlinc-js src -output dist/script.js' (1)
        },
        copy: { (2)
            files: {
                expand: true,
                flatten: true,
                src: ['src/**/*.html', 'src/**/*.css'],
                dest: 'dist'
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-exec');

    grunt.registerTask('default', ['exec', 'copy']);
};
1 Transpiles all Kotlin files found in the src folder to a single JavaScript file
2 Copies CSS and HTML files to the dist folder

Bridging Google Maps API in Kotlin

The following snippet creates a Map object using Google Maps API which will be displayed on a specific div element:

var element = document.getElementById('map');
new google.maps.Map(element, {
        center: { lat: 46.2050836, lng: 6.1090691 },
    zoom: 8
});

Like in TypeScript, there must be a thin Kotlin adapter to bridge the original JavaScript API. Unlike in TypeScript, there’s no existing repository of such adapters. The following is a naive first draft:

external class Map(element: Element?, options: Json?)
The external keyword is used to tell the transpiler the function body is implemented in another file - or library in that case.

The first issue comes from a name collision: Map is an existing Kotlin class and is imported by default. Fortunately, the @JsName annotation allows to translate the name at transpile time.

gmaps.kt
@JsName("Map")
external class GoogleMap(element: Element?, options: Json?)

The second issue occurs because the original API is in a namespace: the object is not Map, but google.maps.Map. The previous annotation doesn’t allow for dots, but a combination of other annotations can do the trick:

/google/maps/gmaps.kt
@JsModule("google")
@JsQualifier("maps")
@JsName("Map")
external class GoogleMap(element: Element?, options: Json?)

This still doesn’t work - it doesn’t even compile, as @JsQualifier cannot be applied to a class. The final working code is:

/google/maps/gmaps.kt
@file:JsModule("google")
@file:JsQualifier("maps")
@file:JsNonModule

package google.maps

@JsName("Map")
external class GoogleMap(element: Element?, options: Json?)

Calling Google Maps

Calling the above code is quite straightforward, but note the second parameter of the constructor is of type Json. That for sure is quite different from the strongly-typed code which was the goal of using Kotlin. To address that, let’s create real types:

internal class MapOptions(val center: LatitudeLongitude, val zoom: Byte)
internal class LatitudeLongitude(val latitude: Double, val longitude: Double)

And with Kotlin’s extension function - and an out-of-the-box json() function, let’s make them able to serialize themselves to JSON:

internal fun LatitudeLongitude.toJson() = json("lat" to latitude, "lng" to longitude)
internal fun MapOptions.toJson() = json("center" to center.toJson(), "zoom" to zoom)

This makes it possible to write the following:

fun initMap() {
    val div = document.getElementById("map")
    val latLng = LatitudeLongitude(latitude = -34.397, longitude = 150.644)
    val options = MapOptions(center = latLng, zoom = 8)
    GoogleMap(element = div, options = options.toJson())
}

Refinements

We could stop at this point, with the feeling to have achieved something. But Kotlin allows much more.

The low hanging fruit would be to move the JSON serialization to the Map constructor:

internal class KotlinGoogleMap(element: Element?, options: MapOptions) : GoogleMap(element, options.toJson())

KotlinGoogleMap(element = div, options = options)

Further refinements

The "domain" is quite suited to be written using a DSL. Let’s update the "API":

external open class GoogleMap(element: Element?) {
    fun setOptions(options: Json)
}

internal class MapOptions {
    lateinit var center: LatitudeLongitude
    var zoom: Byte = 1
    fun center(init: LatitudeLongitude.() -> Unit) {
        center = LatitudeLongitude().apply(init)
    }
    fun toJson() = json("center" to center.toJson(), "zoom" to zoom)
}

internal class LatitudeLongitude() {
    var latitude: Double = 0.0
    var longitude: Double = 0.0
    fun toJson() = json("lat" to latitude, "lng" to longitude)
}

internal class KotlinGoogleMap(element: Element?) : GoogleMap(element) {
    fun options(init: MapOptions.() -> Unit) {
        val options = MapOptions().apply(init)
        setOptions(options = options.toJson())
    }
}

internal fun kotlinGoogleMap(element: Element?, init: KotlinGoogleMap.() -> Unit) = KotlinGoogleMap(element).apply(init)

The client code now can be written as:

fun initMap() {
    val div = document.getElementById("map")
    kotlinGoogleMap(div) {
        options {
            zoom = 6
            center {
                latitude = 46.2050836
                longitude = 6.1090691
            }
        }
    }
}

Conclusion

Though the documentation is rather terse, it’s possible to only use the JavaScript ecosystem to transpile Kotlin code to JavaScript. Granted, the bridging of existing libraries is a chore, but this is only a one-time effort as the community starts sharing their efforts. On the other hand, the same features that make Kotlin a great language to use server-side - e.g. writing a DSL, also benefit on the front-end.

The complete source code for this post can be found on Github.