What is Kotlin Multiplatform?
Ever since it was first introduced, Kotlin’s ability to target multiple different platforms has been one of its key benefits. With this approach the time spent implementing and maintaining the same code on multiple platforms can be greatly reduced, while still retaining the flexibility and benefits of native programming such as best-in-class performance, tailored APIs and common UI elements among others.
Kotlin Multiplatform is a tool with multiple potential use cases:
- Sharing application logic between Android and iOS applications using Kotlin Multiplatform Mobile (a subset of Kotlin Multiplatform targeting the Android and iOS platforms);
- Sharing application logic between mobile, web and/or desktop applications;
- Sharing logic between the server side and the client side running in a browser. Kotlin/JVM is used to implement the server side and Kotiln/JS to implement the client app;
- Creating multiplatform libraries that can be used in other Kotlin Multiplatform projects and applications;
- Creating CLI tools for multiple target platforms.
To achieve all of this, Kotlin Multiplatform relies on some basic components that work together:
- Common Kotlin can run on all platforms. It contains the language, core libraries and tools;
- Multiplatform libraries help reuse common multiplatform logic between the common and platform-specific code. Common code can depend on libraries to handle common tasks like HTTP communication, serialisation, database access, managing coroutines, etc;
- The platform-specific versions of Kotlin (Kotlin/JVM, Kotlin/JS, Kotlin/Native) are used to provide interoperability with host platforms. Platform-specific versions contain Kotlin language extensions, platform-specific libraries and tools;
What is Kotlin Multiplatform NOT?
It is vital to understand that Kotlin Mutliplatform is NOT a cross-platform app development framework like React Native or Flutter. Kotlin Multiplatform does not provide a way to share UI between platforms and its creators recommend using native UI as a best practice. Although Kotlin Multiplatform Compose is picking up speed, it is a separate project and is not mandatory to use with Kotlin Multiplatform.
Kotlin Multiplatform vs React Native
The difference between Kotlin Multiplatform and React Native is that React Native provides an engine to execute JS code and uses a middleware (bridge) to interact with the platform code. Kotlin Multiplatform is NOT an engine to execute Kotlin on different platforms; instead it directly produces platform specific-code.
Kotlin Multiplatform vs Flutter
When comparing Kotlin Multiplatform to Flutter, the similarities are obvious. Both frameworks produce native code: Flutter compiles to C\C++, and Kotlin Multiplatform is compiled to JVM bytecode for Android and to C\C++ for Native (iOS, macOS, watchOS, etc). The difference is that Flutter does its own UI rendering and Kotlin Multiplatform does not provide any way to share UI out of the box.
Most importantly, Kotlin Multiplatform is NOT a silver bullet. All pros and cons should be carefully weighed before moving forward with it. If the overhead of introducing Kotlin Multiplatform is greater than the benefits that come with it in a specific situation, then maybe it is not a good fit.
What should you share?
Every application, no matter how complex, can be broken down into three main layers: data access layer, business logic layer and presentation layer.
In a typical scenario, the data access layer and the business logic layer across different client applications have the same requirements: all clients use the same data sources, the business rules are the same and all of these must be covered by appropriate unit and integration tests. This makes the data access layer and the business logic layer ideal candidates to be shared using Kotlin Multiplatform. You can implement them once, have a single test suite to validate the implementation and get rid of the nasty bugs dealing with diverging implementations of the same feature on different platforms.
The presentation layer is where different platforms differentiate themselves the most. Usually, the presentation layer is broken down into different components according to an MV* architecture - MVC, MVP, MVVM, VIPER, etc. All the UI definitions, the UI rendering are highly platform-specific and that makes them incredibly hard to share. The Kotlin Multiplatform creators advise against using it to share the presentation layer, but there are also exceptions.
If we have an MVP architecture, we can define the component contracts in a common module and, if the presenter implementation depends solely on the view interface, then it can be shared as well. Contrary, if MVVM is used, view models are going to use very different mechanisms to expose the observable state to the view (LiveData/StateFlow on Android, Combine on iOS, RxJS on the web, etc). Trying to share that using Kotlin Multiplatform is not impossible, but it would be very challenging and intrusive, forcing developers to use non-platform idiomatic technologies.
Each application is different, so there is not a one-size-fits-all approach to deciding what to share. This should be decided after a careful evaluation of the requirements. My personal recommendation is to strive to share the data access layer and business logic layer and leave the presentation layer native.
A typical Kotlin Multiplatform project tech stack
Kotlin Multiplatform is getting more mature every day and there are already some libraries and frameworks that are considered as a standard and can be found in almost every multiplatform project out there. Some of them include:
- The Kotlin standard library: contains the essentials for everyday work with Kotlin;
- Ktor: a multiplatform HTTP client (also supports server side developments targeting Kotlin/JVM);
- Kotlin Serialization: a library that deals with JSON serialisation / deserialisation;
- SQLDelight: a framework for managing local persistence targeting; Kotlin/JVM, Kotlin/JS and Kotlin/Native;
- Kotlin Coroutines: an approach to dealing with concurrency;
- kotlin-test: a library that supports implementing multiplatform unit and integration tests.
Implementing common code
The source code in a Kotlin Multiplatform project is distributed in different modules. The commonMain module contains the common code that is shared across all target platforms. The common code cannot depend on platform-specific code directly and can only depend on multiplatform libraries. The platform-specific code is located in the platform-specific modules (androidMain, iosMain, jsMain, etc). Finally, the platform-specific modules depend on the common module.
Handling HTTP communication
When dealing with HTTP communication in common code, using Ktor has become a defacto standard. It provides a configurable HTTP client and a wide variety of plugins that support all the basic functionality that might be required. Additional customisation can be done using request and response interceptors.
Ktor has per platform HTTP engines that must be created in the platform-specific code modules. All HTTP communication is done in the background using coroutines (more on them later). Ktor client also integrates seamlessly with kotlinx.serialization to deal with JSON serialisation/deserialisation.
Handling data persistence
SQLDelight has established itself as the go-to choice when it comes to dealing with persistence in Kotlin Multiplatform projects. It is a framework based on SQLite and supports Kotlin/JVM, Kotlin/JS and Kotlin/Native.
SQLDelight generates a typesafe kotlin API based on SQL queries. It also provides compile-time checks for verification for schemas, queries and migrations and has IDE support for auto-complete. Not that when targeting multiple platforms, SQLDelight relies on platform-specific database drivers that must be initialised in platform-specific code modules. It does not deal with concurrency out of the box.
Coroutines are the preferred way of dealing with concurrency in Kotlin and Kotlin Multiplatform. They are not a part of the standard library, but an official library that complements it. Coroutines take away the complexity of dealing with threads and passing data between them when work has to be offloaded from the main thread by leveraging the async/await paradigm. Be sure to check out the coroutines documentation if you are not familiar with them. A Kotlin Multiplatform common module should expose suspend functions in its API. Those suspend functions should be safe to call on any thread and should not hardcode any particular dispatcher or coroutine context.
NB: If the project is using a Ktor version prior to 2.0.0, dependency on the native-mt version of the coroutines library should be forced, since it is required to be able to use coroutines on Native correctly.
One downside of using coroutines is that they are really Kotlin-specific and calling them from other platforms like iOS or Java Script, while possible, is highly unpleasant and error-prone. This is why my advice to you is to always create a platform-idiomatic interface and use it in the native part of the applications. But more on this later.
This is Part 1 of a blog series on Kotlin Multiplatform. Stay tuned for Part 2 where we are going to introduce you to the intricacies of interfacing with platform-specific code. See you soon on the Infinite Lambda blog.