Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Capacitor: Bridging Web to Native

Hello! Last time, we built up your diagnostic toolkit for Android, covering how to troubleshoot common build failures in Gradle, the Android SDK, and the AndroidManifest.xml. This knowledge is crucial for maintaining the native layer of your application.

Now, we're going to shift our focus from the build process to the runtime architecture that makes Capacitor work. This lesson addresses the core of Capacitor's functionality: the plugin model. We will dissect how your React code, running in a simple WebView, can reach out and command native device hardware like the camera or biometrics sensor. Our goal is to explain how JavaScript calls are dispatched to native Swift and Kotlin code, a process that might feel conceptually similar to the bridge mechanisms in React Native or the IPC in Electron, both of which you've encountered before.

Understanding this bridge is fundamental to achieving your goal of integrating features like push notifications and filesystem access.

The Three Pillars of a Capacitor App

At its heart, a Capacitor application consists of three main components that work in concert: the native shell, the WebView, and the Capacitor bridge. You've already worked with the first two: the native shell is the Xcode or Android Studio project itself, and the WebView is the component inside that shell where your React SPA is rendered.

The missing piece is the bridge that connects them. The Capacitor team provides an excellent high-level diagram that illustrates this relationship.

This diagram shows how a Web App communicates with native plugins through the Capacitor Bridge and Runtime, all within your main App Project.

Let's break down what's happening here:

  1. Web App in WebView: Your React application (HTML, CSS, JS) runs inside a native WebView control. Think of a WebView as a lightweight, chromeless browser window embedded directly into the native app screen. This is a very similar concept to how Electron hosts web content.
  2. Capacitor Runtime & Bridge: This is the engine of Capacitor. It's a layer of native code that is injected into your app. Its primary jobs are to load plugins and facilitate communication between the WebView and the native code.
  3. Plugins: These are bundles of native code (Swift/Kotlin) that expose specific device features (like Camera, Filesystem, or Push Notifications) through a consistent JavaScript API.

The "magic" happens in how a call from your JavaScript code travels through the bridge to execute a function in a native plugin and get a result back.

The Journey of a Plugin Call

The process of calling a native feature from JavaScript is a well-defined, asynchronous message-passing flow. Let's trace this journey from start to finish. For a detailed overview of the bridge and runtime, the official "How Capacitor Works" blog post is an excellent resource.

Capacitor Blog - How Capacitor Works

This article provides a concise explanation of Capacitor's core architectural components.

Please read the sections Native Bridge and Runtime. As you read, focus on how the window.Capacitor.Plugins object is created and how the system manages asynchronous calls between the WebView and the native layer.

As you just read, the entire process is asynchronous. Let's formalize the step-by-step flow of a call like Camera.getPhoto():

  1. JavaScript Call: Your React component calls Camera.getPhoto(). The Camera object you import from a Capacitor package (e.g., @capacitor/camera) is not the native implementation itself. It's a JavaScript proxy object.
  2. Proxy & Serialization: This proxy object, created via a function called registerPlugin from @capacitor/core, intercepts the call. It packages the plugin name ("Camera"), the method name ("getPhoto"), and any options into a JSON object.
  3. Dispatch to Native: The proxy sends this serialized message across the JavaScript-to-native bridge. At this point, it returns a Promise to your web app, which will await a response.
  4. Native Runtime Routing: The Capacitor Runtime on the native side receives the message. It looks up the native plugin registered under the name "Camera" and finds the method corresponding to "getPhoto".
  5. Native Execution: The runtime invokes the actual native Swift or Kotlin function. This function then interacts with the underlying OS APIs to perform the requested action, such as opening the camera UI.
  6. Native Response: Once the native task is complete (e.g., the user takes a picture), the native function calls back to the Capacitor Runtime with the result (e.g., the image file path). This is typically done using a resolve method on a call object.
  7. Return to JavaScript: The Capacitor Runtime sends a response message back across the bridge to the WebView.
  8. Promise Resolution: The JavaScript-side of the bridge receives the response and uses it to resolve or reject the original Promise from step 3. Your await completes, and your React code now has the result from the native operation.

The Native Implementation: Swift and Kotlin/Java

To make this concrete, let's look at what the native side of a plugin actually looks like. You don't need to be a native developer to use plugins, but seeing the structure demystifies the process. Your extensive development background means you'll recognize the patterns of class extension and method decoration immediately.

The video "How to create project specific local plugin" provides a clear, practical demonstration of creating the native and web portions of a plugin.

How to create project specific local plugin | Ionic | Capacitor

This video walks through building a simple plugin from scratch. We will focus on the key parts that define the native interface.

Android (Java): Watch the segment from Android setup. Notice how the Java class extends Plugin, is annotated with @CapacitorPlugin, and the exposed method getHeight is annotated with @PluginMethod. The PluginCall object is the key to receiving data and sending a response with call.resolve(). iOS (Swift): Now watch the iOS portion from iOS setup. The pattern is analogous. The Swift class extends CAPPlugin, the class and method are marked with @objc, and the method receives a CAPPluginCall object used to resolve() the call. JavaScript Interface: Finally, see how the web interface is created from JS interface. The registerPlugin('MyStatusBar') call is what creates the JavaScript proxy object. The name "MyStatusBar" is the string key that links the JS object to the native classes you just saw.

The core components are:

  • For Android (Kotlin/Java):

    • A class that extends com.getcapacitor.Plugin.
    • An @CapacitorPlugin(name = "...") annotation on the class to register it with the runtime.
    • @PluginMethod annotations on public methods to expose them to the bridge.
    • Each method receives a PluginCall object, which is used to get parameters and return data via call.resolve() or call.reject().
  • For iOS (Swift):

    • A class that extends CAPPlugin.
    • An @objc decorator on the class.
    • An @objc decorator on each function exposed to the bridge.
    • Each function receives a CAPPluginCall object for parameters and returning data.
    • An additional Objective-C file (.m) is needed to bridge the Swift code, using macros like CAP_PLUGIN and CAP_PLUGIN_METHOD to register the plugin and its methods.

This consistent, symmetrical structure across both platforms is what allows a single JavaScript API call to work seamlessly on iOS and Android.

Contrasting with Your Experience: Electron and React Native

Given your background, it's useful to place Capacitor's model in context with other frameworks you've used.

  • Capacitor vs. Electron: Electron uses Inter-Process Communication (IPC) to send messages between the web-based renderer process (ipcRenderer) and the Node.js-based main process (ipcMain). This is architecturally very similar to Capacitor's bridge—both are asynchronous message-passing systems connecting a web environment to a more privileged backend. The key difference is the target: Electron's backend is a Node.js server environment for desktop apps, while Capacitor's is the native iOS/Android runtime for mobile apps. Capacitor's plugin model provides a more formalized contract (@PluginMethod, CAPPluginCall) compared to the more free-form event listeners of IPC.

  • Capacitor vs. React Native: React Native also uses a bridge to communicate between a JavaScript thread and the native platform. However, the purpose is fundamentally different. In React Native, your JavaScript code dictates the rendering of native UI components. When you write <View>, it becomes a native UIView or android.view.View. The bridge is constantly busy communicating layout and UI updates. In Capacitor, the UI is 100% web-based, living entirely within the WebView. The bridge is used only when you need to escape the web sandbox to access a native device service (like biometrics). This makes the Capacitor bridge much "quieter" and results in a different development experience: you build your UI with standard web tools (DOM, CSS), and only "call out" to native for specific services.

Conclusion

In this lesson, we've unpacked the mechanism that gives your web app its native superpowers. You now understand the complete lifecycle of a plugin call and the architecture that enables it.

Here are the key takeaways:

  • The Architecture: A Capacitor app is a native shell hosting a WebView. A Bridge and Runtime layer connects the two.
  • The Call Flow: A JavaScript call on a plugin proxy is serialized into a JSON message, sent across the bridge, and handled by the native runtime, which invokes the corresponding native Swift/Kotlin method. The result is sent back to resolve the original JavaScript Promise.
  • The Native Side: Native plugin classes on iOS and Android follow a clear, annotation-based pattern (@PluginMethod, @objc) and use a PluginCall object to manage the asynchronous communication back to the web view.
  • The Web Side: The @capacitor/core package's registerPlugin function is the key to creating the JavaScript proxy that initiates this entire process.

In our next lesson, we will put this knowledge into immediate practice. We'll install the @capacitor/push-notifications plugin, which will require us to both use its JavaScript API in our React code and perform the necessary native configuration in Xcode and Android Studio, perfectly illustrating the two sides of the bridge we've explored today.

Can't find a good explanation? Sign up and we'll make it for you

Sign up