Cross-platform development has always been a topic here at SinnerSchrader. With 14 years of mobile development experience - yes, mobile started before the iPhone - we have used many cross-platform solutions like PhoneGap/Cordova, React Native, and most recently, Flutter. We have embedded functionality through website fragments into WebViews. And we looked at technologies like Titanium, Xamarin, and NativeScript, which then ultimately never made it into our projects.
To be honest, none of these projects ever felt right.
They were a good compromise to achieve competing project goals. But in the end, they were just compromises. Don't get me wrong. They worked. Some were beautiful, some had decent performance, and certainly, all fulfilled their purpose.
In this post, I want to share what felt wrong with all of these approaches and how Kotlin enables a substantially different approach to cross-platform development.
Let's start by looking at the common approach of all current cross-platform frameworks.
All of the frameworks mentioned above claim full project ownership1. They bring all the necessary tooling to build an Android app, an iOS app, and some even a web app.
This sounds great at a first glance, but let's take a closer look. All of the above frameworks come with their own build systems, dependency management, UI libraries, ... If you need native functionality, you bridge the native functionality into the alien framework. The whole project is developed with alien technology. Alien to at least one of its target platforms. Most of the time alien to Android and iOS.
Do you see what's going wrong here?
- They have abstraction and encapsulation layers around everything, trying to make apples look like pears (or like Androids).
- Mapping the iOS code signing setup to Cordova configuration files is a pain. Now you need someone who understands iOS app signing and who knows NPM build systems. (Just an example.)
- UX and UI from a single code base never feel native to all platforms. It is not enough to have buttons and text boxes look platform native out of the box. You start adjusting Android/iOS/Web-specific UI within that single code base, using the one-size-fits-all technology stack.
- If one of the platform designs change in the future, as happened with the introduction of Android Material Design, your Flutter app will continue to use the imitation of the old native UI elements.
- Need to save some state on Android life cycle events in Flutter? Sorry, life cycle events were not implemented from Nov 2016 until Aug 2020.
- Accessibility in ReactNative? Was basically unusable before Aug 2018.
- We will see more of these issues in the future as the frameworks are always in catch-up mode to abstract and encapsulate the latest developments in native platforms.
Why go through all this when there is a better approach?
Today, most professional application development follows some incarnation of Clean Architecture. Be it MVVM, MVP, MVI. All well done Clean Architecture approaches have one thing in common. They move all the platform-dependent components to an outer architectural layer. All the inner layers are platform-independent.
What if we could keep the IDE, the build system, the UI native?
- Build your web app in HTML, with JS and an NPM development server.
- Build your iOS app in XCode with Swift and UIKit or SwiftUI.
- Build your Android app in Android Studio, with Kotlin, with Gradle.
The project would be owned by the platform-tools again.
What if at the same time we could achieve code sharing of up to 80% by writing all the platform-independent inner layers into a library in a modern language that seamlessly interfaces to and from native code?
With MVVM, for example, you could potentially bring everything up to the view models into a multiplatform library.
Only views and platform access (network, GPS, file system) would you implement natively and inject into your library. Now if you also had a rich ecosystem of ready-made cross-platform libraries for dependency injection, networking, databases, serialization, and others. That's my vision of cross-platform development.
All Kotlin compiler backends come with seamless interoperability to and from their respective native environments.
- Depend on NPM modules.
- Generate Kotlin bindings from TypeScript definitions.
- Export TypeScript definitions.
- Compile Kotlin to UMD, AMD or CommonJS modules.
- Depend on CocoaPods.
- Generate Kotlin bindings from C/Obj-C headers.
- Export C/Obj-C headers.
- Compile Kotlin to CocoaPods framework as CocoaPods.
To further ease your cross-platform development there are ready-made multiplatform frameworks for many of the platform related tasks. These libraries take the effort of native implementation of platform-specific tasks off your shoulders. Here are some well-known examples:
- ktor asynchronous http client library
- kotlinx.io asynchronous File io, Socket io, Buffer, and even encryption library.
- kotlinx.serialization Extensible (de-)serialization framework with support for JSON.
- SQLDelight typesafe SQLite wrapper.
- Firebase Kotlin SDK Firebase client SDK.
- And many more.
The Kotlin environment offers all this to enable a maximum of code sharing. The goal is not to replace your native app with a 'Kotlin cross-platform app'.
This looks too good to be true. Why is this a vision and not the industry standard?
Because it is not here yet :-(
Kotlin for the JVM (including Android) is rock solid and used in production all over the place. You can also start building cross-platform libraries with Kotlin right away. All the tools are there. But most are not ready for production. I.e. in an alpha stage or even experimental.
Here are some of the biggest construction areas:
- Kotlin multiplatform is in alpha stage.
- Kotlin native (iOS) is currently getting its memory management and concurrency architecture completely redesigned.
- Kotlin native is not optimized for performance today.
- Kotlin JS gets the new Kotlin JS IR compiler, which is still in alpha today.
- TypeScript definition generation is only available in the new Kotlin JS IR compiler.
- Dukat, Kotlin's TypeScript binding generator is still experimental. Today, bindings that are automatically generated from TypeScript often need manual adjustments.
None of this should stop you from getting ready for Kotlin multiplatform. You can start experimenting with it today.
The Kotlin team is working hard on getting Kotlin native and Kotlin JS up to the standards of Kotlin JVM. And I am totally confident that when it's ready, this will be the way to develop apps for multiple platforms.
I am aware that there is no common truth in the developer community when it comes to cross-platform. While I am certain that Kotlin will significantly change cross-platform development, you may have a very different opinion. Please feel free to leave a comment and start a lively discussion.
You are right. ReactNative for example can be used to build widgets for embedding into your native app. But you get the point. It still wants to own a full vertical slice of your project from model to view. ↩