Kotlin Multiplatform allows you to use the same programming language on mobile, desktop, web, backend, IoT devices, and more. There is a large number of possibilities and a steadily growing number of real-world applications.
Kotlin Multiplatform was introduced at the end of 2018 and many teams have started adopting it right away, first only on smaller parts of their projects, which later grew over time.
In the case of the Cleverlance mobile team, we took a different approach in adopting Multiplatform technology. From the beginning, we believed Kotlin Multiplatform was the right approach to share business and application logic between Android and iOS mobile platforms. We've been following cross-platform and multi-platform technologies for a long time, but prior to the advent of Kotlin Multiplatform, none had convinced us of their long-term sustainability in an app agency environment like ours. It's not just about the functionality of the technology itself. What is important is the maturity of the whole platform, the community around it, and last but not least the good availability of experts in the subject matter, as my colleague summarized in his article Questions to ask before choosing mobile app technology.
As with any new technology we consider for use in our production applications, we set up a small project with Kotlin Multiplatform and tried to implement elementary tasks like sharing pure Kotlin code between Android and iOS, sharing network services, database access, or offloading work to a background thread.
At the same time, we started testing the right application architecture and establishing which parts of the application are suitable to share between platforms and how to do it properly. We addressed questions about the structure and rules for building shared code API, whether it's possible to call Kotlin Coroutines from Swift code, etc. And last but not least, we tested the suitable structure of the project and created a build pipeline, at the end of which an Android and iOS app package will be created.
In the beginning, the work went rather slowly. We built a list of problems or unresolved issues that prevented us from using the technology in our production apps. However, Kotlin Multiplatform has evolved very dynamically and we have to really appreciate the response time of its authors when bugs or shortcomings reported by us were resolved and a new version released in a matter of weeks.
During 2020, our demo project was gradually becoming usable, the list of unresolved issues was getting shorter, and we were eagerly awaiting the stable release of Kotlin 1.4, which promised a lot of good things.
This happened at the end of summer 2020 when the list of issues that would prevent the Kotlin Multiplatform from being used in production was down to the last two.
The first one concerned how to optimally build Kotlin code as a framework for iOS apps. Even today, a year later, this topic is still not completely resolved, but the solution in the form of the so-called Umbrella module (or Integration module) turned out to be functional and sufficient for our needs, without limiting our work in any way.
The second one is about the memory model in Kotlin/Native (in our case for iOS), and the possibility of using a background thread for coroutines. This one doesn't have a final implementation yet either, but a final solution is on the way. However, a temporary solution from JetBrains in the form of a special native-mt Coroutines build has proven to be sufficient in not delaying the use of Kotlin Multiplatform any further.
On 1st September we started working on a new project. It was a mobile application for the consumer/marketing apps sector, for Android and iOS. During the first week of the project, we worked in a team of 1 Android and 1 iOS engineer on the basics of the project structure and app architecture, but especially on the alignment of practices for creating interfaces between shared and platform code and between Kotlin and Swift. Both of us were already experienced in Kotlin Multiplatform, which was important especially on the part of the iOS developer, who was thus already familiar with Kotlin and well aware of the platform differences.
At Cleverlance, we are proponents of Clean Architecture and have applied its principles to native app development for years. In terms of structure and architecture, we didn't need to invent any new approach, we just adapted our habits and proven practices in a few places to share code between the two platforms more efficiently.
In the next phase of the project we added one more Android and iOS engineer, but they hadn't worked with Kotlin Multiplatform yet, so it was interesting to see how quickly they would get comfortable with the new mindset.
Perhaps unsurprisingly, this was not a major problem for the Android engineer. Due to the almost identical architecture and familiar language, it was enough to get used to the slightly different project structure (especially the module structure, the separation of shared and platform code, etc.) and to the new technologies, e.g. for network communication or storage, where it is not possible to directly use the well-known platform libraries, but one has to reach for the multiplatform ones (although these often just cover those known platform technologies under a single multi-platform roof).
It was much more interesting to watch the progress of the iOS developer who had never written a single line of Kotlin and had never encountered the Android world or the structure of Gradle projects. But as he was no stranger to the architecture, even though up until this point he had only known it in similar iOS projects written in Swift. It turned out that those principles and practices we shared years before between Android and iOS developers are that crucial foundation, and the technologies or programming languages are just the tools by which they are implemented.
So at first, this iOS engineer worked mainly on the iOS part of the app, and only occasionally modified shared code written in Kotlin. But he very quickly started writing more of the shared code on his own, until he opened his first PR at the end of September, in which a small but complete feature was implemented in the shared code and the iOS platform implementation. I, as an Android developer, then only wrote the small platform part for Android, on which I spent about 2 hours instead of the 2 days that the feature would have taken me if I had written it all natively for Android.
And let me tell you, it feels nice and exalting. Then, when that same iOS developer single-handedly fixed a bug in the Android app for us a few weeks later, I realized that this method of development has a whole new dimension that we never dreamed of before.
The following weeks and months of the project weren't entirely without problems. We occasionally ran into inconsistencies between the nascent Kotlin Multiplatform libraries, especially between Kotlin Coroutines and Ktor. The iOS framework build system stumbled a few times, but none of these issues ever gave us even a hint of stopping the development, and what's more, problems of this type gradually subsided. Around December 2020, after a new version of Kotlin Coroutines 1.4 was released, fully in line with the principles of multiplatform development, these difficulties became completely marginal and we were able to concentrate fully on the app development.
As the project entered its final phase just before its release into production, it was time to look at the numbers.
When I checked the last two projects that we created as standard separate native Android and iOS apps, I found that the amount of code (lines of code) required for the Android and iOS app was pretty similar. In fact, the iOS apps were slightly smaller, but I attribute that mostly to the chatty way UI definitions are done on Android, a thing that is changing dramatically with the advent of technologies like Jetpack Compose and Swift UI. Likewise, at the project level, it can be argued that a similar amount of time is required to implement apps for both platforms.
If the effort to implement one native app is 100%, then with Kotlin Multiplatform almost 60% of the code can be shared, and only a little over 40% needs to be implemented twice, for both Android and iOS, meaning the effort to implement both apps is 140% instead of 200%, saving almost a third of total development costs. Here again, it turns out that the amount of code needed for finalization on both platforms is similar. It should be noted that we also count a non-negligible amount of unit tests that we write only once and share.
The user interface is a very platform-specific layer, but the presentation layer does not contain such differences, and the reasons we did not share it in the first project are more technical. On subsequent multiplatform projects, we have however focused more on this part and now we are able to share the presentation layer code at about 70%, which has a positive impact on the percentage of overall code shared in the project.
However, the dominance of reported Android developer time is not due to Android apps being more demanding, but simply because Android developers had more time to spend on the project. In fact, one of the iOS developers was not always able to devote 100% of his time during the project, but this did not affect the speed of development for both apps, as his time was simply compensated by the Android developers. The same worked the other way around when, for example, both Android developers were on holiday at the same time. This is not to say that an Android and iOS developer is an equivalent entity in terms of project staffing, but definitely, the multiplatform development gives you a certain amount of flexibility in human resource planning.
At the end of this post, I'd like to mention a few interesting facts and side effects we noticed during the development:
- The project is not fundamentally more demanding in its setup than a standard single platform project. Creating the basis for a new project takes a similar amount of time, in the order of days.
- It's very good if Android developers learn at least the basics of Swift and working in XCode. They can better prepare the shared code API and make small adjustments atomically across both platforms and shared code.
- For iOS developers, learning Android technologies and ecosystem often involves discovering better tools than they are used to, which motivates them in their endeavors. *The second usage of shared code works as a very careful code review, and for some teams, this will allow the standard code review done during pull requests to be either reduced or removed entirely, thus increasing the development momentum.
- From a project and business management perspective, we are building just one application. The same is true when communicating with the backend, where both applications act as one, there are no differences in implementation on each platform and it greatly facilitates team communication.
- Short-term planned, but also unplanned developer downtime does not affect team velocity.
- On one of the following projects, we were able to work in a mode of 1.5 Android developers and 3.5 iOS developers, with the development of both apps progressing similarly.
It's been more than a year since we started working on our first application using Kotlin Multiplatform, and as the text above indicates, it hasn't remained an isolated experiment.
We are currently using this technology on five brand-new application projects. Besides that, we are discussing the opportunities with several long-term customers to deploy this technology in existing projects.
Kotlin Multiplatform is maturing like wine and we look forward to bringing the mobile platforms even closer together.
Written by: Pavel Sveda @xsveda