Skip to content
rdlabo.devdocs

Cordova plugin integration

The same npm package, Swift library and Android AAR support Capacitor and Cordova. Engine is a library used by plugin maintainers, not a separately registered Cordova plugin. A Cordova application does not need @capacitor/core or a Capacitor native SDK.

The consuming plugin still owns its native view and the corresponding DOM, including placement, size and stacking order. Supply an existing selector such as native-ad or #player. The DOM may be added after initialization. Image preparation, scroll handling, overlays and restoration use the common Engine implementation.

Requirements

The integration is verified with cordova-ios 8.1.1 and cordova-android 15.1.0 using their standard WebViews. The Engine itself requires iOS 15+ and Android API 23+; the Cordova host or SDK may require a higher minimum. Composited Android capture requires API 26+.

Dependencies

Use the same Engine release for the Web package and native libraries. The API is pre-1.0; pin versions and retain dependency lockfiles.

npm install --save-exact @webview-native-ui/engine@0.0.1

Declare the native dependencies in the consuming plugin as well. npm installation alone does not link either native library.

For Swift Package Manager:

.package(url: "https://github.com/webview-native-ui/engine.git", exact: "0.0.1")
// In the plugin target's dependencies:
.product(name: "WebViewNativeUIEngine", package: "engine")

For the plugin's Android Gradle configuration:

dependencies {
    implementation 'io.github.webview-native-ui:engine:0.0.1'
}

Keep google() and mavenCentral() in the consuming build's dependency repositories.

iOS

In a Cordova SwiftPM plugin, declare both Cordova and Engine as dependencies of the plugin target; neither framework becomes a dependency of the Engine target. Keep the plugin's existing SwiftPM product and source-file declarations.

In the iOS platform section of plugin.xml, register the plugin for startup if its renderer should appear before the first application call:

<platform name="ios" package="swift">
    <config-file target="config.xml" parent="/*">
        <feature name="ExamplePlugin">
            <param name="ios-package" value="ExamplePlugin" />
            <param name="onload" value="true" />
        </feature>
    </config-file>
    <!-- Keep the plugin's existing source-file declarations. -->
</platform>

Merge these entries into the existing platform sections rather than adding duplicate sections. The integration fixture includes complete plugin.xml, SwiftPM and Gradle wiring.

The examples extend an existing renderer: nativeView is its attached, positioned view, not a view created by the Engine.

import Cordova
import WebKit
import WebViewNativeUIEngine

@objc(ExamplePlugin)
class ExamplePlugin: CDVPlugin {
    private var projection: NativeUIProjection?
    private var nativeView: UIView?

    override func pluginInitialize() {
        // Existing renderer creates, attaches and positions nativeView here.
        do {
            projection = try NativeUIProjection(
                webView: webView as? WKWebView,
                plugin: "ExamplePlugin",
                projectionSelector: "#player",
                receiveListeners: .init(
                    update: ["contentChanged"],
                    projection: .init(native: ["shown"], web: ["covered"], none: ["hidden"])
                ),
                runtime: .cordova,
                view: { [weak self] in self?.nativeView }
            )
        } catch {
            // Keep the existing Web fallback when registration is unavailable.
            NSLog("Native UI projection unavailable: %@", String(describing: error))
        }
    }

    override func dispose() {
        projection?.destroy()
        projection = nil
        super.dispose()
    }
}

If the iOS renderer loads later, create the registration after its view is attached, and destroy the old registration before replacing the renderer.

Forward the plugin's existing presentation/content notifications with projection?.receive("shown"), receive("hidden") or receive("contentChanged"). Put this alongside the existing callback/event delivery, or in the plugin's shared notification method. receive updates the Engine session only: it does not deliver a second application callback. A matching destination emitted before JS startup is retained for the initial connection.

Cordova iOS's built-in WkWebView.convertFilePath serves the prepared PNGs. Engine resolves this internally. No file plugin, additional permission or author-provided URL converter is required. The default allowed origins include app://localhost; pass the actual trusted origin if the app changes its scheme or hostname.

Android

Declare the Maven dependency in the existing Cordova plugin Gradle configuration. Also declare implementation 'androidx.webkit:webkit:1.14.0' when using the CordovaPluginPathHandler signature; Cordova's internal WebKit dependency is not exposed on the plugin compile classpath. In the Android platform section of the plugin's plugin.xml, merge this registration so initialization runs before the first WebView navigation:

<platform name="android">
    <config-file target="res/xml/config.xml" parent="/*">
        <feature name="ExamplePlugin">
            <param name="android-package" value="com.example.ExamplePlugin" />
            <param name="onload" value="true" />
        </feature>
    </config-file>
    <!-- Keep existing source-file and Gradle framework declarations. -->
</platform>
import android.view.View
import android.webkit.WebView
import io.github.webview_native_ui.engine.NativeUIProjection
import org.apache.cordova.CordovaPlugin
import org.apache.cordova.CordovaPluginPathHandler

class ExamplePlugin : CordovaPlugin() {
    private var projection: NativeUIProjection? = null
    private var nativeView: View? = null

    override fun pluginInitialize() {
        val web = webView.view as WebView
        projection = runCatching {
            // One origin-restricted channel per WebView, shared with later registrations.
            NativeUIProjection.install(web)
            // Existing renderer creates, attaches and positions nativeView here.
            NativeUIProjection(
                window = cordova.activity.window,
                webView = web,
                plugin = serviceName,
                projectionSelector = "#player",
                receiveListeners = NativeUIProjection.ReceiveListeners(
                    update = listOf("contentChanged"),
                    projection = NativeUIProjection.ReceiveListeners.Projection(
                        native = listOf("shown"), web = listOf("covered"), none = listOf("hidden")
                    )
                ),
                runtime = NativeUIProjection.Runtime.CORDOVA,
                view = { nativeView }
            )
        }.onFailure {
            android.util.Log.e(javaClass.simpleName, "Native UI projection unavailable", it)
        }.getOrNull()
    }

    override fun getPathHandler() = CordovaPluginPathHandler { path ->
        projection?.handleResource(path)
    }

    override fun onDestroy() {
        projection?.close()
        projection = null
        super.onDestroy()
    }
}

Use projection?.receive(eventName, data) at the existing notification point, as on iOS. Native view creation and placement remain on the UI thread. If the renderer loads asynchronously, keep install(web) in pluginInitialize() and create the registration after the view is ready. Close the old registration before replacing that renderer.

The PathHandler delegates same-origin image requests to Engine. It serves only the registration's PNG files in app-private cache storage; unrelated paths return null. If the plugin already has a handler, combine them with projection?.handleResource(path) ?: existingHandler.handle(path). For multiple registrations, delegate to the matching owner. Keep the handler installed for the WebView lifetime. No WebViewClient replacement or storage permission is needed.

The defaults support Cordova's https://localhost and http://localhost. If the hostname changes, pass the same explicit origin set to install and each registration. Wildcard origins and file:// app origins are not supported by this integration.

Lifetime and verification

Keep the registration for the WebView lifetime. Engine reconnects after a document reload and clears the old document's image state. If the plugin destroys its native UI in onReset, update its view provider and emit the appropriate destination; do not leave a registration pointing at a destroyed view. Dispose registrations when the plugin/WebView is destroyed.

Capacitor registrations retain their existing default. The Cordova integration requires the standard Cordova WebView resource handler. Running a Cordova plugin inside a Capacitor app is not covered by this integration; the host must use the Capacitor integration instead. See the shared DOM and native surface requirements for selector, layout and capture rules. Ionic overlay support remains optional; plain Cordova apps need not enable it.

The integration fixture uses Cordova CLI 13, cordova-ios 8.1.1 and cordova-android 15.1.0 with their standard WebViews. It checks actual PNG handoff, native restoration and hidden-state retention across document reload, not merely JS bridge mocks. Older Cordova releases and custom WebView engines require separate verification.

Run the real consumer checks with an already booted simulator/emulator:

npm run test:cordova -- ios SIMULATOR_UDID
npm run test:cordova -- android emulator-5554

The script creates a disposable app, uses local SwiftPM/Maven Engine artifacts, and records the application result in the printed temporary directory. It never publishes to npm or Maven. Android requires JDK 21+, SDK 36 and ANDROID_HOME; iOS requires Xcode.