Industry: Software Development, Mobile Development
JetBrains products used: Kotlin Multiplatform Mobile
VMware software powers the world’s complex digital infrastructure. The company’s cloud, app modernization, networking, security, and digital workspace offerings help customers deliver any application on any cloud across any device.
Could you say a few words about your company?
Our digital workspace offering is centered around VMware Workspace ONE – a digital workspace platform that simply and securely delivers and manages any app on any device by integrating access control, application management, and multi-platform endpoint management.
VMware also provides a suite of Workspace ONE productivity apps that support email, notes and tasks, content, intranet, and more. The suite of productivity apps includes Workspace ONE Intelligent Hub, Workspace ONE Boxer, Workspace ONE Mobile Flows, and Workspace ONE Content, among others.
Our software is used on millions of devices worldwide.
How is Kotlin Multiplatform used in your product (maybe with respect to its architecture scheme)?
Kotlin Multiplatform is used in various modules to enable different use cases in a consistent, cross-platform way across our Workspace ONE productivity app portfolio. I recently blogged about that in the article Adopting a Cross-Platform Strategy for Mobile Apps. Framework of choice: Kotlin Multiplatform, which will answer this question best:
When our team decided to seriously pursue a cross-platform development strategy for mobile apps, we began by investigating a handful of various options, each with its share of strengths and weaknesses. As our team carefully weighed the benefits and shortcomings of building our strategy upon each of the frameworks, Kotlin Multiplatform ultimately emerged as the framework of choice. This didn’t mean that Kotlin would become the only framework our team would ever consider using, but it came to be our default, go-to framework.
Our team had a wide-range of reasons for settling on Kotlin Multiplatform. Some of the most compelling include:
- Kotlin Multiplatform is backed by JetBrains and Google via the Kotlin Foundation.
- There is a significant investment in this technology by JetBrains and the Kotlin Foundation.
- It’s built on Kotlin/Native, which is already quite robust even though it’s still in beta.
- Our team develops apps, and therefore half of our developers were already familiar with Kotlin.
- There is first class support for the technology in various JetBrains IDEs (mainly IntelliJ IDEA and AppCode), and Android Studio.
- It allows for utilizing libraries built with various technologies as dependencies.
There are a number of documentation pages, articles, and sample projects that dive into the structure and details of a Kotlin Multiplatform project. Rather than repeating others’ hard work on the subject, here are some resources that I found helpful when getting started: multiplatform programming reference and the KotlinConf App.
Do keep in mind that Kotlin Multiplatform is still an experimental technology. It’s at an early stage of development and may be subject to breaking changes going forward. There is no timeline from JetBrains yet in terms of a first stable release. That does introduce risk when using it in production.
After the release of Kotlin 1.4, KMM is in Alpha status. It means that you can use it in production and the Kotlin team is fully committed to continue working on this technology, improving and evolving it further, and won't suddenly drop it. Though we try our best, we can not eliminate migration issues yet, so please check the KMM evolution page for information about KMM components stability status.
In our team, we generally develop new modules that can be consumed by one or more of our existing apps. This means we create Multiplatform libraries that are integrated by the app teams. Taking this into account, I use IntelliJ IDEA as my primary IDE for library development. With the latest Kotlin plugin, there are a number of handy new-project templates that can be used to create projects. In our team we use Mobile Shared Library, which does not create the app projects, only the library project. By default, a project will contain a JVM and an iOS target. Based on your needs, you may need to “convert” the JVM target to Android.
For our first Proof Of Concept (POC), JVM was adequate because we weren’t referencing the Android SDK APIs. This template will create all the source sets you need to get started, along with the basic Kotlin dependencies. It will also include some example code to illustrate the bare basics, which you can remove once you’re comfortable with it.
The Problem of Dependencies
Hopefully, before you reach this point, you will have had the chance to consider the scope and function of your library, and you will have some idea of the libraries that you might need to pull in to achieve your goal. One issue you will most likely encounter with Kotlin Multiplatform is that the set of libraries that support it are very small at this point. There is support for HTTP (Ktor), JSON (kotlinx.serialization), a subset of coroutines (main thread only), and a few bits of functionality that you will find here and there. This list will obviously grow over time, but at this point it’s still in early development. As a result, you might see the need to import a library that is specific to one target, and another (or a port), that is specific to another target. This is what we had to do in our case.
For our project, we wanted to use libphonenumber from Google. The library is primarily authored in Java. This made it very easy to add it as a dependency for the JVM target. There is also an official port of the library to C++ that is maintained by Google. Outside of that, there are several unofficial ports. There is a port of the library to Objective-C, and Kotlin/Native supports Objective-C dependencies by generating bindings to be used from Kotlin (which is referred to as cinterop). Unfortunately, the port is incomplete and mostly unmaintained. This made it a non-starter, and left us with the C++ port as the only practical option.
This created two problems for our team:
- The library and all its dependencies (there are a few, including ICU) need to be cross-compiled to every supported iOS ABI.
- Kotlin/Native doesn’t support C++.
We solved the first problem with significant effort from our team, and we were able to find a workaround for the second issue based on this premise:
- Objective-C integrates with C++ using Objective-C++.
- From an API perspective (and with regard to the header files), it’s not relevant that the implementation is Objective-C++ (that’s an implementation detail).
- Kotlin/Native supports Objective-C dependencies (and therefore Objective-C++ dependencies, as long as there is no C++ in the header files).
As a result, we were able to create a shim interface in Objective-C(++) that we could link from our Kotlin Multiplatform project. You could do something similar with pure C, but since this was to be consumed by iOS apps, creating an Objective-C framework made the most sense. It wasn’t the most elegant solution, but it was far better than writing our own version of libphonenumber. After validating this approach, we began the task of cross-compiling. Fast forward an inordinate amount of time, and we had all of the required native libraries cross-compiled for iOS. However, I’ll spare you the details.
From this point we were able to create a Cocoa Touch Framework project from within Xcode. We imported the cross-compiled fat libraries, and then added the small amount of source that was needed to implement the shim. We set up the project to build a fat framework that could then be pulled into our Kotlin Multiplatform project. Suffice to say, there was much cheering and rejoicing at this moment.
A Brief Overview of Cinterop
Cinterop is (or was) not the most well-documented piece of Kotlin functionality. There’s enough out there to get by, but it took our team a while to find everything we needed to get set up properly, including:
- A directory named c_interop was created under the source root where we put the aforementioned fat framework. The actual name and location are arbitrary.
- Under src/nativeInterop/cinterop, which is the default location for DEF files, a file named phonenumbers.def was created.
- Within the DEF file, we placed the following code block:
Here we are telling Kotlin that we have an Objective-C framework, which headers it should look for, and to only process headers that are specific to the project (starting with the prefix “AW”). The header processing is recursive, so the platform headers don’t need to be parsed. We are also telling the compiler the framework name to link when linking the target binary.
- The cinterop was then added as a dependency for our iOS targets by adding the following code block to the targets block within the build.gradle:
We have to tell the compiler where to find the headers within the project source. Also, we have to tell the linker where to find the framework. Once this was all set up, we were able to access the framework APIs from Kotlin using the com.airwatch.phonenumbers package, and more rejoicing ensued.
“Expect” & “Actual” Keywords
The goal of Kotlin Multiplatform is obviously to have as much code in the common source set as possible. However, you will undoubtedly encounter situations where you will need a platform-specific implementation. In this case, since we had 2 target-specific implementations of libphonenumber with different APIs, we needed a bridge within the common code to define a common interface. This was accomplished using the expect and actual keywords that are provided by Kotlin Multiplatform.
The easiest way to think about this concept is that expect declares an interface that has exactly 1 implementation per target and is provided by actual. Separate names for the interface and the implementations aren’t needed. The common code simply constructs the interface and calls its methods, which in this case are a bridge to the underlying APIs.
Integrating the Module
Once we had implemented our Multiplatform library (and tested it, of course), it was time to decide how to pull it into the apps that will consume it. In doing so, we needed to consider 2 different workflows:
- A developer who does not work on the cross-platform library, and does not need to compile its source.
- A developer who does work on the library, and therefore needs to build it inline.
There were a few ways we could go about integrating the library, each with their own pros and cons. The simplest way was to keep the Multiplatform project separate and allow developers to clone it and work on it as necessary. Meanwhile, the built artifacts could be dropped directly into the app’s source tree. This is a very straightforward approach, but not friendly for the developers who would be working on the library. It would create the need for lots of copying around of artifacts and what not. Also, it would be difficult to know which version of the library’s artifact would actually be included in the source tree. This approach would not be scalable.
In my opinion, the most flexible way to integrate the library was to add it as a Git submodule of each app. Depending on whether your project already has submodules or not, developers may not even need to initialize the submodule at all. On Android, it’s possible to use a local .properties file with a flag that indicates whether the library should be pulled in as a subproject, or pulled from an artifact repository (like Artifactory or JCenter). Because Kotlin Multiplatform has added CocoaPods support, a similar technique can be achieved on iOS. Two entries can be added to the Podfile: one pointing to the local Podspec, and one pointing to a Podspec hosted in git. The local entry will need to be commented out in source control. So, when an iOS developer wants to work on the library, they simply need to change which entry is commented out in the Podfile. This will allow for easy switching to the local source on both platforms.
What have been your most significant gains and pains?
The benefit is quite simply one consistent implementation of a particular use case across different platforms and apps. There have been many pain points. These include:
- Lack of documentation and examples (though this has slowly been improving).
- Lack of 3rd party libraries/frameworks (again, slowly improving).
- Lack of C++ interop for Kotlin Native.
- Limit of one Kotlin Multiplatform dependency per Kotlin Native target.
- By far the biggest pain point is the concurrency story for Kotlin Native, from its total inconsistency with Kotlin/JVM to its many strict rules and high cognitive load. Also, the API design is not really supportive of the mental model, and there is a lack of proper coroutines.
We are working on a replacement for the current memory management implementation in Kotlin/Native. The new memory manager will allow us to lift the restrictions on object-sharing in Kotlin/Native, and it will provide fully leak-free concurrent programming primitives that are both safe and don’t require any special management or annotations from the developer. Existing code will continue to work and will be supported. Read the whole story in our blog.
Do you have any tips or advice you’d like to share with our readers?
You will need to get really good at navigating through Gradle source, including plugins. This is much easier when using Kotlin DSL, due to its static typing.
As our team carefully weighed the benefits and shortcomings of building our strategy upon each of the frameworks, Kotlin Multiplatform ultimately emerged as the framework of choice.
Kris Wong, Software engineer/architect at VMware
Kris Wong, Software engineer/architect at VMware
Autodesk introduced Kotlin Multiplatform Mobile to their PlanGrid app to ship a single source of truth for offline sync logic and data models on 3 mobile platforms: iOS, Android, and Windows.
Yandex uses Kotlin Multiplatform Mobile in their Disk and Maps apps to share the business logic between iOS and Android apps and wrap the existing cross-platform C++ library.