Skip to content

transpiler

2 posts with the tag “transpiler”

The Flutter Kerfuffle

News recently broke that Google is laying off members of the Dart and Flutter teams. At the same time that Microsoft is ending Xamarin support, many Flutter users are left wondering about the future of the technology stack on which they’ve built their apps.

For what it’s worth, we don’t buy into the speculation that Flutter is about the be killed off. While we believe that Skip has the correct approach to cross-platform mobile development, Flutter is an excellent product with a devoted community. A recent post by an insider notes that the Google layoffs were broad and didn’t single out the Flutter and Dart teams. Nevertheless, this episode emphasizes the need to always consider the ejectability of the core technologies you use.

“And every one who hears these sayings of Mine and does not do them will be likened to a foolish man who built his house on the sand.”

A software framework is said to be ejectable if you can remove it without completely breaking your app. Ejectability is not binary: having to remove a given framework might only require you to excise certain features, and some frameworks are easier to replace in our apps than others.

Unfortunately, most cross-platform development tools are not ejectable to any degree. If official Flutter support were end-of-lifed, there would not be any path for Flutter codebases to migrate to another technology stack. Dart is a nice language and Widgets are cool, but Apple is not about to transition from Swift and SwiftUI, nor Android from Kotlin and Jetpack Compose. Languages like Dart and Javascript will forever be “alien” on both iOS and Android, just as Swift and Kotlin will be the primary, supported, and vendor-recommended languages for the foreseeable future.

Flutter apps would continue to work, and the community would likely maintain the Flutter engine and libraries for quite some time. But the pace of integration with the latest Android and iOS features would slow, and businesses who care about the long-term prospects for their apps would look to move on. Unfortunately, their only option would be something seasoned developers avoid for good reason: a complete re-write.

To be clear, we do not believe that Flutter support is going to end any time soon, and the same goes for other popular cross-platform solutions like React Native. But you should also not take the decision to use these frameworks lightly, because they are not ejectable. If support unexpectedly ends - or if a new Apple or Android platform or feature arrives that these non-native frameworks are not able to integrate with - you have no off-ramp.

Skip’s approach to cross-platform development is fundamentally different. You work in a single codebase, but Skip creates fully native apps for each platform: Swift and SwiftUI on iOS, and Kotlin and Compose on Android. This is in keeping with the plainly-stated, unambiguous advice from the platform vendors themselves:

SwiftUI is the preferred app-builder technology -Apple

Jetpack Compose is Android’s recommended modern toolkit -Google

A cross-platform Skip app is a modern iOS app. Your shared code is written in Xcode using Swift and SwiftUI. For example, here is a simple weather model:

import Foundation
struct Weather : Decodable {
let latitude: Double
let longitude: Double
let time: Double
let timezone: String
}
extension Weather {
/// Fetches the weather from the open-meteo API
static func fetch(latitude: Double, longitude: Double) async throws -> Weather {
let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)&current_weather=true")!
var request = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(Weather.self, from: data)
}
}

At build time, the Skip tool integrates with Xcode to transpile your iOS codebase into a native Android app written in Kotlin.

As you can see in the sample above, your Swift code does not require any dependencies on Skip. Even integrating Android customization into your app is accomplished without Skip libraries. If you remove all traces of Skip, your iOS app will continue to build and run in Xcode exactly as before.

On the Android side, the Kotlin source code and artifacts that Skip generates are yours, regardless of whether you continue to use Skip. Unlike the iOS app, the translated Android app does rely on Skip libraries to mirror the Foundation, SwiftUI and other Apple APIs that your iOS code likely uses. These libraries, however, are free and open source. Critically, they are not a “runtime” or “engine”. You can continue to use them - or not - without hampering your ability to expand the app and integrate new Android features.

Here is the Kotlin that Skip generates from the sample weather model above. It is not significantly different than Kotlin you’d write by hand. It is longer than the source Swift only because Kotlin does not have built-in JSON decoding, so Skip must add it. (SwiftUI code translated to Skip’s Compose-based UI library for Android is much less idiomatic. If you stopped using Skip you’d likely want to migrate to pure Compose over time, but there would be no immediate need to do so.)

import skip.foundation.*
internal class Weather: Decodable {
internal val latitude: Double
internal val longitude: Double
internal val time: Double
internal val timezone: String
constructor(latitude: Double, longitude: Double, time: Double, timezone: String) {
this.latitude = latitude
this.longitude = longitude
this.time = time
this.timezone = timezone
}
constructor(from: Decoder) {
val container = from.container(keyedBy = CodingKeys::class)
this.latitude = container.decode(Double::class, forKey = CodingKeys.latitude)
this.longitude = container.decode(Double::class, forKey = CodingKeys.longitude)
this.time = container.decode(Double::class, forKey = CodingKeys.time)
this.timezone = container.decode(String::class, forKey = CodingKeys.timezone)
}
companion object: DecodableCompanion<Weather> {
/// Fetches the weather from the open-meteo API
internal suspend fun fetch(latitude: Double, longitude: Double): Weather = Async.run l@{
val url = URL(string = "https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true")
var request = URLRequest(url = url)
val (data, response) = URLSession.shared.data(for_ = request)
return@l JSONDecoder().decode(Weather::class, from = data)
}
override fun init(from: Decoder): Weather = Weather(from = from)
private fun CodingKeys(rawValue: String): CodingKeys? {
return when (rawValue) {
"latitude" -> CodingKeys.latitude
"longitude" -> CodingKeys.longitude
"time" -> CodingKeys.time
"timezone" -> CodingKeys.timezone
else -> null
}
}
}
private enum class CodingKeys(override val rawValue: String, @Suppress("UNUSED_PARAMETER") unusedp: Nothing? = null): CodingKey, RawRepresentable<String> {
latitude("latitude"),
longitude("longitude"),
time("time"),
timezone("timezone");
}
}

(Skip is not a cloud-based tool, but there is an online playground where you can experiment with Swift-to-Kotlin transpilation.)

Skip is fully ejectable. When you eject Skip, you are left with a native iOS app and a native Android app, both using their respective platform vendor-recommended technologies. You can immediately continue to iterate on these apps, with no rewrites and no pause in your ability to integrate new platform features.

We believe that Flutter’s future is secure, despite what some commentary has speculated. You should, however, carefully consider ejectability alongside the many other factors that go into choosing the cross-platform framework that’s right for your business needs.

Bringing Swift and SwiftUI to Android

Swift and SwiftUI are Apple’s recommended technologies for app development, and with good reason. Their emphasis on safety, efficiency, and expressiveness have made it easier than ever to build fast, polished, and robust apps for the Apple ecosystem.

Recent stories about Swift on Windows, Swift on the Playdate, and a SwiftUI-like library for Gnome highlight developers’ desire to take advantage of Swift and SwiftUI in other environments too, and advances in Swift tooling have facilitated the porting process. We’re excited to share how Skip combines several Swift platform technologies to bring Swift and SwiftUI development to Android.

Cross-platform development in Xcode

There are multiple paths to supporting Swift on Android, and - like everything in engineering - each comes with its own set of tradeoffs. We chose transpilation as our primary mechanism. Transpiling your Swift source to Android’s native Kotlin language maximizes interoperability, allowing you to call Kotlin and Java APIs directly from Swift - a key concern when you want to take advantage of Android-specific features in your apps.

Our Swift-to-Kotlin transpiler is powered by SwiftSyntax, and we use use Swift Package Manager to both invoke the transpiler as part of the build process, and to integrate our suite of open source Android libraries.

Diagram of Skip's Swift-on-Android build process

The result is a workflow in which you work in Xcode, writing standard Swift and SwiftUI. Our build plugin leaves your source code untouched, but generates, packages, and builds the equivalent Kotlin and Jetpack Compose alongside it. One Swift and SwiftUI codebase, two fully native apps.

Let’s take a closer look at the Swift platform technologies that make this possible.

SwiftSyntax is an open source Swift library by Apple that provides powerful tools for parsing and transforming Swift source code. SwiftSyntax has existed for some time, but it has only recently risen to prominence as the library powering Swift macros.

Our transpiler uses SwiftSyntax to parse your Swift code into a highly detailed syntax tree. Once we have this tree, we’re able to analyze it and translate it into an equivalent syntax tree for Kotlin, the modern JVM-based language used in Android development. The fidelity that SwiftSyntax provides not only allows us to perfectly capture the semantics of the Swift source, but even to preserve your comments and formatting. The Kotlin we output is often indistinguishable from hand-written code.

Example Swift:
protocol Action {
associatedtype R
var name: String { get }
func perform() throws -> R
}
/// Action to add two integers
struct AddAction: Action, Equatable {
let lhs: Int // Left hand side
let rhs: Int // Right hand side
var name: String {
return "Add"
}
func perform() -> Int {
return lhs + rhs
}
}
Transpiles to:
internal interface Action<R> {
val name: String
fun perform(): R
}
/// Action to add two integers
internal class AddAction: Action<Int> {
internal val lhs: Int // Left hand side
internal val rhs: Int // Right hand side
override val name: String
get() = "Add"
override fun perform(): Int = lhs + rhs
constructor(lhs: Int, rhs: Int) {
this.lhs = lhs
this.rhs = rhs
}
override fun equals(other: Any?): Boolean {
if (other !is AddAction) return false
return lhs == other.lhs && rhs == other.rhs
}
}

ViewBuilders - a special case of Swift’s ResultBuilders - lie at the heart of SwiftUI’s easy-to-use syntax. SwiftSyntax is able to perfectly parse these as well, but this is one area where our output does not look hand-written. Kotlin doesn’t support the expressive ViewBuilder syntax, and Jetpack Compose - Android’s modern UI framework - is based on nested function calls instead. The transpilation from ViewBuilders to function calls is effective, but it results in mechanical-looking code.

You can see all of this in action using our online Swift-to-Kotlin transpiler playground. While it doesn’t replicate the integrated Xcode experience of the real thing, it is fun to experiment with, and it demonstrates the speed and sophistication of SwiftSyntax.

Translating Swift into Kotlin is interesting, but a complete cross-platform solution must also integrate with your development workflow, support the Swift and SwiftUI APIs you’re accustomed to using, and scale to multi-module projects. For these needs, we leverage Swift Package Manager.

Swift Package Manager (SwiftPM) is the standard dependency management tool for Swift projects, and it has become an integral part of the Swift ecosystem. We use SwiftPM’s plugin support, dependency resolution, and module system.

Swift Package Manager Plugins are a way to extend the functionality of SwiftPM. They allow developers to securely add custom commands or behaviors to the package manager. Parts of the plugin API are specifically designed for reading source code and generating additional source code, and we utilizes these capabilities to invoke our transpiler. Thanks to Xcode’s seamless SwiftPM integration, this happens transparently on every build, and any transpilation errors are surfaced right inline.

We maintain a suite of open source libraries to mirror standard frameworks like Foundation, Observation, and SwiftUI for Android. SwiftPM allows you to easily integrate these libraries into your project, keep them up to date, and manage their transitive dependencies. Because SwiftPM’s Package.swift files have all the capabilities of Swift, we can add logic allowing you to exclude these Android libraries when performing Apple platform release builds, keeping your Apple releases free from any dependencies on Skip.

As the size of a project grows, so does the importance of modularization. SwiftPM makes it as easy as possible to break up your code into modules that you can test and iterate on independently. Compartmentalizing your codebase can also significantly improve compilation speeds, as modules that haven’t changed don’t need to be recompiled. We’re able to use this optimization as well, avoiding re-transpiling and recompiling for Android when a module hasn’t been modified.

Unit testing is critical for verifying functionality and ensuring that what worked yesterday will still work tomorrow. This is doubly important for code that runs on multiple platforms.

XCTest is Apple’s native framework for writing and running unit tests in Swift. Through our open source SkipUnit library, we support the XCTest API on top of JUnit, the venerable Java unit testing framework.

Diagram of Skip's XCTest-on-Android test process

Being able to run a unified set of Swift unit tests across your Apple and Android targets is a critical aspect of any Swift-on-Android solution. In fact the Skip modules themselves rely heavily on this testing support: we use GitHub actions to run our suite of Swift unit tests across both iOS and Android on every commit to prevent regressions.

While Swift and SwiftUI are often associated with development for Apple devices, their principles and paradigms are universal, and their use across platforms is spreading. Advances in the Swift ecosystem have unlocked powerful integration possibilities. We leverage these advances to bring Swift and SwiftUI to Android with Skip, allowing you to create fully native cross-platform libraries and apps from a single Swift and SwiftUI codebase.