I’ve been involved in cross-platform development since the early days of my career, from Cordova to React Native, targeting everything from Smart TVs to Android, iOS, macOS, Windows (and even the dearly departed Windows Phone - RIP my Lumia 720). I wouldn’t claim to be a master of all, but I’ve definitely picked up some insights along the way. Before my memory fades completely, I wanted to jot down a two-part series reflecting on the evolution of cross-platform development and summarizing my experiences.
This first part is a trip down memory lane, revisiting and briefly introducing various solutions. Keep in mind that my memory might be a bit fuzzy on some details, and things might have changed since then. Feel free to correct me if I’m wrong!
Cordova was an early player in the cross-platform game, branching out from the even older PhoneGap. Back when I started dabbling in cross-platform development (around 2012), it was the hottest thing around. Why? Because it was dead simple to pick up, especially compared to its competitors at the time, Xamarin and Titanium, both of which were paid solutions.
Cordova’s architecture is straightforward: a basic Activity (I’ll be using Android terminology for simplicity’s sake, not because I’m clueless about iOS, I swear!), hosting a CordovaWebView component. This souped-up WebView came equipped with Cordova APIs, allowing you to communicate with native components. Essentially, your app was a glorified mobile web app with some extra native communication skills.
This elegantly simple design gave Cordova several advantages. Firstly, it was incredibly easy to learn. Front-end developers (or web developers, as they were still commonly known back then) could churn out cross-platform apps without breaking a sweat, just by leveraging their existing mobile web development skills. Sounds tempting, right?
However, as with most things in life, it wasn’t all sunshine and rainbows. Cordova’s simplicity was a double-edged sword. On the one hand, it was essentially a web app, so you could throw any web development trickery at it. On the other hand, it was just a web app, and no amount of lipstick could turn it into a native experience. Back then, mobile hardware wasn’t exactly top-of-the-line (we’re talking 1GB RAM territory), and the performance gap between mobile web and native views was painfully obvious, especially in terms of UI responsiveness. On Android, the difference between a web UI and a native UI was stark, and most users could easily tell them apart (iOS devices fared slightly better due to their different rendering architecture, but that’s a story for another time).
Naturally, web developers weren’t going to take this lying down. They came up with all sorts of clever hacks (mostly involving GPU acceleration) to improve the Android UI experience. Among them, Ionic stood out from the crowd. This was a time of rapid growth for mobile web UI frameworks, with contenders like jQuery Mobile, Sencha Touch, Kendo UI, and Bootstrap popping up left and right. Ionic managed to rise above the noise thanks to one word: “polish.” They didn’t offer a massive library of UI components (in fact, they had fewer than most), but the ones they did provide were buttery smooth. They obsessed over even the tiniest UX details, ensuring a delightful user experience. This, coupled with their tight integration with AngularJS and meticulous optimization for Cordova WebView, made them the go-to choice for Cordova app development.
Sadly, no matter how hard the Ionic team and the wider web development community tried, they couldn’t overcome the inherent limitations of the architecture. This paved the way for the next generation of cross-platform solutions.
(Side note: Cordova’s UX shortcomings, stemming from its architecture and the hardware limitations of the time, coupled with its widespread adoption, unfortunately, led to a pervasive belief that “cross-platform development = poor performance and bad UX.” This became a major hurdle for subsequent cross-platform solutions, often requiring lengthy explanations to convince skeptical developers that things had changed since the Cordova days.)
Xamarin had already made a name for itself before being acquired by Microsoft. However, unlike the open-source Cordova, Xamarin came with a price tag, which hampered its popularity compared to its free counterpart. Another factor contributing to its lukewarm reception was its choice of language: C#. Now, don’t get me wrong, C# is a perfectly fine language (in fact, it was my gateway drug into the world of programming), but back then, before the advent of .NET Core (and Oracle’s litigious tendencies), the C# community was relatively insular, with a smaller pool of open-source resources compared to the rapidly growing JavaScript ecosystem. This put Xamarin at a disadvantage from the get-go.
Xamarin’s architecture was more complex than Cordova’s. It involved a lot of moving parts, including a complete set of Android & Java bindings and a cross-platform .NET runtime called Mono (technically, Mono predates Xamarin, but they were developed by the same company, and Xamarin was Mono’s claim to fame back then). Xamarin can be roughly divided into several components: Xamarin.Android, Xamarin.iOS, Xamarin.Mac (a later addition), and Xamarin.Forms. Since Microsoft acquired Xamarin, they’ve done a stellar job with documentation (take notes, Facebook!), so I’ll just briefly touch upon the pros and cons of this architecture. For a deep dive, refer to the official documentation.
The main advantage of Xamarin’s architecture was its minimal performance overhead. Compared to Cordova, which suffered from the inherent performance limitations of web rendering, Xamarin only had to deal with the overhead of cross-language interoperability, which was relatively negligible. This translated into a significant UX advantage. Of course, this architecture wasn’t without its drawbacks. The biggest pain point was the reliance on official bindings for new native APIs. Thankfully, Xamarin has since embraced the open-source world (thanks to a reader for pointing this out!), so you can always roll up your sleeves and create your own bindings if you’re feeling adventurous. Another minor gripe was its size, but considering the hefty nature of its successors, it wasn’t that bad.
Sorry, I don’t know that guy very well. I just remember he also charged for his solution, which used JS to Native Binding. But I heard it was super buggy, so I steered clear! You could call him a pioneer, in a way, haha.
NativeScript can be considered a second-generation cross-platform solution. Developed by Telerik (the folks behind Kendo UI), it later went open-source. Their approach, similar to Titanium’s, aimed to bind JavaScript and native code. They achieved this through reflection, leveraging the open APIs provided by V8 to manipulate the Javascript runtime (like Node.js does with CommonJS). NativeScript mirrored Android/iOS APIs into its runtime, allowing developers to directly access native APIs using JavaScript. This granted NativeScript incredible extensibility, enabling the use of any native API, including third-party libraries, without extra steps. Platform API changes were immediately available (unless reflection broke during updates), unlike Xamarin, which required team updates. Pretty cool, right?
(Note: NativeScript doesn’t directly use Java reflection for dynamic API lookup due to performance issues. Instead, it pre-generates metadata within the APK for lookup and mapping.)
NativeScript also excels in the View department. Besides native components, it offers a CSS subset for styling, simplifying UI development compared to native apps. (Side note: It’s a shame NativeScript doesn’t use React Native’s Yoga for CSS compilation. Unifying resources could open up more possibilities.) Initially, NativeScript used Angular for UI, but later separated into three development approaches: basic js/ts, Angular, and Vue (yes, that’s why I didn’t bother with Weex). All three enjoy the convenience of data-binding. Awesome, right?
Another major advantage is NativeScript’s first-class TypeScript support (it’s also written in TypeScript). This makes sense considering the reflection mechanism. Without TypeScript, API refactoring would be a nightmare (imagine writing Java in Notepad!).
However, this architecture has drawbacks. First, developers need some native knowledge. Since NativeScript’s runtime is based on V8/JavaScriptCore, not Webkit, you can’t use Web APIs (unless someone implements them separately). For example, XmlHttpRequest or fetch, belonging to Webkit, require native platform implementations in NativeScript. (Interestingly, while searching for implementations, I stumbled upon issues opened by Vj, a React Native core developer, on NativeScript, suggesting differing views on implementation.) This increases reliance on native knowledge. However, NativeScript simplifies this by allowing third-party library mapping into JavaScript with just a .d.ts file, unlike React Native’s constant bridging.
Overall, I consider NativeScript a great cross-platform solution. Reflection provides vast extensibility, and its UI integration with modern Angular/Vue boosts development efficiency. Its lack of popularity stems from unfortunate timing. React Native, backed by the React community, emerged just as NativeScript was finding its footing. Had it arrived earlier, the landscape might look different today.
“Strength in numbers” perfectly describes React Native. Mention cross-platform development in 2018, and everyone (except those stuck in the Cordova era) will bring up React Native. I was among the first to use it in production (version 0.0.3, the first public release – I was a brave soul!). Let’s explore why it’s so popular.
React Native shares NativeScript’s goal of “controlling Native with Javascript” but takes a different approach. Instead of reflection, it builds an RCTBridge for communication between JavaScript and native. Using native APIs or components requires bridging on the native side and exporting to JavaScript (similar to writing C++ addons for Node). This results in smaller package sizes and simpler architecture but requires bridging for each native module. Honestly, I find NativeScript’s approach superior, but React Native must have its reasons. I haven’t found official discussions on this; maybe I’ll open an issue someday.
React Native’s success is largely attributed to the thriving React ecosystem and Facebook’s implementation in their app, providing confidence for many. Adoption by giants like Airbnb and Uber further fueled its rise. However, React Native isn’t perfectly stable yet, with bi-weekly updates and frequent breaking changes, requiring developer attention.
Another important aspect: you need native platform knowledge. Most post-Cordova cross-platform solutions demand it. While you can build a functional app with React Native without any native knowledge, it limits extensibility (relying on third-party or official plugins) and risks performance pitfalls without understanding native rendering and operations. (I’ve seen many performance complaints stem from misunderstanding API principles and implementing weird workarounds.)
React Native came onto the scene promising “learn once, write everywhere.” Instead of chasing the elusive “write once, run everywhere,” they focused on letting each platform keep its unique feel. This core idea made it super easy to extend React Native to other platforms. We’re talking third-party projects like react-native-macos
and react-native-appletv
(which officially joined the club), Microsoft’s own react-native-windows
(it started as UWP, but then UWP got kinda awkward…), and even Samsung jumped in with react-native-tizen
. You can find React Native trying to get a foothold on almost every platform (though how well it works is another story).
Overall, React Native is a pretty solid choice for cross-platform development. It’s not perfectly stable yet, but it’s got a strong foundation, it’s easy to extend, and there’s a huge community behind it. Right now, there aren’t any major red flags. If you’re thinking about dipping your toes into cross-platform development, learning React Native is a good bet.
(What do you mean, why is there a desktop framework here? I said “cross-platform,” not just Android and iOS! You gonna fight me? Punk?)
Electron started life as Atom-Shell, a side project from GitHub when they released their open-source Atom editor. But this little side project became way more popular than Atom itself, so they gave it a new name and spun it off into its own thing. That’s how we got Electron.
Electron is pretty simple at its core: it’s just Chromium + Node.js, talking to each other with IPC (there’s an older project called Node-Webkit that does something similar, but I haven’t used it, so I won’t go into it).
I’m not sure what else to say about Electron, because it’s just so darn mature. In 2018, building an Electron app is a walk in the park for any JavaScript developer worth their salt. The front end is a full-fledged web environment, powered by the ever-reliable and cutting-edge Chromium, so you can use all the latest and greatest JavaScript features and web APIs. The back end is our good friend Node.js, and it’s a pretty recent version, so you don’t have to worry about missing language features or module support.
Electron doesn’t skimp on platform integration either. Thanks to the thriving Node.js ecosystem, you can find a module for pretty much anything you need. And Electron and its community have you covered for common app features like auto-updates and packaging. Just like the website says, Electron takes care of all the boring stuff so you can focus on building your app. It just works!
If I had to pick a flaw with Electron, it would be… its weight. It’s like a curse that all cross-platform solutions have to bear (sigh). Since you’re bundling the entire Chromium browser, even the smallest “Hello World” app ends up being tens of megabytes after compression. This isn’t a huge deal for desktop software, but it does have two side effects: memory usage and startup time.
Because it uses the notoriously resource-hungry Chromium, Electron has a bit of a reputation for being a memory hog. It’s not uncommon for an Electron app to gobble up 100+ MB of RAM just by existing, and that number can skyrocket if your app is complex. This is pretty much all on Chromium, so there’s not much the Electron team can do except hope that Chromium gets better about it in the future.
As for startup time, Microsoft’s Visual Studio Code is a great example of how to do it right. They use some clever lazy loading techniques to achieve a pretty snappy startup time, even though it’s a feature-rich app (especially compared to Atom). But there’s still a limit to how much you can optimize, because Chromium’s own startup time will always be a bottleneck. That’s why VS Code still opens a bit slower than Sublime Text. But in most cases, it’s not a big deal, because the difference is only about a second on a hard drive and practically unnoticeable on an SSD.
On a side note, there’s a new kid on the block called Proton Native. As the name suggests, it’s trying to take on Electron by replacing Chromium with the lightweight libui library and using React Native’s binding mechanism to interact with the UI. It’s a promising idea that could potentially make Electron apps much lighter, but it’s still very early days. I’d recommend waiting a bit before jumping on board.
So, to sum it up: if you’re building a cross-platform desktop app today, Electron is the clear frontrunner. It’s got the maturity, the community, the resources, and the flagship apps (I’m willing to bet you have at least one Electron app installed on your computer right now. And if you don’t, you will soon). It’s the safe and reliable choice. There are other options out there, like CEF, NW.js, and react-native-windows
/react-native-macos
, but Electron is still the king of the hill.
When I wrote this, Flutter was still in beta, and I wasn’t super familiar with it. But I wanted to include it anyway because it’s a really interesting project with a lot of potential.
Flutter is Google’s take on cross-platform development. It uses Dart as its programming language and can run on Android, iOS, and even Fuchsia (Google’s experimental operating system). It’s been in the works for a while (maybe 2-3 years), but it started out as this little-known project called “Sky.” Google has only really started pushing it in the last six months or so.
So, why is Flutter considered a next-gen technology? It’s because it breaks away from the norm in a few key ways.
First off, it ditches JavaScript. Most popular cross-platform solutions have been all about JavaScript, leveraging its massive ecosystem. But Flutter went rogue and chose Dart, a relatively less-known language. (There’s a whole other article explaining why, but we’ll skip that for now).
Now, ditching JavaScript is a double-edged sword. On the downside, you’re leaving behind a goldmine of resources, forcing early adopters to reinvent the wheel. But the upside of Dart seems pretty huge. Besides its language features, Dart allows for both Just-in-Time (JIT) and Ahead-of-Time (AOT) compilation. This means developers get the speed and flexibility of JIT during development, and the performance boost of AOT for the final product.
This AOT capability is a game-changer. It lets Flutter tackle the performance bottlenecks that plague solutions like React Native or NativeScript – long startup times and the overhead of a JavaScript bridge for communication with the native platform.
Another cool thing about Flutter is its own rendering engine, Skia. Google bought and open-sourced this engine, and it’s used in tons of places, like Chrome, Firefox, and Android. Every UI element in Flutter is drawn using Skia, completely independent of the platform’s UI components. This means Flutter achieves true cross-platform UI consistency.
Sounds awesome, right? Well, it has its pros and cons. The good news is you don’t have to create an abstraction layer to unify UI components from different platforms like you do with React Native. Flutter UI components look and behave the same everywhere, and native rendering means blazing-fast performance. The downside? You’re ditching years of platform-specific component development to build your own set from scratch.
Right now, Flutter’s vision is clear and ambitious. It prioritizes elegant architecture and high performance, even if it means sacrificing access to a pre-existing ecosystem. It’s a bold move, but also a risky one. Just like I mentioned in the image caption, good things don’t always become popular – tech history is full of examples. Flutter can’t rely on Google’s support forever. With such a radical approach, attracting developers to build its ecosystem will be crucial.
To sum it up, until Fuchsia (Google’s new OS) becomes mainstream, Flutter as a standalone cross-platform UI solution isn’t quite there yet. Its strengths lie in its high-performance architecture and great developer experience. But its weaknesses are the lack of a mature ecosystem and the absence of major showcases beyond Google’s own products.
So, what does the future hold for Flutter? Only time will tell.
Let’s be real, you probably scrolled straight to the bottom, didn’t you? (Don’t worry, I do it too). Halfway through writing this, I started questioning my life choices. Why not break it into multiple posts? Why subject myself and others to this wall of text? Why am I even writing this lengthy, probably-ignored article? Why doesn’t anyone love me? Why am I here?
Anyway, I’ve said my piece (a rather long one at that), but there’s more to come. Next time, we’ll lighten things up and dive into some general thoughts and insights on cross-platform development.