I’m in the middle of evaluating what we should use for code sharing. We have a heavy iOS dev shop, but KPM seems to be a solid option. Is anyone here using KMP and evaluating Swift for Android?
Great question, Oscar. This topic deserves a whole article or blog post, but I'll sketch out a quick response here.
From a very high level, you might view Swift on Android as a direct inverse of Kotlin Multiplatform: rather than exporting your Kotlin project (along with their garbage-collected runtime) into your iOS app, you are instead cross-compiling your Swift code as a native Android library.
And unless you are using a whole-app UI stack like Skip.tools[1], you will have the same UI-to-business-logic considerations as you would with KMP. You would be building completely separate parallel UI layers for each platform (likely SwiftUI on iOS and Jetpack Compose for Android), and maintaining a clear delineation between the shared cross-platform business logic layer and the unshared UI layer.
Communicating between the UI layer and the shared business logic will be dependent on the architecture of your app, but both KMP/Swift will require some sort of "bridging". KMP automatically generates Objective-C bindings when it exports Kotlin as a "native" iOS framework, which can be consumed by your Swift on the iOS side. For the Swift SDK for Android, there isn't currently any automatic bridging that is performed. You can see some examples at GitHub - swiftlang/swift-android-examples that use a variety of techniques like the swift-java project. The underlying technology here is the Java Native Interface (JNI), the venerable foreign function interface for communicating between Java/Kotlin and native languages like C, C++, and Swift. You generally do not want to be writing JNI "by hand", so whether you use something like the Java bindings created by the swift-java project or the Kotlin bindings generated by Skip's bridging mode (see Native Swift on Android, Part 3: Sharing a Swift Model Layer | Skip), you'll definitely want some automated tool to assist in this part.
Ultimately, which technology you choose will be highly dependent on your team, your needs, and your chosen deployment platforms. Despite my biases towards Swift, KMP does have some distinct advantages: it has been around a lot longer and so it is more mature, it has good tooling support (benefitting from JetBrains being the creator of both KMP and Android Studio), and it provides a smooth upgrade path towards a full-stack app-building solution like Compose Multiplatform (an entirely different topic that has a different set of trade-offs, especially on the iOS side). Some disadvantages of KMP are that it is difficult to debug on the iOS side, it bloats the size of the iOS app, it sometimes forces Kotlin-ish idioms on your iOS UI layer, and it doesn't have support for @Observable and other niceties that SwiftUI developers have come to rely on.
Both these technologies have the same fundamental benefit: you can share a single codebase between your apps and not have to write everything twice, or resort to some third "alien" language like Dart (Flutter) or JavaScript (RN). They are both vendor-recommended languages for their respective platforms, so they are certain to be well-supported for a very long time, and they both have first-class IDE support in their respective domains. For an excellent presentation on using Swift on Android in the real world, you might be interested in watching @pcifani's talk at NSSpain: https://www.youtube.com/watch?v=EIGl6GOo210
one thing about kmp is that since it exports objective-c, exceptions are going to be something you wont be able to catch (c++/obj-c exceptions) from swift unless you manually add some kind of handler on the kmp side and annotate your kotlin code for each fun that can throw (which is tedious and easy to forget)
also, as you could then also guess, since everything is a reference (objective-c object) your going to be dealing with reference semantics for your data objects which will cause potential bugs if you need mutation. on kotlin you can make everything a val (aka let in swift) and still change a single parameter via copy() but in obj-c/swift side your going to be making full-on static extensions to copy every parameter for everything which is again tedious
you will also be dealing with very idosyncratic code for swift developers where you have to deal with “companion objects” instead of initializers and strange autogenerated functions/accessors. to say nothing of the huge mess of autogenerated obj-c headers which are almost human unreadable
kpm also doesn’t play well with swiftui so you will need to bridge observable objects/@observable with kotlin logic side etc
lastly, the kotlin garbage collector will be humming in the background consuming around 5% of cpu (last time i measured) and occasionally be a source of crashes since you have two memory management models causing leaks etc
there are many issues that make swift apps a kind of second-class citizen compared to android, so if you are a primarily ios/swift shop i would recommend against it (where i have worked we ripped out kmp and move our biz logic server side where we have teams managing that anyways)
and speaking of teams, the biggest issue with a cross-platform logic layer is your now dealing with code-deploy issues where changes that fix some issue in android may cause an exception or altered behavior on ios, and development can take place only in android studio which means ios devs familiar with xcode or other tools will need to fully build and deploy an xcframework then have some step to reload that on xcode (or just close/open again) etc.
regardless if you use swift-android or kmp, i’d stongly recommend having a dedicated team that can respond to bugs/requests handle automatic deployment and write proper unit tests rather than burden android/ios teams with dual maintenence because more than anything because a slowdown in your dev cycle can be a big issue (small digression: its too bad appcode was dropped from jetbrains, might have made kmp much easier)
edit: one other small issue is that kotlin in kmp isnt the same kotlin you use for android in that many libraries cant be used in kmp and there are different annotations available etc, so you are still going to suffer the “third alien language” issue just to a lesser extent than say rust or dart
True, you can't avoid those pain points, no matter the path you choose. On the other hand, compared to the (bloated) Swift runtime, KMP sits on top of bigger foundations to depend on, and harder to get rid of (a JVM). I believe that Swift will improve this aspect over time, whereas Kotlin is kind of limited by the overall approach. But man, to this day, adapting a complex Swift codebase to Android is by no means a piece of cake.
The core Swift libraries aren't all that large. If you just depend on the standard library, it will only add a few megs worth of dependency .so files. It is only once you include Foundation that it does really inflate things, largely because of the chonky FoundationInternationalization (see Android app size and lib_FoundationICU.so) but also because FoundationNetworking has to bundle libcurl and boringssl. That being said, most useful Swift libraries do have a Foundation dependency, so it is something we are actively trying to improve on.
But yes, KMP (and RN, and Flutter) all bring along an entire managed runtime with garbage collection and the works. Contrast with Swift on Android, where it doesn't bring a "runtime" per se: you can call into a Swift function from Android and it will be as efficient as if you had called a C function. There are no GC threads or other overhead that are associated with spinning up an entire managed runtime.
Swift's value as a cross-platform app development language should be judged not just in terms of what it brings to the Android side of your app, but also in terms of what it does not detracted from the iOS side.
Yeah, sorry, I tend to mix that Foundation (the real bloat) is not technically part of the standard Swift library. However, it's almost ubiquitous because Xcode includes the import by default, and Apple devs (who make the vast majority of Swift devs) are not told that even basic types like Data, Date, and URL have major footprint implications outside the Apple ecosystem.
But as I already said, I've seen how these things about Swift are improving over time, whereas depending on a JVM or a web engine feels like a point of no return to me.
I’ve been experimenting with Swift on Android recently, and my first impression is that it actually feels more at home there than KMP ever has on iOS. I’ve been using KMP alongside Flutter in a fairly complex application and been very happy with it overall, but it does involve quite a bit of boilerplate, whereas Swift on Android feels a bit more idiomatic to me. KMP still wins on platform coverage, of course — we also use it as a JS library on the web.
For a new project I’m starting, I was originally planning to use Flutter or KMP, but I’m not keen on how Material Design looks on iOS. Instead, I’m going to give Swift on Android a proper try. I want to give my iOS users the best possible native experience, but I also don’t want to rewrite everything for Android, and being able to share the Swift business logic across both platforms feels like the best middle ground.
I would implement the UI separately for each platform, but would still try to share core business logic. I am currently following that approach in porting my previously Mac-only desktop app to Windows (with a shared Swift/C++ core).
Same here, taking a SwiftUI app "everywhere". What are you using for the Windows frontend? For the business logic, I'm trying to expose a sort of C API from Swift (with @_cdecl) with JSON as the exchange format for the domain entities, which I map to native language models with a codegen (Swift, Kotlin, C/C++).
I'm curious to hear how others are approaching this problem.
Great discussion! Very happy to read up on your experiences as using a multi-platform solution is something that comes up in our organisation regularly.
What do you mean here? Having a separate team that focuses on the shared code part and owns that completely? So you would have three teams: an iOS 'UI' team, an Android 'UI' team and a multiplatform 'business login' team?
Indeed, I can very well imagine the additional complexity of managing the separate runtimes, increased debugging complexity, impact on pipelines and toolchains quickly ends up eating up all the time saved sharing code.
(emphasis mine)
This is indeed the solution we are currently using as well and so far works really well for our use cases.
No. We tried to have common logic in C++ but at the end of the day that didn't work for us long term, basically because of this:
We were a small shop and it was easier and cheaper for us to hire two Android/Kotlin junior developers than one more experienced one who knew and could maintain and debug C++ code (in addition to knowledge of Android). I also remember extra inefficiency of passing objects to and from C++ which was more of a problem with C-sharp <-> C++ interaction rather than Kotlin <-> C++ interaction via JNI, but the latter was also far from zero cost, and the most disgusting was the resulting need of using NSObject on Apple platforms. So, yeah, we've been though it, done it, and it worked reasonably well, but it was way higher complexity due to additional layer to maintain and debug, trickier to attract the needed talent, the overall speed of development was slower, and the overall size reduction was probably negative. Hence we decided to drop the "do it once via harder means" approach and switched to "do it natively on each platform". IIRC that journey costed us about two years x 3 people before we returned to the right track. YMMV.
Ok, in my case the macOS and Windows apps are both written in Swift, calling into a shared core written in Swift (the C++ is actually hidden inside the Swift core library). So it’s Swift throughout. Maybe that’s what makes the approach of sharing core logic feasible for me.
yep, the coordination of having a binary target that you have to deliver and sync between ios/android can be a productivity hole because now ever other team has to care about the other and deploy fixes to each other… (and it gets worse if you start sharing between multiple apps) so its better (in my experience anyways) to just have a few people (or even just one person) devoted to maintenance/management and development; the code quality and consistency and deploy management will be so much better in that scenario
That is a good question. When deciding whether to split your app into model/UI halves and bridge between languages, the benefits of a shared model layer are mostly felt when the model (and any dependencies of the model) is a large percentage of the app's overall codebase. Some apps are very model-heavy, whereas others are mostly UI. In these latter cases, using a single shared codebase might simply not be worth it, since you will still need to coordinate between separate teams for creating and ongoing maintenance, plus you will need to always be managing bridging considerations when cross language boundaries on one of your platforms (in Swift's case, Android; in KMP's case, iOS).
It's these ongoing coordination costs that kills productivity, especially with large teams (see https://allenpike.com/2021/gravity-of-cross-platform-apps), and has been cited multiple times as the reason why a popular app has regressed from a nice native implementation to a less-nice non-native cross-platform reimplementation.
This is the reason Skip encourages a holistic "whole stack" approach, where the entire app is written in Swift, and so the entire codebase is shared between the iOS and Android platforms. That way it doesn't matter if the app is model-heavy or UI-heavy: it is all just Swift code using the platform's native interface components.