Long before joining Ionic, I built web-based apps (using jQuery and Knockout.js!) and deployed them to iOS and Android using Cordova. They weren’t pretty (I didn’t have something like this 😉 available), the code was messy, but they got the job done: I was a web developer building mobile apps using one codebase!
Despite my enthusiasm, I quickly ran into issues that would continue to haunt me over time.
- Limited cross-platform deployment: I wanted to make my apps available on iOS, Android, and the web. Cordova’s focus on mobile, as well as limited browser APIs, made it challenging, if not impossible, to reach all platforms successfully.
- Opaque native configuration: Builds would fail or features wouldn’t work as expected, and I struggled to solve them since I didn’t understand Cordova’s native project abstractions.
- Stability: I dreaded updating the apps because native plugins would constantly break between new mobile OS versions or conflicting plugin versions.
Those were dark times. However, I’ve recently been building a new Real App™️ using Capacitor and well, it’s made me fall in love with mobile all over again. In this post, I’ll cover how Capacitor solves all of these issues, including cross-platform support, easy native configuration, long-term stability, and the built-in Cordova migration support.
Side-note: If you’re not already familiar with Capacitor, you can find a ton of info here and here. TL;DR - it’s a new native runtime for building cross-platform mobile, desktop, and web apps, built by the Ionic team. You can think of it as a spiritual successor to Cordova that applies many hard-earned lessons from the last 10 years of hybrid mobile development.
And now, let’s review how Capacitor applies those lessons, resulting in a much-improved developer experience.
Beyond Mobile
Cordova’s focus on mobile, coupled with limited web browser APIs, made it challenging, if not impossible, to reach all platforms with a single codebase successfully.
Recognizing this, Capacitor embraces a web-first approach with its Core APIs, meaning they work on the web, iOS, Android, and desktop. Since they provide access to commonly needed functionality, they cover much of the core Cordova plugins while also including some new features.
The Capacitor Camera API is a great example. With a single method call, you can take a photo with the device’s camera on the web, iOS, and Android:
import { Plugins, CameraResultType } from '@capacitor/core';
const { Camera } = Plugins;
async takePicture() {
const image = await Camera.getPhoto({
quality: 90,
resultType: CameraResultType.Uri
});
imageElement.src = image.webPath;
}
That said, what about features that aren’t available on the web? In those cases, web plugins can be built to act as a fallback. When in doubt, check Can I Use to see what’s possible.
Additionally, I was thrilled to learn that browser APIs have evolved to become more feature-rich since I began building hybrid apps years ago. As you can see from my favorite reference site, What Web Can Do Today, device integration is more powerful than ever. Everything from Bluetooth to offline storage to virtual/augmented reality is available today.
Pairing Capacitor with these new browser APIs, I could build my app quickly in the browser like before, while also ensuring true cross-platform deployment.
Easy Native Project Configuration
Cordova leverages a single configuration file that abstracts away native project details from the developer, which is great for managing all of your configurations together. However, when project builds fail or features don’t work as expected, it’s difficult to understand what the issue is and where it’s occurring (is it Cordova tooling or native project code?) since any applied changes are a black box to web developers. As a result, it’s too easy to get stuck on a problem completely unrelated to app development.
Capacitor takes the opposite approach, fully embracing configuration via native IDEs. There are two steps to implementing a native mobile feature with Capacitor: configuring the native project and handling the feature “the Capacitor way” within the app’s code.
Native Project Management
I’ll admit that I was initially skeptical about Capacitor’s approach to native project management. Despite Cordova's issues, I liked having a single configuration file to manage my native iOS and Android projects. Moving to Capacitor meant managing the native projects myself. Naturally, this was intimidating because I thought the whole point of the hybrid app approach was to avoid learning native app development. How much time would this take to learn? Ugh.
After trying it though, I was pleasantly surprised. Despite being only somewhat familiar with the native IDEs (Xcode and Android Studio), it turns out that the learning curve is quite small. You can go as shallow or deep into them as needed. Most of the time you just make small manual changes to AndroidManifest.xml
(Android) or Info.plist
(iOS).
When implementing complex mobile features (think: deep links, OAuth), you research the topic (example: “ios deep links” leads you to Apple’s docs) and follow the exact steps from the official documentation. Unlike Cordova, which abstracts these details away from you, features are implemented using the same instructions a native developer follows.
Implementing Features
Once configuration is complete, implementing the feature “the Capacitor way” isn’t all that challenging or “custom.” Depending on the use case, this could mean using a Capacitor Core API, a community plugin, or simply regular code. The effort varies, but generally, it’s straightforward.
As a bonus, if you do learn native mobile development someday (or build a Capacitor plugin), you’ll be better prepared because you understand the native ecosystem already.
Regardless of which cross-platform solution you choose, you have to learn mobile concepts anyway. Why not learn them the right way?
Stability
While we usually look forward to new software features and improvements, I dreaded updating my Cordova apps. Native plugins would constantly break between new mobile OS versions and conflicting plugin versions would mean I could update one plugin but not the other. Without a native development background, I got stuck often, forced to post on online forums and just hope for an answer.
While Capacitor doesn’t fully resolve the above challenges, it does a great job of equipping you to handle them. After just a bit of time developing apps with Capacitor, I feel more confident implementing features directly in each native project, as well as in Capacitor’s long-term stability as well.
Given that Capacitor gives you full control over native project management, many native features don’t require a plugin anymore (like deep linking - new guide coming soon!), so app size is reduced and performance is improved. Less code to maintain (especially if it’s not yours!) is a huge plus.
Also, most app features are configured once, then never touched again. And, given Apple and Google’s strong attention to backward compatibility, it could be years (if ever) before you need to make changes.
When you do run into issues while developing an app, it’s easy to search online for the answer. With no abstraction layer in place, you can search for and follow the answer as a native developer would. Personally, I feel much more confident in my ability to make changes and not get stuck.
Migration: Moving from Cordova to Capacitor
If by now you’re sold on giving Capacitor a try, you’ll be thrilled to learn that Capacitor has built-in Cordova migration support, designed to make the process as seamless as possible. Here’s a sampling of what it offers.
When you add a new platform (iOS, Android, etc.) to the project, a warning appears if an incompatible plugin is found. Most of the time, this is because Capacitor has an equivalent core plugin, or it simply isn’t needed anymore. Here’s cordova-plugin-splash-screen
after running ionic cap add ios
for example:
Found 1 incompatible Cordova plugin for ios, skipped install
cordova-plugin-splashscreen (5.0.2)
Also, if you install additional Cordova plugins at any time, then sync the project (this updates the native platforms and their dependencies), Capacitor tells you what you need to do with Cordova plugins that are supported but need additional native project configuration. Here’s the deep links plugin warning, for example:
$ ionic cap sync
[warn] Plugin @ionic-enterprise/deeplinks might require you to add
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>$URL_SCHEME</string>
</array>
</dict>
in the existing CFBundleURLTypes entry of your Info.plist to work
Cordova preferences are migrated over, too. When Capacitor is added to an existing Cordova project, it reads the <preferences>
in config.xml
and brings them into capacitor.config.json
. You can manually add more preferences to the cordova.preferences
object, too.
// capacitor.config.json
{
"cordova": {
"preferences": {
"ScrollEnabled": "false",
"android-minSdkVersion": "19",
}
}
This is just a sampling of how smooth the migration process is. See complete migration details here.
We’ve come a long way since I started building hybrid mobile apps years ago. These days, I’m more productive than ever and loving mobile development again.
What’s been your experience with Capacitor been like so far? Let us know in the comments below. If you haven't tried out Capacitor yet and you’d like to give it a try, check out our new tutorial here.