We love the idea of Kotlin/Native and Kotlin-Multiplatform. That’s why we took an in-depth look at it and tried it out in a sample project. We wanted to know if Kotlin/Native is production ready. Unfortunately, the technology cannot serve our needs right now. Here is why we crave to use Kotlin/Native and Kotlin Multiplatform but decided against using it in a production project for now.
As a mobile app agency, we have a lot of apps that are developed for iOS and Android at the same time. We, as a team, highly believe in the “pure native app experience” and most of our customers also prefer fully native apps.
While there are many upsides of native apps over cross-platform solutions, there are of course also some downsides: We have to allocate separate iOS and Android teams to develop two independent applications. Problems need to be solved twice (to some extent), requirements will often be interpreted twice and the number of bugs and problems tends to be higher as well. We invest all this energy just to build two code bases in pretty similar languages (Kotlin & Swift) which should do the exact same thing.
We tried to solve these pain points of Native App Development by using an architecture that makes it easier to reuse logic between two code bases. But our wish was always to truly share the same logic code between both platforms (using a language that app developers are familiar with like Kotlin/Swift rather than e.g. C/C++).
It is obvious that we were really excited once we first glanced over Kotlin/Native and Kotlin Multiplatform! With this technology, we could separate our apps into two parts, of which one is shared by both platforms (iOS & Android):
- App-Frontend: All code that is required to build the UI of the app
- App-Backend: Database, Network, Services, ViewModels, …
We would be able to develop this App-Backend in pure Kotlin and share it as a library/module with the Android/iOS App. The logic (which should be the same for both platforms) gets only written once. At the same time, we can still write a custom native User Interface for each platform. Therefore, the user gets the same awesome native experience as before. All problems solved 🙌🎉 But are Kotlin/Native and Kotlin Multiplatform ready to be used in production?
Our first experience: Gradle
After Kotlin/Native 0.9 finally supported IntelliJ and Android Studio, and Kotlin 1.3 „completely reworked the model of multiplatform projects „ we decided to give our dream a first try! We started to build a sample application, structurally comparable to projects that we usually handle and with the „App-Backend/App-Frontend“ separation in mind. After painful hours of figuring out how to configure Kotlin-Multiplatform with groovy style
build.gradle scripts, we had a very simple application set up. It contained multiple modules like
app-android-ui-registration, … where the modules
app-backend-registration obviously would target iOS and Android. It compiled and we got the Android app running. So we were happy.
Until we opened the project the next day and were greeted with the IDE marking almost all source files with errors: ‘Check your module classpath for missing or conflicting dependencies‘ (IDEA-204477). After trying to clean the project we did what all gentle IntelliJ users would do: “Invalidate caches and restart”, delete all
.iml files and nuke all
.gradle folders. It fixed the problem…until we had to
sync the IDE with Gradle again! Same issue. We filed the ticket and waited for the next release of Kotlin.
January 23, 2019, *Kotlin 1.3.20* is released and offers support for Kotlin DSL build scripts in multiplatform projects and its great! Almost all the Gradle pain that we felt is gone. We had auto-completion in the build script and it just felt right to us!
So we picked up our original plan again: Sharing the App-Backend with Android and iOS! After a short period of time, we noticed that syncing the project took extremely long. The reason: Each time we had to
sync our project it happened to re-download all Koltin/Native POM files (KT-28128). Not the biggest problem, since this problem is already fixed for future builds of the kotlin gradle plugin, but still requires some “Gradle tricks” to avoid right now.
After setting the project up further we figured that we would need some intermediate common source sets in order to be productive. Unfortunately, this is not supported by the Multiplatform Gradle plugin for now (Support multilevel multiplatform projects in the new model KT-27801). It is expected to be resolved in the near future.
There are some (dirty) tricks to get around the issues described above but it’s a bit of a hassle nevertheless.
After a while: The Xcode project was set up and we could finally build the
.framework and were able to consume our App-Backend APIs!
Except. We can’t (really) consume our APIs. While I read the („Objective-C and Swift Interop“) document, some very important detail is missing from there, in my opinion. Let’s have a look at one example on how we modeled parts of the App-Backend:
The API described above is a
ViewModel: It has some inputs that the View can set and some outputs that the View can observe (More information about this pattern here: App architecture: Functional MVVM with RxJava & RxSwift).
But what’s the problem with this API now? Here is what the API looks like from Swift/Xcode:
Subject<String> loses all its generic information (Kotlin Generics to Swift Generics. · Issue #2429 · JetBrains/kotlin-native · GitHub). This alone renders our original plan to be not desirable anymore. We thought about modeling our “App-Backend” in a way that does not need generics, but this turned out to be extremely hard and does not fit our current style of development at all. We also thought about „maybe we can just live with it“. But no. We need to find another solution.
Okay. So what about developing the iOS “App-Frontend” in pure Kotlin/Native and not using Swift/Xcode wherever possible? The problem would be gone then, right? Yeah, but now all generic APIs defined by Swift/ObjC (like parts of the iOS Framework) will lose the generic type when consumed by Kotlin:
Now Kotlin/Native just sees
List<*> where we would expect
List<UIView>. So we are running out of options now! We do not think that consuming the shared App-Backend from Swift nor Kotlin is a viable option for writing the iOS App-Frontend.
Okay but not all of our App-Backend code requires generics and we could still share these parts easily, right? So lets quickly re-structure the code we had previously, re-implement some parts in swift and test it: Works on Android 🎉. Does not work on iOS 🤔
It’s not crashing but it is not doing the right thing either. Looking at the console in Xcode just reveals
kotlin.TypeCastException . Nothing more. We figured it out, after a short phase of trial and error! We love coroutines and of course, did we use them for our App-Backend implementation to handle asynchronous and parallel code. But since Kotlin’s memory model works vastly different on Native than on the JVM, there is currently no support for multi-threaded coroutines on Kotlin/Native or any other common abstraction for multi-threading (you can use coroutines in Kotlin/Native and Kotlin-Multiplatform but expressions like
withContext(Dispatchers.Default) won’t work in a ‘Multiplatform’ context).
We do not think that this missing common ground is just an inconvenience, but also a huge problem for our original plan, to share the App-Backend, since we have a lot of code that requires switching threads. Of course, we have good libraries like Stately, that enables us to write common code that supports state management in multithreaded environments. Unfortunately, this still does not offer any real solution for writing parallel common code and leaks Native platform details into our common source set.
Please don’t get me wrong: Kotlin/Native’s memory management is great, the new concept could save us a lot of frustrating hours of bug-fixing for our projects. It is the context of “Multiplatform” where problems occur and raise doubts, whether a more “traditional” memory management model would have fitted better. Obviously, this is exactly the context in which we wanted to apply Kotlin/Native.
Conclusion: Is Kotlin/Native production ready?
Our assessment after trying Kotlin/Native and Kotlin Multiplatform for a while now was: Kotlin/Native is ready for production, but Kotlin Multiplatform is not, at least for our use case at this moment. The Kotlin-Multiplatform Gradle plugin misses some features that we would require, our APIs make heavy use of generics (which are not fully supported for the Swift/Kotlin interop), and multi-threading is yet-to-be-solved in a delightful manner.
While our first experience was pretty rough, we shall not forget that our original plan was pretty ambitious and the technology is young and developing rapidly. Problems that we experienced at the very first beginning, like the difficulties in setting up the
build.gradle files have been almost wiped out with the introduction of the Kotlin DSL support. And other bugs that we mentioned like the re-downloading of pom files are already fixed for future versions of Kotlin.
We expect to see many major improvements to Kotlin’s Multiplatform approach in the next months. The Swift/Kotlin interoperability is a known issue, according to one JetBrains employee we talked to, and actively worked on. The missing common ground for multithreading between Kotlin/Native and the JVM will require „changes to Koltin/Native’s memory model“ and „we will see a prototype in this year“
There are still many parts of traditional mobile apps are already suited pretty well for Kotlin-Multiplatform: Maybe sharing the network or database code would make a lot of sense for many teams, since there are already pretty great tools that support this technology. We, at QuickBird Studios, will wait for another few months until Kotlin/Native further matures and are very excited to see where the journey with this awesome idea will head. We highly encourage everyone to try it out and share their experience with the world.