Building user-facing applications can be challenging. Widget or GUI toolkits provide essential abstractions for OS-level capabilities like graphics, sound, reusable widgets and event handling. There are a large number of these toolkits available. Let’s review some of the differences in their design.
Definitions
Many toolkits are single-platform, designed for a specific operating system and programming language. e.g. Cocoa on macOS is paired with Swift, and cannot be used on Windows.
The largest source of single-platform frameworks are operating system vendors (aka vendor-frameworks) who produce frameworks to help people build applications specifically for their platforms. This is why Carbon (Apple) or WTL (Microsoft) will never be ported to a different OS: those vendors have little interest in making it easy for you to build software for a different, competing OS.
This also means that new OS features are introduced in vendors-frameworks first. Third-party frameworks will always need to “catch-up” when new features are released.
Single-platform toolkits will always have the most-recent features and provide the best performance. However, using them incurs the cost of writing platform-specific code for each platform that you target.
Cross-platform toolkits are designed so that user-interface code can be written once and reused across multiple platforms. Cross-platform toolkits reduce the cost of building and deploying software by allowing us to reuse our user-interface code.
It’s important to recognize that sharing UI code is only part of the solution. Applications include more than just interfaces, so to maximize reuse, we also need to pair them with cross-platform programming languages (and other shared libraries).
Toolkit Approaches
What at the high-level tradeoffs to each approach?
Vendor-platforms
- The best support for new OS features.
- The tightest integration to the underlying operating system, and typically the best performance.
- Sets the “standard” look-and-feel for applications on that platform. Vendor provided applications will typically match the framework’s design language (“native”) and deviation from that as seen as un-desirable (“non-native”).
Single-platform
- “Worst of all worlds”. Disadvantages of being single-platform, but usually few gains.
- Why would you do this and not just use the vendor-framework? Probably because you want to use a programming language not supported by the vendor.
Cross-platform
- Build-once, run anywhere. Maybe.
- When paired with a cross-platform language, the ability to have an application that can be easily be built for multiple target platforms.
How are cross-platform toolkits designed? There’s two high-level approaches:
- Design the cross-platform framework as a wrapper that just invokes underlying native APIs to draw native widgets. This produces native UIs (great!) but requires a lot of work to maintain the bindings, especially when vendors modify these APIs regularly (and are likely not motivated to help you maintain a competing platform).
- As much as possible, replace the underlying API functionality with code that can actually be compiled on each target platform. This can be done by, for example, having a native
canvas
exposed from the native API that the toolkit can use to draw the UI. This produces mostly cross-platform code (great!) but extra work has to be done to re-implement the look-and-feel of native APIs, or you need to accept a non-native UI. Performance may also be a concern compared to a more native solution.
As you probably know, I’m a fan of cross-platform solutions. It’s more up-front work, but it’s absolutely a positive direction for the industry a whole.
There was a time when software used to advertise hardware compatibility (“Works with HP Laserjet!”). Why was this necessary? We didn’t have appropriate abstractions for devices, so developers needed to write code specifically for each device they wanted to support. What changed?
We started to hide implementation details for graphics, printing, networking behind powerful abstractions that allowed us to build to a standard API. We don’t write code for specific graphics cards anymore, because we have powerful abstraction layers in OpenGL and DirectX. Building high-performing user interfaces is one of the final areas when we still, somehow, accept that we should be rewriting the same code for each platform.
I’m not sure that any single cross-platform framework will be the solution, I’m strongly in favour of less platform-specific code (and less vendor lock-in).
Comparison
Let’s compare some recent cross-platform solutions.
Flutter (Dart)
Flutter is an open-source cross-platform toolkit developed by Google. Currently, it can be used to target web, mobile (iOS, Android) and desktop (Windows, macOS, Linux) platforms. It was released in 2018, and is regularly updated by Google.
Feature | Details |
---|---|
Programming language | Dart (declarative) |
Packages/libraries | Dart’s pub package manager |
Engine | Flutter (canvas using Skia) |
Flutter’s engine is the native component (written in C++) that uses Googles Skia graphics library to draw widgets on a native canvas for each platform1. It is also able to interface with platform-specific APIs, through it’s plugin ecosystem.
The Flutter framework contains two sets of widgets: Material Design widgets that implement Google’s Material Design, and Cupertino widgets that implement Apple’s iOS Human Interface Guidelines. The Flutter framework is written in Dart, and supports application development using the Dart programming language. There is no ability to use Flutter UIs from other languages.
Compose (Kotlin)
Compose was originally released by Google as Jetpack Compose for Android in 2017. It has since been ported to desktop/JVM by Jetbrains, and released as Compose Multiplatform in 2021. There are currently pre-release projects to port Compose Multiplatform to iOS and Web as well.
Feature | Details |
---|---|
Programming language | Kotlin (declarative) |
Packages/libraries | Maven, any Java library |
Engine | Compose (canvas using Skia) |
Compose’s engine is also a high-performance runtime that also uses Google’s Skia graphics library to draw widgets on a native canvas. Kotlin Multiplatform support allows easy native integration to underlying APIs. For example, it’s possible to mix both native UI and Compose UI elements with Kotlin.
Compose includes Google’s Material widgets (the original implementation from Android). There is no supported set of iOS-themed widgets, but you can easily customize the look-and-feel. There are also some third-party themes that attempt to mimic other environments.
The observant will notice that both of these solutions are heavily supported by Google. Flutter/Dart was under development when Kotlin was released, and Google quickly adopted Kotlin as their primary language.
Which one will “win”? Probably both.
- Flutter is further advanced across platforms. It’s probably the better choice if you need to deliver an application now on iOS or web.
- Flutter (I believe!) has larger market share and is more mature.
- Kotlin has a larger ecosystem to draw up, especially since it supports 100% Java compatibility.
- Kotlin has better native interop and is likely a better solution if you want to incorporate native UI capabilities.
- Given the pace of development, Kotlin may catch-up to Flutter in 1-2 years.
It feels like Flutter is more complete and polished at this point, but Kotlin might have some long-term advantages.
Reference
#compose #flutter
-
Skia is a high-performance library also used in Google Chrome. ↩︎