A practical Intro to Kotlin Multiplatform

Jamie Lynch

Kotlin has enjoyed an explosion in popularity ever since Google announced first-class support for the language on Android, and Spring Boot 2 offered Kotlin support.

You’d be forgiven for thinking that Kotlin only runs on the JVM, but that’s no longer true. Kotlin Multiplatform is an experimental language feature that allows you to run Kotlin in JavaScript, iOS, and native desktop applications, to name but a few. And best of all, it’s possible to share code between all these targets, reducing the amount of time required for development.

This blog post will explore the current state of Kotlin Multiplatform by building a simple app that runs on Android, iOS, Browser JS, Java Desktop, and Spring Boot. Maybe in a few years, Kotlin will be a popular choice on all these platforms as well.

The basics of Kotlin Multiplatform

Before we start building an app, we should take the time to understand the basics of Kotlin Multiplatform. If you’re unfamiliar with Kotlin generally, we’d recommend visiting the Kotlin website or reading our previous blog post.

Common code

To share Kotlin code between platforms, we’ll create a common module that has a dependency on the Kotlin standard library. For each platform we’ll support we need to create a separate module that depends on the common module and the appropriate Kotlin language dependency.

For example, a JavaScript app would depend on the kotlin-stdlib-jslibrary, and also a common module with a dependency on kotlin-stdlib-common. The Kotlin docs provide a great guide for setting up a multiplatform project with Gradle, so we’ll skip over this aspect from now on.

After our project is setup, we can write some simple code in the common module:

### //declared within a common module
fun
calculateFoo() = 2 + 2

Because we added the common module as a dependency, we can now call the common code from within each of our platform modules:


### //declared within an android application module
overridefun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
   Logger.log(calculateFoo()) ### //print '4' to Android Logcat
}

This is great for sharing code between different platforms, but what happens if we need to access platform-specific APIs, such as File APIs on Java, or the DOM on JavaScript? The answer is to create platform-specific declarations.

Platform-specific declarations

In our common module, we can use the expect keyword to declare that a method will be implemented separately on each platform. We can then call it just like a regular Kotlin function:

expect fun logMessage(msg: String)

fun runLongTask() {
###// ...
   logMessage("Long task complete")
}

For our applications to compile, we’ll need to provide a separate implementation for each platform. These use the actual keyword, and handle the message in an appropriate manner, by using Kotlin bindings of platform-specific APIs:


### //JVM
actual fun logMessage(msg: String) {
   println(msg) ### //print the message to System.out
}

### // Android
actual fun logMessage(msg: String) {
   Log.d("AndroidApp", msg) ### //log the message to logcat
}

### // JS
actual fun logMessage(msg: String) {
   console.log(msg) // log the message to the browser console
}

### // iOS
actual fun logMessage(msg: Sring) {
   NSLog(msg) ### // log the message using NSLog
}

The expect and actual keywords can also be applied to classes and other language features, making them pretty powerful as they allow us to access useful platform specific functionality from common code. Now that we’ve covered the basics, lets start by writing an Android app.

Writing an Android App with Kotlin Multiplatform

Writing common Kotlin code

The simple example app we’ll use throughout will display a scrollable list of programming languages, and state useful information when each item is clicked. The end result looks like this:

Android app built with Kotlin Multiplatform

We’ll start by adding code to our common module. To demonstrate platform-specific declarations, we’ll create a function that returns the platform name:

expect fun platformMessage(): String // declared in the common sourceSet
actual fun platformMessage() = "Hello, Kotlin JVM" // implemented in the JVM sourceSet

We also need a class that models information about a programming language. We’ll call this LanguageFact, and define it as a data class as its sole purpose is to hold information. We’ll also create a LanguageFactRepository that fetches a collection of programming languages we’re interested in:

data class LanguageFact(val name: String, val description: String)

class LanguageFactRepository {
  ### // in a real app, this might call a REST API
   fun fetchLanguageFacts() = listOf(
       LanguageFact("Kotlin", "A modern programming language that runs on multiple platforms")
   )
}

Writing Android specific code

We can now focus on the platform-specific UI. For Android we’ll create a single Activity with a RecyclerView and a binding Adapter. This is pretty standard stuff, so let’s focus on where we interact with the multiplatform code:

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   ### // 1. set the toolbar title using the platform-specific declaration
   title = platformMessage()

  ### // 2. fetch language facts
   val languageFacts = LanguageFactRepository().fetchLanguageFacts()

  ### // 3. bind data to Android UI
   adapter = LanguageListAdapter(languageFacts, View.OnClickListener {
       val position = listRecyclerView!!.getChildAdapterPosition(it)
       val item = adapter.getItem(position)
       Toast.makeText(this, item.description, Toast.LENGTH_LONG).show()
   })
}

As you can see, the interoperability between multiplatform and regular Kotlin is absolutely seamless, and would have been impossible to detect from glancing at this code in isolation. The full Android app example can be found on GitHub, if you wish to study it before we implement the other platforms.

Writing an iOS app with Kotlin Multiplatform

iOS apps are a bit different from other multiplatform targets, as they use Kotlin Native to compile a native binary. In this example, we’ll generate an Apple Framework that contains Objective-C bindings for our Kotlin code, then write the rest of the app in Swift. This is probably the most pragmatic approach for an existing application, but if you’re a purist, it is possible to write an iOS app in 100% Kotlin. The KotlinConf Spinner App is a great example of this.

Creating an Apple Framework

We need to update our common module to generate an Apple framework whenever ./gradlew build is run. To avoid complexity in our example app, we’ll setup the framework to build in the debug configuration only, and for the iOS simulator’s CPU architecture:

iosX64("ios") {
   compilations.main.outputKinds("framework")
}
Apple’s developer docs give a comprehensive overview of what a framework actually is, but for our purposes, all we need to care about is that it includes a blob of native code, and an Objective-C header file. If you peek inside the generated main.framework, you’ll find the following methods which bind our Kotlin code:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("LanguageFactRepository")))
@interface MainLanguageFactRepository : KotlinBase
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
- (NSArray<MainLanguageFact *> *)fetchLanguageFacts __attribute__((swift_name("fetchLanguageFacts()")));
@end;

Now that we’ve got a framework, we need to create a single-view iOS app using XCode, and link the framework as an embedded binary in the project’s scheme. A full guide on how to achieve this is available on the Kotlin website. We’re now ready to run Kotlin in our iOS app.

Writing iOS specific code

A lot of concepts in iOS are somewhat analogous to Android, so we’ll skim over some of the details. CodePath iOS has an excellent guide on UITableView, but to cover the basics, a UIViewController is analogous to an Activity, a UITableView is analogous to a RecyclerView, UITableViewDelegate and UITableViewDataSource perform the same function as an adapter, and a Storyboard is equivalent to an Android XML layout. Ultimately, our ViewController class will look something like this:

import UIKit
import main ### // import framework containing our Kotlin multiplatform code

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
   private let languageFacts = LanguageFactRepository().fetchLanguageFacts() ### // fetch all the language facts

   @IBOutlet weak var detailLabel: UILabel! ### // binds a storyboard outlet to a Swift field
   @IBOutlet weak var tableView: UITableView!

   override func viewDidLoad() {
       super.viewDidLoad()
       detailLabel.text = PlatformMessageKt.platformMessage() ### // set the default message
   }
}

And will also implement delegate methods that control the content displayed by the UITableView:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return languageFacts.count ### // there should be N items in the list
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = UITableViewCell()
   cell.textLabel?.text = languageFacts[indexPath.row].name ### // bind the language fact to the list cell
   return cell
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
   detailLabel.text = languageFacts[indexPath.row].component2() ### // update the detail label when clicked
}

Interacting with Kotlin on iOS is pretty seamless once we’ve performed the initial setup. Putting it all together, our app should now look something like this:

iOS app built with Kotlin Multiplatform

Writing a desktop application with JavaFx and Kotlin Multiplatform

There are a lot of potential technology choices available for a desktop application. For simplicity, we’ll use the JVM-based JavaFx UI framework, but it would be equally possible to use Kotlin Native. To start off, we’ll define an entrypoint for our application:

fun launchApp(primaryStage: Stage, classLoader: ClassLoader) {
   val root = FXMLLoader.load<Parent>(classLoader.getResource("app.fxml")!!)
   primaryStage.title = platformMessage() ### // sets the platform-specific message as the window title
   primaryStage.scene = Scene(root, 1280.0, 720.0)
   primaryStage.show()
}

This entrypoint loads an XML definition of our View that is bound to an AppController class. The XML definition is quite similar to Android XML layouts:

<HBox fx:controller="com.bugsnag.example.kotlinmp.AppController"
     xmlns:fx="http://javafx.com/fxml"
     fx:id="rootPane">
   <ListView fx:id="listView"/> <!-- display a listview -->
</HBox>

The AppController class, will fetch all the language facts from our multiplatform code, then react to UI events by changing the detail label’s text:

class AppController {
   lateinit var detailLabel: Label
   lateinit var listView: ListView<LanguageFact>

   fun initialize() {
       val languageFacts = LanguageFactRepository().fetchLanguageFacts()
       listView.items.addAll(languageFacts)

       listView.selectionModel.selectedItemProperty().addListener { _, _, newValue ->
           detailLabel.text = newValue.description
       }
   }
}

The end result should look something like the following:

JavaFx app built with Kotlin Multiplatform

We think that JavaFx is a great choice as the XML layouts for views will be a familiar pattern to Android developers. As it’s a JVM-based technology, it’s also possible to use the very large ecosystem of 3rd party Java libraries to speed up development.

Writing a JavaScript app with React and Kotlin Multiplatform

Adding JavaScript dependencies

Our next platform is a React app using the Kotlin Frontend Plugin and Kotlin Wrappers, which bind popular JavaScript libraries to Kotlin. The first thing we’ll do is make our common code output a ‘commonjs’ JavaScript module. This seems to be a known issue with the plugin.

fromPreset(presets.js, "js") {
   compileKotlinJs {
       kotlinOptions.moduleKind = "commonjs"
   }}

We’ll then add the necessary React dependencies, and add a kotlin DSL for DOM manipulation. As Kotlin JavaScript uses node modules, we’ll also add the Material Components Web package to style our application. Finally, we’ll bundle our code using Webpack, and serve it at localhost:8088.

dependencies {
   compile(project(":common"))
   compile("org.jetbrains.kotlin:kotlin-stdlib-js:$Versions.kotlin")
   compile("org.jetbrains.kotlinx:kotlinx-html-js:0.6.11")
   compile("org.jetbrains:kotlin-react:16.6.0-pre.62-kotlin-1.3.0")
   compile("org.jetbrains:kotlin-react-dom:16.6.0-pre.62-kotlin-1.3.0")
}

kotlinFrontend {
   npm {
       dependency("kotlinx-html-js")
       dependency("react")
       dependency("react-dom")
       dependency("material-components-web") ### // installed via npm only
   }
   webpackBundle {
       port = 8088   ### // dev server port
   }
}

Building a React App with Kotlin

We’re now ready to build a JavaScript app. Our application will be composed from two React components: an AppComponent which renders everything and responds to state changes, and a LanguageListComponentwhich renders a list of facts and is stateless. We’ll start off by defining the props for the ListComponent:

interface LanguageListProps : RProps {
   var data: List<LanguageFact>
   var block: (LanguageFact) -> Unit
}

This inherits from RProps, which is part of the Kotlin binding for the React API. We can then define the LanguageListComponent that displays a scrollable list, by overriding a render method which is called whenever the component’s state changes:

### // requires previously defined LanguageListProps as its React props
class LanguageListComponent : RComponent<LanguageListProps, RState>() {
   override fun RBuilder.render() {
       ul { ### // create an unordered list
           props.data.forEach { item -> ### // create a list item for each language fact
               li {
                   span {
                       span("primary-text") { +item.name } ### // specify a CSS class for this element
                       span("secondary-text") { +item.description }
                   }
                   attrs { ### // add a click listener to each li element
                       onClickFunction = {
                           props.block(item)
                       }
                   }
               }
           }
       }
   }
}

This is when Kotlin’s Type-Safe Builders really begin to shine. Whereas React typically uses JSX, with Kotlin it’s possible to define the entire React DSL within the normal confines of the language. This brings all the advantages of a declarative UI, and combines them with type-safety, IDE code completion, and compile-time rather than runtime errors.

We’ll then define our AppState for our second component. This extends RState as the message field is mutable:

interface AppState : RState {
   var languages: List<LanguageFact>
   var message: String
}

The AppComponent will set the initial state when it is about to be displayed in the UI. The component will fetch the language facts from our multiplatform code, and set the default message as our platform-specific declaration:

private class AppComponent : RComponent<RProps, AppState>() {
   override fun componentWillMount() {
       setState {
           languages = LanguageFactRepository().fetchLanguageFacts()
           message = platformMessage()
       }
   }

Finally, we’ll update the render method to display a h2 heading, and a LanguageListComponent, which is passed a lambda. When an item is clicked in this component, it will invoke the lambda and the AppComponent will set the React state for the component, triggering another render to refresh the stale UI:

override fun RBuilder.render() {
   div {
       h2 {
           +state.message
       }
       languageList(state.languages) {
           setState {
               this.message = it.description
           }
       }
   }
}

Our end result looks something like this:

ReactJS app built with Kotlin Multiplatform

JavaScript Interoperability footnote

As a footnote, it’s worth noting that Kotlin allows interoperability with existing JavaScript code using the dynamic type, which essentially disables type-safety checks, or the external modifier, which provides type-safety but requires Kotlin bindings. The documentation on this is fairly comprehensive, and worth a read if you’re thinking of introducing Kotlin into an existing codebase.

Writing a REST API with Spring Boot and Kotlin Multiplatform

Last but not least, we’ll create a REST API using Spring Boot. This is extremely straightforward using Kotlin, and only takes around 10 lines of code. We’ll use Spring Initializer to generate the boilerplate for a Spring app, then add a dependency on the common module, like before. All that remains after that is to add a RestController class, and bootRun the app:

@RestController
class LanguageFactController {
   private val facts = LanguageFactRepository().fetchLanguageFacts()

   @GetMapping("languages")
   fun fetchLanguageFacts() = facts

   @GetMapping("languages/{name}")
   fun fetchLanguageFact(@PathVariable name: String) = facts.singleOrNull { it.name.equals(name, true) }
}

The RestController annotation denotes that the class handles incoming requests, and the GetMapping annotations denote methods that handle individual endpoints: one returns a list of languages, the other returns details about one specific language. After starting the application, a JSON response should be returned when making a request:

curl http://localhost:8080/languages/kotlin

{
 "name": "Kotlin",
 "description": "A modern programming language that runs on multiple platforms"
}

It’s also worth noting that the Ktor framework is another common choice for writing backend applications in Kotlin. Ktor makes heavy use of coroutines and type-safe builder DSLs, so depending on your preferences may be your first choice over Spring Boot.

The pros and cons of Kotlin Multiplatform

If you’d like to view the final product, all the example apps created in this blog post are available on GitHub.

Hopefully some of the advantages of Kotlin multiplatform should seem pretty clear at this point. Kotlin Multiplatform makes it possible to reuse code between wildly different platforms, while allowing developers to implement platform-specific functionality as needed. Interoperability with existing languages is pretty seamless, which makes it easy to begin using Kotlin in brownfield projects. And of course, type safety, null safety, and the general brevity of the Kotlin language makes developing a pleasure on all the targets we’ve tried.

So what are some of the disadvantages?

Reusing too much code

It can be very tempting to pigeon-hole all of your code into one common module, using an abstraction that nearly fits most of your use cases. It’s hard to know where to draw the line, and can require a bit more time to get the right design.

Experimental technology

At the time of writing, Kotlin Multiplatform is an experimental language feature, and is subject to breaking changes. Although the API has been pretty stable so far, something you rely upon in production now might break in the future without warning.

Less documentation and tutorials

The documentation for Kotlin Multiplatform can sometimes be a bit lacking, particularly for Kotlin Native. As it’s an experimental technology, that’s somewhat expected - and documentation and example apps will likely improve once Multiplatform becomes stable.

Fewer multiplatform libraries

There are a vast number of 3rd party libraries available to Kotlin on the JVM, but there are comparatively few multiplatform libraries available. Additionally, library maintainers might not have implemented the same level of support for each platform, or may have even skipped certain platforms altogether.

Each Platform is still different

There’s a world of difference between writing web applications in the browser, and writing embedded systems in C. Kotlin Multiplatform allows you to use the same language to perform both, but an engineer without prior experience on a platform will probably take longer to develop an app than someone who’s familiar with all the idiosyncracies.

To use an example in this blog post, the eagle-eyed may have noticed that the iOS app uses component2() rather than description when binding the LanguageFact to a View. This is due to a name clash with an existing method on NSObject, which could cause a lot of confusion and wasted time to anyone unfamiliar with iOS. This emphasises how important it is to get the design of common code right, and to consider whether it will interact nicely with all targetted languages.

A glorious future for Kotlin Multiplatform

Overall, we think Kotlin Multiplatform has great potential for sharing data classes and business logic between Android and iOS apps, as the form factors are very similar, and Swift is syntactically close to Kotlin, resulting in less mental overhead for developers working in both languages. Multiplatform was a big theme at KotlinConf 2018, and we’re very excited to see how it’s used in the ecosystem once it becomes a stable API, as we believe it has the potential to save developers a lot of time and frustration.

———

Would you like to know more?

Hopefully this has helped you learn a bit more about Kotlin Multiplatform. If you have any questions or feedback, please feel free to get in touch. Feel free to inspect the repository on GitHub.

Try Bugsnag’s Kotlin crash reporting.

Share